""" Arcádia People - Motor de RH (HRM) Serviço FastAPI para gestão de recursos humanos """ from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from typing import Optional, List, Dict, Any from datetime import datetime, date from decimal import Decimal import json import os app = FastAPI( title="Arcádia People", description="Motor de RH - Folha de Pagamento, Ponto, Férias, eSocial", version="1.0.0" ) app.add_middleware( CORSMiddleware, allow_origins=[os.getenv("APP_URL", "http://localhost:5000")], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ========== Tabelas de Cálculo (2024) ========== TABELA_INSS_2024 = [ {"faixaInicio": 0, "faixaFim": 1412.00, "aliquota": 7.5}, {"faixaInicio": 1412.01, "faixaFim": 2666.68, "aliquota": 9.0}, {"faixaInicio": 2666.69, "faixaFim": 4000.03, "aliquota": 12.0}, {"faixaInicio": 4000.04, "faixaFim": 7786.02, "aliquota": 14.0}, ] TETO_INSS_2024 = 7786.02 DEDUCAO_DEPENDENTE_IRRF = 189.59 TABELA_IRRF_2024 = [ {"faixaInicio": 0, "faixaFim": 2259.20, "aliquota": 0, "deducao": 0}, {"faixaInicio": 2259.21, "faixaFim": 2826.65, "aliquota": 7.5, "deducao": 169.44}, {"faixaInicio": 2826.66, "faixaFim": 3751.05, "aliquota": 15.0, "deducao": 381.44}, {"faixaInicio": 3751.06, "faixaFim": 4664.68, "aliquota": 22.5, "deducao": 662.77}, {"faixaInicio": 4664.69, "faixaFim": 999999999, "aliquota": 27.5, "deducao": 896.00}, ] SALARIO_FAMILIA_2024 = [ {"faixaFim": 1819.26, "valor": 62.04}, ] # ========== Models ========== class Funcionario(BaseModel): nome: str cpf: str salario: float dataAdmissao: str cargoId: Optional[int] = None departamentoId: Optional[int] = None tipoContrato: str = "clt" jornadaTrabalho: str = "44h" class CalculoFolhaRequest(BaseModel): funcionarioId: int competencia: str # YYYY-MM salarioBase: float diasTrabalhados: int = 30 horasExtras50: float = 0 horasExtras100: float = 0 horasNoturnas: float = 0 faltas: int = 0 atrasos: float = 0 dependentesIrrf: int = 0 descontosAdicionais: List[Dict[str, Any]] = [] proventosAdicionais: List[Dict[str, Any]] = [] class CalculoFeriasRequest(BaseModel): funcionarioId: int salarioBase: float diasFerias: int = 30 diasAbono: int = 0 mediaHorasExtras: float = 0 dependentesIrrf: int = 0 class CalculoRescisaoRequest(BaseModel): funcionarioId: int salarioBase: float dataAdmissao: str dataDemissao: str tipoRescisao: str # sem_justa_causa, justa_causa, pedido_demissao saldoFerias: int = 0 avisoPrevio: str = "trabalhado" # trabalhado, indenizado, dispensado dependentesIrrf: int = 0 class PontoRequest(BaseModel): funcionarioId: int data: str entrada1: str saida1: str entrada2: Optional[str] = None saida2: Optional[str] = None jornadaDiaria: float = 8.0 # ========== Funções de Cálculo ========== def calcular_inss(salario_bruto: float) -> dict: """ Calcula INSS progressivo (2024) conforme Portaria MPS Tabela INSS 2024: Faixa 1: Até R$ 1.412,00 = 7,5% Faixa 2: De R$ 1.412,01 a R$ 2.666,68 = 9% Faixa 3: De R$ 2.666,69 a R$ 4.000,03 = 12% Faixa 4: De R$ 4.000,04 a R$ 7.786,02 = 14% Cálculo progressivo: cada faixa incide apenas sobre o valor dentro dela. """ if salario_bruto > TETO_INSS_2024: base_calculo = TETO_INSS_2024 else: base_calculo = salario_bruto inss_total = 0.0 detalhamento = [] # Limites exatos das faixas para cálculo progressivo faixas = [ {"limite_inferior": 0, "limite_superior": 1412.00, "aliquota": 7.5}, {"limite_inferior": 1412.00, "limite_superior": 2666.68, "aliquota": 9.0}, {"limite_inferior": 2666.68, "limite_superior": 4000.03, "aliquota": 12.0}, {"limite_inferior": 4000.03, "limite_superior": 7786.02, "aliquota": 14.0}, ] for i, faixa in enumerate(faixas): limite_inferior = faixa["limite_inferior"] limite_superior = faixa["limite_superior"] aliquota = faixa["aliquota"] if base_calculo <= limite_inferior: break # Base da faixa: min(salário, limite_superior) - limite_inferior teto_faixa = min(base_calculo, limite_superior) base_faixa = teto_faixa - limite_inferior if base_faixa > 0: valor_faixa = base_faixa * (aliquota / 100) inss_total += valor_faixa detalhamento.append({ "faixa": i + 1, "limiteInferior": limite_inferior, "limiteSuperior": limite_superior, "base": round(base_faixa, 2), "aliquota": aliquota, "valor": round(valor_faixa, 2) }) return { "baseCalculo": round(base_calculo, 2), "valorInss": round(inss_total, 2), "tetoAplicado": salario_bruto > TETO_INSS_2024, "detalhamento": detalhamento } def calcular_irrf(base_irrf: float, dependentes: int = 0) -> dict: """Calcula IRRF (2024)""" deducao_dependentes = dependentes * DEDUCAO_DEPENDENTE_IRRF base_calculo = base_irrf - deducao_dependentes if base_calculo < 0: base_calculo = 0 irrf = 0 aliquota_efetiva = 0 faixa_aplicada = 0 for i, faixa in enumerate(TABELA_IRRF_2024): if base_calculo >= faixa["faixaInicio"] and base_calculo <= faixa["faixaFim"]: irrf = (base_calculo * faixa["aliquota"] / 100) - faixa["deducao"] aliquota_efetiva = faixa["aliquota"] faixa_aplicada = i + 1 break if irrf < 0: irrf = 0 return { "baseCalculo": round(base_calculo, 2), "deducaoDependentes": round(deducao_dependentes, 2), "aliquotaEfetiva": aliquota_efetiva, "faixaAplicada": faixa_aplicada, "valorIrrf": round(irrf, 2) } def calcular_fgts(base_fgts: float) -> dict: """Calcula FGTS (8%)""" valor_fgts = base_fgts * 0.08 return { "baseCalculo": round(base_fgts, 2), "aliquota": 8.0, "valorFgts": round(valor_fgts, 2) } def calcular_salario_familia(salario: float, filhos_menores: int) -> dict: """Calcula Salário Família""" valor_cota = 0 for faixa in SALARIO_FAMILIA_2024: if salario <= faixa["faixaFim"]: valor_cota = faixa["valor"] break valor_total = valor_cota * filhos_menores return { "salarioBase": round(salario, 2), "filhosMenores": filhos_menores, "valorCota": valor_cota, "valorTotal": round(valor_total, 2), "temDireito": valor_cota > 0 } def calcular_horas_trabalhadas(entrada1: str, saida1: str, entrada2: str = None, saida2: str = None) -> dict: """Calcula horas trabalhadas a partir dos registros de ponto""" def time_to_minutes(time_str: str) -> int: h, m = map(int, time_str.split(":")) return h * 60 + m minutos_periodo1 = time_to_minutes(saida1) - time_to_minutes(entrada1) minutos_periodo2 = 0 if entrada2 and saida2: minutos_periodo2 = time_to_minutes(saida2) - time_to_minutes(entrada2) total_minutos = minutos_periodo1 + minutos_periodo2 total_horas = total_minutos / 60 return { "periodo1": round(minutos_periodo1 / 60, 2), "periodo2": round(minutos_periodo2 / 60, 2), "totalHoras": round(total_horas, 2), "totalMinutos": total_minutos } # ========== Endpoints ========== @app.get("/") async def root(): return { "service": "Arcádia People", "version": "1.0.0", "status": "running", "modules": ["funcionarios", "folha", "ferias", "ponto", "beneficios", "esocial"] } @app.get("/health") async def health(): return {"status": "healthy", "timestamp": datetime.now().isoformat()} @app.get("/tabelas/inss") async def get_tabela_inss(): """Retorna tabela INSS atual""" return { "vigencia": "2024", "teto": TETO_INSS_2024, "faixas": TABELA_INSS_2024 } @app.get("/tabelas/irrf") async def get_tabela_irrf(): """Retorna tabela IRRF atual""" return { "vigencia": "2024", "deducaoDependente": DEDUCAO_DEPENDENTE_IRRF, "faixas": TABELA_IRRF_2024 } @app.get("/tabelas/salario-familia") async def get_tabela_salario_familia(): """Retorna tabela Salário Família atual""" return { "vigencia": "2024", "faixas": SALARIO_FAMILIA_2024 } @app.post("/calcular/inss") async def calcular_inss_endpoint(salario: float): """Calcula INSS para um salário""" return calcular_inss(salario) @app.post("/calcular/irrf") async def calcular_irrf_endpoint(base: float, dependentes: int = 0): """Calcula IRRF para uma base""" return calcular_irrf(base, dependentes) @app.post("/calcular/folha") async def calcular_folha(request: CalculoFolhaRequest): """Calcula folha de pagamento completa para um funcionário""" # Cálculo do salário proporcional salario_proporcional = request.salarioBase * (request.diasTrabalhados / 30) # Desconto de faltas valor_dia = request.salarioBase / 30 desconto_faltas = valor_dia * request.faltas # Horas extras valor_hora = request.salarioBase / 220 # 220 horas mensais padrão valor_he_50 = request.horasExtras50 * (valor_hora * 1.5) valor_he_100 = request.horasExtras100 * (valor_hora * 2.0) # Adicional noturno (20%) valor_adicional_noturno = request.horasNoturnas * (valor_hora * 0.2) # Total de proventos proventos_adicionais = sum(p.get("valor", 0) for p in request.proventosAdicionais) total_proventos = ( salario_proporcional + valor_he_50 + valor_he_100 + valor_adicional_noturno + proventos_adicionais ) # INSS inss_calc = calcular_inss(total_proventos) valor_inss = inss_calc["valorInss"] # IRRF (base = bruto - INSS) base_irrf = total_proventos - valor_inss irrf_calc = calcular_irrf(base_irrf, request.dependentesIrrf) valor_irrf = irrf_calc["valorIrrf"] # FGTS fgts_calc = calcular_fgts(total_proventos) valor_fgts = fgts_calc["valorFgts"] # Descontos adicionais descontos_adicionais = sum(d.get("valor", 0) for d in request.descontosAdicionais) # Total de descontos total_descontos = valor_inss + valor_irrf + desconto_faltas + descontos_adicionais # Líquido total_liquido = total_proventos - total_descontos return { "funcionarioId": request.funcionarioId, "competencia": request.competencia, "proventos": { "salarioBase": round(request.salarioBase, 2), "salarioProporcional": round(salario_proporcional, 2), "horasExtras50": { "horas": request.horasExtras50, "valor": round(valor_he_50, 2) }, "horasExtras100": { "horas": request.horasExtras100, "valor": round(valor_he_100, 2) }, "adicionalNoturno": { "horas": request.horasNoturnas, "valor": round(valor_adicional_noturno, 2) }, "adicionais": request.proventosAdicionais, "total": round(total_proventos, 2) }, "descontos": { "inss": inss_calc, "irrf": irrf_calc, "faltas": { "dias": request.faltas, "valor": round(desconto_faltas, 2) }, "adicionais": request.descontosAdicionais, "total": round(total_descontos, 2) }, "encargos": { "fgts": fgts_calc, "inssPatronal": { "aliquota": 20.0, "valor": round(total_proventos * 0.20, 2) } }, "resumo": { "totalProventos": round(total_proventos, 2), "totalDescontos": round(total_descontos, 2), "totalLiquido": round(total_liquido, 2), "custoEmpresa": round(total_proventos + valor_fgts + (total_proventos * 0.20), 2) } } @app.post("/calcular/ferias") async def calcular_ferias(request: CalculoFeriasRequest): """Calcula férias""" # Valor base das férias valor_ferias = request.salarioBase * (request.diasFerias / 30) # Adicional de 1/3 terco_constitucional = valor_ferias / 3 # Abono pecuniário (venda de dias) valor_abono = 0 terco_abono = 0 if request.diasAbono > 0: valor_abono = request.salarioBase * (request.diasAbono / 30) terco_abono = valor_abono / 3 # Média de horas extras media_he = request.mediaHorasExtras # Total bruto total_bruto = valor_ferias + terco_constitucional + valor_abono + terco_abono + media_he # INSS sobre férias inss_calc = calcular_inss(total_bruto) valor_inss = inss_calc["valorInss"] # IRRF base_irrf = total_bruto - valor_inss irrf_calc = calcular_irrf(base_irrf, request.dependentesIrrf) valor_irrf = irrf_calc["valorIrrf"] # Líquido total_liquido = total_bruto - valor_inss - valor_irrf return { "funcionarioId": request.funcionarioId, "diasFerias": request.diasFerias, "diasAbono": request.diasAbono, "proventos": { "ferias": round(valor_ferias, 2), "tercoConstitucional": round(terco_constitucional, 2), "abonoPecuniario": round(valor_abono, 2), "tercoAbono": round(terco_abono, 2), "mediaHorasExtras": round(media_he, 2), "total": round(total_bruto, 2) }, "descontos": { "inss": inss_calc, "irrf": irrf_calc, "total": round(valor_inss + valor_irrf, 2) }, "resumo": { "totalBruto": round(total_bruto, 2), "totalDescontos": round(valor_inss + valor_irrf, 2), "totalLiquido": round(total_liquido, 2) } } @app.post("/calcular/rescisao") async def calcular_rescisao(request: CalculoRescisaoRequest): """Calcula rescisão de contrato de trabalho""" from datetime import datetime data_admissao = datetime.strptime(request.dataAdmissao, "%Y-%m-%d") data_demissao = datetime.strptime(request.dataDemissao, "%Y-%m-%d") # Tempo de serviço dias_trabalhados = (data_demissao - data_admissao).days meses_trabalhados = dias_trabalhados / 30 anos_trabalhados = dias_trabalhados / 365 # Saldo de salário (dias do mês da demissão) dia_demissao = data_demissao.day saldo_salario = request.salarioBase * (dia_demissao / 30) # 13º proporcional meses_13 = data_demissao.month decimo_terceiro = (request.salarioBase / 12) * meses_13 # Férias proporcionais + 1/3 meses_ferias = int(meses_trabalhados % 12) if meses_ferias == 0: meses_ferias = int(meses_trabalhados) ferias_proporcionais = (request.salarioBase / 12) * meses_ferias terco_ferias = ferias_proporcionais / 3 # Férias vencidas ferias_vencidas = request.salarioBase * (request.saldoFerias / 30) terco_vencidas = ferias_vencidas / 3 # Aviso prévio aviso_previo = 0 dias_aviso = 30 + (3 * int(anos_trabalhados)) if dias_aviso > 90: dias_aviso = 90 if request.tipoRescisao == "sem_justa_causa" and request.avisoPrevio == "indenizado": aviso_previo = request.salarioBase * (dias_aviso / 30) # Multa FGTS (40% ou 20%) # Estimativa do saldo FGTS (8% do salário * meses) saldo_fgts_estimado = (request.salarioBase * 0.08) * meses_trabalhados multa_fgts = 0 if request.tipoRescisao == "sem_justa_causa": multa_fgts = saldo_fgts_estimado * 0.40 elif request.tipoRescisao == "acordo": multa_fgts = saldo_fgts_estimado * 0.20 # Total bruto total_bruto = ( saldo_salario + decimo_terceiro + ferias_proporcionais + terco_ferias + ferias_vencidas + terco_vencidas + aviso_previo ) # Descontos inss_calc = calcular_inss(saldo_salario) irrf_calc = calcular_irrf(total_bruto - inss_calc["valorInss"], request.dependentesIrrf) total_descontos = inss_calc["valorInss"] + irrf_calc["valorIrrf"] total_liquido = total_bruto - total_descontos + multa_fgts return { "funcionarioId": request.funcionarioId, "tipoRescisao": request.tipoRescisao, "tempoServico": { "dias": dias_trabalhados, "meses": round(meses_trabalhados, 1), "anos": round(anos_trabalhados, 1) }, "verbas": { "saldoSalario": round(saldo_salario, 2), "decimoTerceiro": round(decimo_terceiro, 2), "feriasProporcionais": round(ferias_proporcionais, 2), "tercoFerias": round(terco_ferias, 2), "feriasVencidas": round(ferias_vencidas, 2), "tercoVencidas": round(terco_vencidas, 2), "avisoPrevio": { "tipo": request.avisoPrevio, "dias": dias_aviso, "valor": round(aviso_previo, 2) }, "totalBruto": round(total_bruto, 2) }, "descontos": { "inss": inss_calc, "irrf": irrf_calc, "total": round(total_descontos, 2) }, "fgts": { "saldoEstimado": round(saldo_fgts_estimado, 2), "multa": round(multa_fgts, 2), "percentualMulta": 40 if request.tipoRescisao == "sem_justa_causa" else 20 if request.tipoRescisao == "acordo" else 0 }, "resumo": { "totalBruto": round(total_bruto, 2), "totalDescontos": round(total_descontos, 2), "multaFgts": round(multa_fgts, 2), "totalLiquido": round(total_liquido, 2) } } @app.post("/calcular/ponto") async def calcular_ponto(request: PontoRequest): """Calcula horas trabalhadas e extras""" horas = calcular_horas_trabalhadas( request.entrada1, request.saida1, request.entrada2, request.saida2 ) jornada_diaria = request.jornadaDiaria horas_trabalhadas = horas["totalHoras"] horas_extras = max(0, horas_trabalhadas - jornada_diaria) horas_faltantes = max(0, jornada_diaria - horas_trabalhadas) # Verificar adicional noturno (22h às 5h) horas_noturnas = 0 # Simplificado - precisaria de lógica mais complexa return { "funcionarioId": request.funcionarioId, "data": request.data, "registros": { "entrada1": request.entrada1, "saida1": request.saida1, "entrada2": request.entrada2, "saida2": request.saida2 }, "calculo": { "jornadaDiaria": jornada_diaria, "horasTrabalhadas": round(horas_trabalhadas, 2), "horasExtras": round(horas_extras, 2), "horasFaltantes": round(horas_faltantes, 2), "horasNoturnas": round(horas_noturnas, 2) }, "detalhamento": horas } @app.get("/esocial/eventos") async def get_eventos_esocial(): """Lista eventos do eSocial suportados""" eventos = [ {"codigo": "S-1000", "descricao": "Informações do Empregador", "tipo": "tabela"}, {"codigo": "S-1005", "descricao": "Tabela de Estabelecimentos", "tipo": "tabela"}, {"codigo": "S-1010", "descricao": "Tabela de Rubricas", "tipo": "tabela"}, {"codigo": "S-1020", "descricao": "Tabela de Lotações Tributárias", "tipo": "tabela"}, {"codigo": "S-1030", "descricao": "Tabela de Cargos/Empregos Públicos", "tipo": "tabela"}, {"codigo": "S-1040", "descricao": "Tabela de Funções/Cargos em Comissão", "tipo": "tabela"}, {"codigo": "S-1050", "descricao": "Tabela de Horários/Turnos de Trabalho", "tipo": "tabela"}, {"codigo": "S-1070", "descricao": "Tabela de Processos Administrativos/Judiciais", "tipo": "tabela"}, {"codigo": "S-2190", "descricao": "Registro Preliminar de Trabalhador", "tipo": "nao_periodico"}, {"codigo": "S-2200", "descricao": "Cadastramento Inicial / Admissão", "tipo": "nao_periodico"}, {"codigo": "S-2205", "descricao": "Alteração de Dados Cadastrais", "tipo": "nao_periodico"}, {"codigo": "S-2206", "descricao": "Alteração de Contrato de Trabalho", "tipo": "nao_periodico"}, {"codigo": "S-2230", "descricao": "Afastamento Temporário", "tipo": "nao_periodico"}, {"codigo": "S-2299", "descricao": "Desligamento", "tipo": "nao_periodico"}, {"codigo": "S-2300", "descricao": "Trabalhador Sem Vínculo - Início", "tipo": "nao_periodico"}, {"codigo": "S-2306", "descricao": "Trabalhador Sem Vínculo - Alteração", "tipo": "nao_periodico"}, {"codigo": "S-2399", "descricao": "Trabalhador Sem Vínculo - Término", "tipo": "nao_periodico"}, {"codigo": "S-1200", "descricao": "Remuneração de Trabalhador", "tipo": "periodico"}, {"codigo": "S-1210", "descricao": "Pagamentos de Rendimentos", "tipo": "periodico"}, {"codigo": "S-1260", "descricao": "Comercialização da Produção Rural", "tipo": "periodico"}, {"codigo": "S-1270", "descricao": "Contratação de Trabalhadores Avulsos", "tipo": "periodico"}, {"codigo": "S-1280", "descricao": "Informações Complementares aos Eventos Periódicos", "tipo": "periodico"}, {"codigo": "S-1298", "descricao": "Reabertura dos Eventos Periódicos", "tipo": "periodico"}, {"codigo": "S-1299", "descricao": "Fechamento dos Eventos Periódicos", "tipo": "periodico"}, ] return { "eventos": eventos, "total": len(eventos) } @app.post("/esocial/gerar-xml/{evento}") async def gerar_xml_esocial(evento: str, dados: dict): """Gera XML para evento do eSocial""" # Estrutura básica do XML do eSocial xml_estrutura = { "evento": evento, "versao": "S-1.2", "ambiente": dados.get("ambiente", "2"), # 1=Produção, 2=Homologação "dados": dados, "status": "estrutura_gerada", "observacao": "XML será gerado conforme leiaute oficial do eSocial" } return xml_estrutura if __name__ == "__main__": import uvicorn port = int(os.environ.get("PEOPLE_PORT", 8004)) uvicorn.run(app, host="0.0.0.0", port=port)