diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..96719552cc6f1ea259db927aa1015807d0213ee3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,115 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Virtual environment +venv/ +.env/ +.venv/ +env/ + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +coverage.xml + +# Jupyter Notebook +.ipynb_checkpoints + +# Environments +.env +.venv +env/ + +# IDE specific files +.vscode/ +.idea/ +*.swp +*.swo + +# Build artifacts +dist/ +*.egg-info/ +*.egg +*.manifest +*.spec + +# Ignore only PyInstaller/setuptools build output inside build/, not our scripts +build/lib/ +build/bdist*/ +build/temp*/ + +# CLI binaries are tracked so Render can serve them for download +!app/cli/dist/ +!app/cli/dist/agents-linux +!app/cli/dist/agents-windows.exe + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# pyenv +.python-version + +# mypy +.mypy_cache/ +dmypy.json + +# PyCharm +.idea/ + +# VS Code +.vscode/ +*.code-workspace + +# macOS +.DS_Store + +# Windows +Thumbs.db + +# Database files +*.db +*.sqlite + +# Log files +*.log + +# Sphinx documentation +docs/_build/ + +# PyInstaller +*.spec + +# IPython +profile_default/ +ipython_config.py + +# Poetry +poetry.lock + +# PDM +__pypackages__/ +.pdm.toml + +# pytype +.pytype/ + +# Cython debug symbols +cython_debug/ +# Generated at build time +app/cli/_build_config.py diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a22720caca86807fb33be4d5293a3aa6cfd53693 Binary files /dev/null and b/__pycache__/main.cpython-312.pyc differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..6357e2fa7ea2c42cda17eb2ca48197be90099a16 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,43 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.backends.legacy:build" + +[project] +name = "llm-sped-fiscal" +version = "0.1.0" +description = "Sistema LLM para gestão de obrigações fiscais brasileiras (SPED)" +requires-python = ">=3.12" +license = {text = "MIT"} +dependencies = [ + "torch>=2.3.0", + "numpy>=1.26.0", + "pydantic>=2.7.0", + "httpx>=0.27.0", + "aiofiles>=23.2.1", + "python-dateutil>=2.9.0", + "cryptography>=42.0.0", + "lxml>=5.2.0", + "python-dotenv>=1.0.0", + "requests>=2.31.0", + "zeep>=4.2.1", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.2.0", + "pytest-asyncio>=0.23.0", + "ruff>=0.4.0", + "mypy>=1.10.0", +] + +[tool.setuptools.packages.find] +where = ["."] +include = ["src*"] + +[tool.ruff] +line-length = 100 +target-version = "py312" + +[tool.mypy] +python_version = "3.12" +strict = true diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..f1f42f73ba1736efe891b55bbb2b49dbdcc64227 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,18 @@ +torch>=2.3.0 +numpy>=1.26.0 +pydantic>=2.7.0 +httpx>=0.27.0 +aiofiles>=23.2.1 +rich>=13.7.0 +typer>=0.12.0 +python-dateutil>=2.9.0 +cryptography>=42.0.0 +lxml>=5.2.0 +python-dotenv>=1.0.0 +sentencepiece>=0.2.0 +requests>=2.31.0 +zeep>=4.2.1 +# testes +pytest>=8.0.0 +pytest-asyncio>=0.23.0 +pytest-cov>=5.0.0 diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f0d55e791dd3bb88c017776444c5f9ebe10f8765 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,10 @@ +""" +LLM Fiscal Brasileiro — Sistema de Obrigações SPED com PyTorch. + +Ponto de entrada: + from src import PipelineFiscal + pipeline = PipelineFiscal() +""" +from src.models.fiscal_llm import PipelineFiscal, TransformerFiscal, TokenizadorFiscal + +__all__ = ["PipelineFiscal", "TransformerFiscal", "TokenizadorFiscal"] diff --git a/src/__pycache__/__init__.cpython-312.pyc b/src/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9774769f0cb2480db85a33d9d82d6571f47e536d Binary files /dev/null and b/src/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/calculators/__init__.py b/src/calculators/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9fa44bcb363813e7a493e305899408dd550fd832 --- /dev/null +++ b/src/calculators/__init__.py @@ -0,0 +1,29 @@ +"""Calculadoras de tributos brasileiros.""" +from __future__ import annotations + +from dataclasses import dataclass +from decimal import Decimal, ROUND_HALF_UP + + +def arred(valor: Decimal, casas: int = 2) -> Decimal: + quantizador = Decimal(10) ** -casas + return valor.quantize(quantizador, rounding=ROUND_HALF_UP) + + +@dataclass +class ResultadoCalculo: + base_calculo: Decimal + aliquota: Decimal + valor_tributo: Decimal + deducoes: Decimal = Decimal("0") + creditos: Decimal = Decimal("0") + valor_a_recolher: Decimal = Decimal("0") + detalhes: dict = None + + def __post_init__(self): + if self.detalhes is None: + self.detalhes = {} + if self.valor_a_recolher == Decimal("0"): + self.valor_a_recolher = max( + Decimal("0"), self.valor_tributo - self.creditos - self.deducoes + ) diff --git a/src/calculators/__pycache__/__init__.cpython-312.pyc b/src/calculators/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1545b5c0262ecddfd59d19f8fbf181d7d155d599 Binary files /dev/null and b/src/calculators/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/calculators/__pycache__/icms.cpython-312.pyc b/src/calculators/__pycache__/icms.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..46ce4cc76f47cae4ffd9cdd5a4748f42083f888a Binary files /dev/null and b/src/calculators/__pycache__/icms.cpython-312.pyc differ diff --git a/src/calculators/__pycache__/ipi.cpython-312.pyc b/src/calculators/__pycache__/ipi.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d2d92bfbb6ec30e9697385f7b8a2924de34a55dc Binary files /dev/null and b/src/calculators/__pycache__/ipi.cpython-312.pyc differ diff --git a/src/calculators/__pycache__/irpj_csll.cpython-312.pyc b/src/calculators/__pycache__/irpj_csll.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..41876e874b6be65d43a3aed606275aed7e59a5ab Binary files /dev/null and b/src/calculators/__pycache__/irpj_csll.cpython-312.pyc differ diff --git a/src/calculators/__pycache__/iss.cpython-312.pyc b/src/calculators/__pycache__/iss.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c8b1c1c2b620dfa96ee27ab5db87444fc2548560 Binary files /dev/null and b/src/calculators/__pycache__/iss.cpython-312.pyc differ diff --git a/src/calculators/__pycache__/pis_cofins.cpython-312.pyc b/src/calculators/__pycache__/pis_cofins.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5c0933dfd5592c87b11def50677808e157661c0a Binary files /dev/null and b/src/calculators/__pycache__/pis_cofins.cpython-312.pyc differ diff --git a/src/calculators/__pycache__/simples_nacional.cpython-312.pyc b/src/calculators/__pycache__/simples_nacional.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..970e305fbc8743c34297a836ece1f1313b65befd Binary files /dev/null and b/src/calculators/__pycache__/simples_nacional.cpython-312.pyc differ diff --git a/src/calculators/icms.py b/src/calculators/icms.py new file mode 100644 index 0000000000000000000000000000000000000000..9291fb83185570ce29bc0598964b4e4df94ba0a4 --- /dev/null +++ b/src/calculators/icms.py @@ -0,0 +1,137 @@ +"""Calculadora de ICMS com suporte a ST, DIFAL e FCP.""" +from __future__ import annotations + +from dataclasses import dataclass, field +from decimal import Decimal +from typing import Optional + +from src.calculators import ResultadoCalculo, arred +from src.fiscal.constants import ALIQ_ICMS_INTERNA, aliq_icms_interestadual + + +@dataclass +class ParametrosICMS: + valor_mercadoria: Decimal + aliquota: Decimal + cst: str = "000" + uf_origem: str = "SP" + uf_destino: str = "SP" + reducao_base: Decimal = Decimal("0") + frete: Decimal = Decimal("0") + seguro: Decimal = Decimal("0") + outras_despesas: Decimal = Decimal("0") + desconto: Decimal = Decimal("0") + # Substituição Tributária + aliq_st: Optional[Decimal] = None + mva: Optional[Decimal] = None + base_st_retido: Decimal = Decimal("0") + # FCP (Fundo de Combate à Pobreza) + aliq_fcp: Decimal = Decimal("0") + aliq_fcp_st: Decimal = Decimal("0") + # DIFAL (Diferencial de Alíquota - EMENDA 87) + calcular_difal: bool = False + consumidor_final: bool = False + + +@dataclass +class ResultadoICMS: + base_calculo: Decimal = Decimal("0") + aliquota: Decimal = Decimal("0") + valor_icms: Decimal = Decimal("0") + # ST + base_st: Decimal = Decimal("0") + aliq_st: Decimal = Decimal("0") + valor_st: Decimal = Decimal("0") + # FCP + base_fcp: Decimal = Decimal("0") + valor_fcp: Decimal = Decimal("0") + valor_fcp_st: Decimal = Decimal("0") + # DIFAL + valor_difal_origem: Decimal = Decimal("0") + valor_difal_destino: Decimal = Decimal("0") + # Total + valor_total_icms: Decimal = Decimal("0") + + +def calcular_icms(p: ParametrosICMS) -> ResultadoICMS: + r = ResultadoICMS() + + if p.cst in {"040", "041", "050", "051", "060"}: + return r + + base_bruta = ( + p.valor_mercadoria + p.frete + p.seguro + p.outras_despesas - p.desconto + ) + + if p.cst in {"020", "070"}: + r.base_calculo = arred(base_bruta * (1 - p.reducao_base / 100)) + else: + r.base_calculo = arred(base_bruta) + + r.aliquota = p.aliquota + r.valor_icms = arred(r.base_calculo * p.aliquota / 100) + + # FCP + if p.aliq_fcp > 0: + r.base_fcp = r.base_calculo + r.valor_fcp = arred(r.base_fcp * p.aliq_fcp / 100) + + # Substituição Tributária + if p.cst in {"010", "030", "070"} and p.aliq_st is not None and p.mva is not None: + aliq_interna_dest = ALIQ_ICMS_INTERNA.get(p.uf_destino, Decimal("18")) + r.base_st = arred(r.base_calculo * (1 + p.mva / 100)) + r.aliq_st = aliq_interna_dest + icms_proprio_dest = arred(r.base_st * aliq_interna_dest / 100) + r.valor_st = max(Decimal("0"), arred(icms_proprio_dest - r.valor_icms)) + if p.aliq_fcp_st > 0: + r.valor_fcp_st = arred(r.base_st * p.aliq_fcp_st / 100) + + # DIFAL (EC 87/2015) para operações interestaduais para consumidor final + if p.calcular_difal and p.consumidor_final and p.uf_origem != p.uf_destino: + aliq_interestadual = aliq_icms_interestadual(p.uf_origem, p.uf_destino) + aliq_interna_dest = ALIQ_ICMS_INTERNA.get(p.uf_destino, Decimal("18")) + difal = arred(r.base_calculo * (aliq_interna_dest - aliq_interestadual) / 100) + # Partilha DIFAL: 2019+ => 100% destino + r.valor_difal_destino = arred(difal * Decimal("0.60")) + r.valor_difal_origem = arred(difal * Decimal("0.40")) + + r.valor_total_icms = arred( + r.valor_icms + r.valor_st + r.valor_fcp + r.valor_fcp_st + + r.valor_difal_destino + r.valor_difal_origem + ) + return r + + +def calcular_icms_credito_entrada( + valor_nf: Decimal, + aliquota: Decimal, + percentual_aproveitamento: Decimal = Decimal("100"), +) -> Decimal: + credito_bruto = arred(valor_nf * aliquota / 100) + return arred(credito_bruto * percentual_aproveitamento / 100) + + +def calcular_icms_apuracao( + debitos: list[Decimal], + creditos: list[Decimal], + saldo_anterior: Decimal = Decimal("0"), +) -> ResultadoCalculo: + total_debitos = arred(sum(debitos)) + total_creditos = arred(sum(creditos) + saldo_anterior) + saldo = total_creditos - total_debitos + valor_a_recolher = max(Decimal("0"), arred(total_debitos - total_creditos)) + saldo_credor = max(Decimal("0"), arred(total_creditos - total_debitos)) + return ResultadoCalculo( + base_calculo=total_debitos, + aliquota=Decimal("0"), + valor_tributo=total_debitos, + deducoes=Decimal("0"), + creditos=total_creditos, + valor_a_recolher=valor_a_recolher, + detalhes={ + "total_debitos": total_debitos, + "total_creditos": total_creditos, + "saldo_devedor": valor_a_recolher, + "saldo_credor": saldo_credor, + }, + ) diff --git a/src/calculators/ipi.py b/src/calculators/ipi.py new file mode 100644 index 0000000000000000000000000000000000000000..735d61e19ffecd09733cf409daeb0845fc94c6b0 --- /dev/null +++ b/src/calculators/ipi.py @@ -0,0 +1,144 @@ +"""Calculadora de IPI (Imposto sobre Produtos Industrializados).""" +from __future__ import annotations + +from dataclasses import dataclass +from decimal import Decimal +from typing import Optional + +from src.calculators import ResultadoCalculo, arred + + +@dataclass +class ParametrosIPI: + valor_produtos: Decimal + aliquota: Decimal + cst: str = "50" + frete: Decimal = Decimal("0") + seguro: Decimal = Decimal("0") + outras_despesas: Decimal = Decimal("0") + desconto: Decimal = Decimal("0") + # Para operações por unidade (pauta) + quantidade: Decimal = Decimal("0") + valor_pauta_unidade: Optional[Decimal] = None + + +def calcular_ipi(p: ParametrosIPI) -> ResultadoCalculo: + # CSTs isentos/não tributados + if p.cst in {"51", "52", "53", "54", "55", "02", "03", "04", "05"}: + return ResultadoCalculo( + base_calculo=Decimal("0"), + aliquota=p.aliquota, + valor_tributo=Decimal("0"), + valor_a_recolher=Decimal("0"), + ) + + if p.cst == "03" and p.valor_pauta_unidade and p.quantidade: + # Tributação por pauta + base = arred(p.quantidade * p.valor_pauta_unidade) + else: + base = arred( + p.valor_produtos + p.frete + p.seguro + p.outras_despesas - p.desconto + ) + + valor = arred(base * p.aliquota / 100) + + return ResultadoCalculo( + base_calculo=base, + aliquota=p.aliquota, + valor_tributo=valor, + valor_a_recolher=valor, + detalhes={"cst": p.cst}, + ) + + +def calcular_ipi_apuracao( + debitos: list[Decimal], + creditos: list[Decimal], + saldo_anterior: Decimal = Decimal("0"), +) -> ResultadoCalculo: + total_debitos = arred(sum(debitos)) + total_creditos = arred(sum(creditos) + saldo_anterior) + valor_a_recolher = max(Decimal("0"), arred(total_debitos - total_creditos)) + saldo_credor = max(Decimal("0"), arred(total_creditos - total_debitos)) + return ResultadoCalculo( + base_calculo=total_debitos, + aliquota=Decimal("0"), + valor_tributo=total_debitos, + creditos=total_creditos, + valor_a_recolher=valor_a_recolher, + detalhes={ + "total_debitos": total_debitos, + "total_creditos": total_creditos, + "saldo_credor": saldo_credor, + }, + ) + + +# Alíquotas IPI simplificadas por capítulo NCM (TIPI) +TIPI_ALIQUOTAS: dict[str, Decimal] = { + "01": Decimal("0"), # Animais vivos + "02": Decimal("0"), # Carnes e miudezas + "03": Decimal("0"), # Peixes e crustáceos + "04": Decimal("0"), # Leite, laticínios, ovos + "05": Decimal("0"), # Outros produtos de origem animal + "06": Decimal("0"), # Plantas vivas e produtos de floricultura + "07": Decimal("0"), # Produtos hortícolas + "08": Decimal("0"), # Frutas + "09": Decimal("0"), # Café, chá, mate + "10": Decimal("0"), # Cereais + "11": Decimal("0"), # Produtos da indústria de moagem + "12": Decimal("0"), # Sementes e frutos oleaginosos + "16": Decimal("0"), # Preparações de carne, peixe + "17": Decimal("5"), # Açúcares e produtos de confeitaria + "18": Decimal("5"), # Cacau e suas preparações + "19": Decimal("5"), # Preparações à base de cereais + "20": Decimal("5"), # Preparações de produtos hortícolas e frutas + "21": Decimal("5"), # Preparações alimentícias diversas + "22": Decimal("15"), # Bebidas, líquidos alcoólicos e vinagres + "24": Decimal("300"), # Tabaco e seus sucedâneos manufaturados + "25": Decimal("5"), # Sal; enxofre; terras e pedras + "27": Decimal("5"), # Combustíveis minerais, óleos minerais + "28": Decimal("5"), # Produtos químicos inorgânicos + "29": Decimal("5"), # Produtos químicos orgânicos + "30": Decimal("0"), # Produtos farmacêuticos + "31": Decimal("0"), # Adubos ou fertilizantes + "33": Decimal("7"), # Óleos essenciais e resinoides; cosméticos + "34": Decimal("5"), # Sabões, agentes orgânicos de superfície + "38": Decimal("5"), # Produtos diversos das indústrias químicas + "39": Decimal("15"), # Plásticos e suas obras + "40": Decimal("15"), # Borracha e suas obras + "41": Decimal("5"), # Peles, exceto peles com pelo + "42": Decimal("15"), # Obras de couro; artigos de seleiro + "44": Decimal("5"), # Madeira, carvão vegetal e obras de madeira + "48": Decimal("10"), # Papel e cartão + "49": Decimal("5"), # Livros, jornais, gravuras + "50": Decimal("10"), # Seda + "51": Decimal("10"), # Lã, pelos finos ou grosseiros + "52": Decimal("10"), # Algodão + "61": Decimal("20"), # Vestuário e seus acessórios (malha) + "62": Decimal("20"), # Vestuário e seus acessórios (exceto malha) + "64": Decimal("20"), # Calçados, polainas e artefatos semelhantes + "69": Decimal("15"), # Produtos cerâmicos + "70": Decimal("15"), # Vidro e suas obras + "72": Decimal("5"), # Ferro fundido, ferro e aço + "73": Decimal("10"), # Obras de ferro fundido, ferro ou aço + "74": Decimal("10"), # Cobre e suas obras + "76": Decimal("10"), # Alumínio e suas obras + "82": Decimal("10"), # Ferramentas, artefatos de cutelaria + "84": Decimal("10"), # Máquinas e aparelhos mecânicos + "85": Decimal("15"), # Máquinas e aparelhos elétricos + "86": Decimal("5"), # Veículos e material para vias férreas + "87": Decimal("25"), # Veículos automóveis, tratores + "88": Decimal("0"), # Aeronaves e aparelhos espaciais + "90": Decimal("15"), # Instrumentos e aparelhos de óptica + "91": Decimal("20"), # Instrumentos de relojoaria + "92": Decimal("25"), # Instrumentos musicais + "94": Decimal("15"), # Móveis; mobiliário médico-cirúrgico + "95": Decimal("25"), # Brinquedos, jogos, artigos para divertimento + "96": Decimal("10"), # Obras diversas +} + + +def obter_aliquota_ipi_ncm(ncm: str) -> Decimal: + capitulo = ncm[:2] + return TIPI_ALIQUOTAS.get(capitulo, Decimal("5")) diff --git a/src/calculators/irpj_csll.py b/src/calculators/irpj_csll.py new file mode 100644 index 0000000000000000000000000000000000000000..43b271ec141715dc309c2001f7cbbb47aa6c07db --- /dev/null +++ b/src/calculators/irpj_csll.py @@ -0,0 +1,209 @@ +"""Calculadora de IRPJ e CSLL (Lucro Real e Lucro Presumido).""" +from __future__ import annotations + +from dataclasses import dataclass, field +from decimal import Decimal +from typing import Optional + +from src.calculators import ResultadoCalculo, arred +from src.fiscal.constants import ( + ALIQ_CSLL_FINANCEIRAS, + ALIQ_CSLL_PADRAO, + ALIQ_IRPJ_ADICIONAL, + ALIQ_IRPJ_BASE, + LIMITE_ADICIONAL_IRPJ_TRIMESTRAL, + PERCENTUAL_PRESUMIDO_CSLL, + PERCENTUAL_PRESUMIDO_IRPJ, +) +from src.fiscal.entities import RegimeTributario + + +@dataclass +class ResultadoIRPJ: + lucro_real_ajustado: Decimal = Decimal("0") + base_calculo: Decimal = Decimal("0") + aliquota_base: Decimal = Decimal("15") + valor_base: Decimal = Decimal("0") + base_adicional: Decimal = Decimal("0") + valor_adicional: Decimal = Decimal("0") + valor_irpj_total: Decimal = Decimal("0") + # Incentivos e deduções + incentivos_fiscais: Decimal = Decimal("0") + irpj_a_recolher: Decimal = Decimal("0") + compensacoes: Decimal = Decimal("0") + detalhes: dict = field(default_factory=dict) + + +@dataclass +class ResultadoCSLL: + base_calculo: Decimal = Decimal("0") + aliquota: Decimal = Decimal("0") + valor_csll: Decimal = Decimal("0") + csll_a_recolher: Decimal = Decimal("0") + compensacoes: Decimal = Decimal("0") + detalhes: dict = field(default_factory=dict) + + +def calcular_irpj_lucro_real( + lucro_antes_ir: Decimal, + adicoes: Decimal = Decimal("0"), + exclusoes: Decimal = Decimal("0"), + compensacoes_prejuizo: Decimal = Decimal("0"), + incentivos_fiscais: Decimal = Decimal("0"), + irpj_estimativas: Decimal = Decimal("0"), + periodo: str = "trimestral", +) -> ResultadoIRPJ: + r = ResultadoIRPJ() + + lucro_ajustado = arred(lucro_antes_ir + adicoes - exclusoes) + r.lucro_real_ajustado = lucro_ajustado + + # Compensação de prejuízo (limite 30% do lucro ajustado) + limite_compensacao = arred(lucro_ajustado * Decimal("0.30")) + comp_efetiva = min(compensacoes_prejuizo, limite_compensacao) + r.base_calculo = max(Decimal("0"), arred(lucro_ajustado - comp_efetiva)) + + r.aliquota_base = ALIQ_IRPJ_BASE + r.valor_base = arred(r.base_calculo * ALIQ_IRPJ_BASE / 100) + + limite_adicional = ( + LIMITE_ADICIONAL_IRPJ_TRIMESTRAL + if periodo == "trimestral" + else arred(LIMITE_ADICIONAL_IRPJ_TRIMESTRAL * Decimal("4")) + ) + r.base_adicional = max(Decimal("0"), arred(r.base_calculo - limite_adicional)) + r.valor_adicional = arred(r.base_adicional * ALIQ_IRPJ_ADICIONAL / 100) + + r.valor_irpj_total = arred(r.valor_base + r.valor_adicional) + r.incentivos_fiscais = arred(incentivos_fiscais) + + r.irpj_a_recolher = max( + Decimal("0"), + arred(r.valor_irpj_total - r.incentivos_fiscais - irpj_estimativas), + ) + + r.detalhes = { + "adicoes": adicoes, + "exclusoes": exclusoes, + "compensacao_prejuizo_utilizada": comp_efetiva, + "compensacoes_disponiveis": compensacoes_prejuizo, + "limite_compensacao_30pct": limite_compensacao, + "estimativas_pagas": irpj_estimativas, + } + return r + + +def calcular_irpj_lucro_presumido( + receita_bruta: Decimal, + outras_receitas: Decimal = Decimal("0"), + atividade: str = "venda_mercadorias", + incentivos_fiscais: Decimal = Decimal("0"), + irpj_anteriores: Decimal = Decimal("0"), +) -> ResultadoIRPJ: + r = ResultadoIRPJ() + + percentual = PERCENTUAL_PRESUMIDO_IRPJ.get(atividade, Decimal("8.0")) + lucro_presumido = arred(receita_bruta * percentual / 100) + r.lucro_real_ajustado = arred(lucro_presumido + outras_receitas) + r.base_calculo = r.lucro_real_ajustado + + r.aliquota_base = ALIQ_IRPJ_BASE + r.valor_base = arred(r.base_calculo * ALIQ_IRPJ_BASE / 100) + + r.base_adicional = max(Decimal("0"), arred(r.base_calculo - LIMITE_ADICIONAL_IRPJ_TRIMESTRAL)) + r.valor_adicional = arred(r.base_adicional * ALIQ_IRPJ_ADICIONAL / 100) + + r.valor_irpj_total = arred(r.valor_base + r.valor_adicional) + r.incentivos_fiscais = incentivos_fiscais + r.irpj_a_recolher = max( + Decimal("0"), + arred(r.valor_irpj_total - incentivos_fiscais - irpj_anteriores), + ) + + r.detalhes = { + "receita_bruta": receita_bruta, + "percentual_presuncao": percentual, + "lucro_presumido": lucro_presumido, + "outras_receitas": outras_receitas, + "atividade": atividade, + } + return r + + +def calcular_csll_lucro_real( + lucro_antes_csll: Decimal, + adicoes: Decimal = Decimal("0"), + exclusoes: Decimal = Decimal("0"), + compensacoes_base_negativa: Decimal = Decimal("0"), + tipo_empresa: str = "padrao", + csll_estimativas: Decimal = Decimal("0"), +) -> ResultadoCSLL: + r = ResultadoCSLL() + + base_ajustada = arred(lucro_antes_csll + adicoes - exclusoes) + limite_comp = arred(base_ajustada * Decimal("0.30")) + comp_efetiva = min(compensacoes_base_negativa, limite_comp) + r.base_calculo = max(Decimal("0"), arred(base_ajustada - comp_efetiva)) + + aliq = ALIQ_CSLL_FINANCEIRAS if tipo_empresa == "financeira" else ALIQ_CSLL_PADRAO + r.aliquota = aliq + r.valor_csll = arred(r.base_calculo * aliq / 100) + r.csll_a_recolher = max(Decimal("0"), arred(r.valor_csll - csll_estimativas)) + + r.detalhes = { + "adicoes": adicoes, + "exclusoes": exclusoes, + "compensacao_utilizada": comp_efetiva, + "estimativas_pagas": csll_estimativas, + } + return r + + +def calcular_csll_lucro_presumido( + receita_bruta: Decimal, + outras_receitas: Decimal = Decimal("0"), + atividade: str = "comercio_industria", + tipo_empresa: str = "padrao", + csll_anteriores: Decimal = Decimal("0"), +) -> ResultadoCSLL: + r = ResultadoCSLL() + + percentual = PERCENTUAL_PRESUMIDO_CSLL.get(atividade, Decimal("12.0")) + base_presumida = arred(receita_bruta * percentual / 100 + outras_receitas) + r.base_calculo = base_presumida + + aliq = ALIQ_CSLL_FINANCEIRAS if tipo_empresa == "financeira" else ALIQ_CSLL_PADRAO + r.aliquota = aliq + r.valor_csll = arred(r.base_calculo * aliq / 100) + r.csll_a_recolher = max(Decimal("0"), arred(r.valor_csll - csll_anteriores)) + + r.detalhes = { + "receita_bruta": receita_bruta, + "percentual_presuncao": percentual, + "base_presumida_bruta": base_presumida, + "outras_receitas": outras_receitas, + "atividade": atividade, + } + return r + + +def calcular_irpj_estimativa_mensal( + receita_bruta_mes: Decimal, + atividade: str = "venda_mercadorias", + outras_receitas: Decimal = Decimal("0"), +) -> dict[str, Decimal]: + """Cálculo de IRPJ por estimativa mensal (Lucro Real).""" + percentual = PERCENTUAL_PRESUMIDO_IRPJ.get(atividade, Decimal("8.0")) + base = arred(receita_bruta_mes * percentual / 100 + outras_receitas) + + # Adicional: excedente ao limite mensal de R$20.000 + limite_mensal = Decimal("20000") + irpj_base = arred(base * ALIQ_IRPJ_BASE / 100) + adicional = arred(max(Decimal("0"), base - limite_mensal) * ALIQ_IRPJ_ADICIONAL / 100) + + return { + "base_estimada": base, + "irpj_aliquota_base": irpj_base, + "irpj_adicional": adicional, + "total_estimativa": arred(irpj_base + adicional), + } diff --git a/src/calculators/iss.py b/src/calculators/iss.py new file mode 100644 index 0000000000000000000000000000000000000000..a25cceab9079a9d5b5a678930db3358475092169 --- /dev/null +++ b/src/calculators/iss.py @@ -0,0 +1,157 @@ +"""Calculadora de ISS (Imposto sobre Serviços).""" +from __future__ import annotations + +from dataclasses import dataclass +from decimal import Decimal +from typing import Optional + +from src.calculators import ResultadoCalculo, arred +from src.fiscal.constants import ALIQ_ISS_MAXIMA, ALIQ_ISS_MINIMA, ALIQ_ISS_SERVICOS + + +@dataclass +class ParametrosISS: + valor_servico: Decimal + codigo_servico: str + aliquota_municipal: Optional[Decimal] = None + retencao_fonte: bool = False + municipio_prestacao: str = "" + municipio_tomador: str = "" + responsavel_retencao: str = "prestador" + + def aliquota_efetiva(self) -> Decimal: + if self.aliquota_municipal is not None: + aliq = self.aliquota_municipal + aliq = max(aliq, ALIQ_ISS_MINIMA) + aliq = min(aliq, ALIQ_ISS_MAXIMA) + return aliq + return ALIQ_ISS_SERVICOS.get( + self.codigo_servico[:2], ALIQ_ISS_MAXIMA + ) + + +def calcular_iss(p: ParametrosISS) -> ResultadoCalculo: + aliq = p.aliquota_efetiva() + valor = arred(p.valor_servico * aliq / 100) + + return ResultadoCalculo( + base_calculo=p.valor_servico, + aliquota=aliq, + valor_tributo=valor, + valor_a_recolher=valor, + detalhes={ + "codigo_servico": p.codigo_servico, + "retencao_fonte": p.retencao_fonte, + "municipio_prestacao": p.municipio_prestacao, + "responsavel": "tomador" if p.retencao_fonte else "prestador", + }, + ) + + +def calcular_iss_retido_tomador( + valor_servico: Decimal, + codigo_servico: str, + aliquota_municipal: Optional[Decimal] = None, +) -> Decimal: + p = ParametrosISS( + valor_servico=valor_servico, + codigo_servico=codigo_servico, + aliquota_municipal=aliquota_municipal, + retencao_fonte=True, + ) + resultado = calcular_iss(p) + return resultado.valor_tributo + + +# Serviços sujeitos a retenção obrigatória pelo tomador (LC 116/2003, Art. 6º) +SERVICOS_RETENCAO_OBRIGATORIA = { + "01", # Serviços de informática e congêneres + "02", # Pesquisa e desenvolvimento + "03", # Serviços prestados mediante locação de bens + "04", # Saúde, assistência médica + "07", # Engenharia, arquitetura, agronomia, agrimensura + "08", # Pesquisa, vigilância, rastreamento, monitoramento + "10", # Serviços de intermediação e congêneres + "11", # Guarda, estacionamento, armazenamento + "13", # Fonografia, fotografia, cinematografia, reprografia + "15", # Serviços bancários ou financeiros + "16", # Serviços de transporte de natureza municipal + "17", # Serviços de apoio técnico, administrativo, jurídico + "20", # Serviços portuários, aeroportuários + "21", # Serviços de registros públicos, cartorários +} + + +def requer_retencao_iss(codigo_servico: str) -> bool: + return codigo_servico[:2] in SERVICOS_RETENCAO_OBRIGATORIA + + +def calcular_iss_simples_nacional( + receita_bruta_acumulada_12m: Decimal, + receita_servicos_mes: Decimal, + anexo: str = "III", +) -> dict[str, Decimal]: + """Cálculo do ISS dentro do Simples Nacional por anexo.""" + # Tabelas simplificadas Simples Nacional (Lei Complementar 123/2006) + tabelas = { + "III": [ # Serviços em geral (Anexo III) + (Decimal("180000"), Decimal("6.0"), Decimal("0")), + (Decimal("360000"), Decimal("11.2"), Decimal("9360")), + (Decimal("720000"), Decimal("13.5"), Decimal("17640")), + (Decimal("1800000"), Decimal("16.0"), Decimal("35640")), + (Decimal("3600000"), Decimal("21.0"), Decimal("125640")), + (Decimal("4800000"), Decimal("33.0"), Decimal("648000")), + ], + "IV": [ # Serviços especializados (Anexo IV) - sem CPP + (Decimal("180000"), Decimal("4.5"), Decimal("0")), + (Decimal("360000"), Decimal("9.0"), Decimal("8100")), + (Decimal("720000"), Decimal("10.2"), Decimal("12420")), + (Decimal("1800000"), Decimal("14.0"), Decimal("39780")), + (Decimal("3600000"), Decimal("22.0"), Decimal("183780")), + (Decimal("4800000"), Decimal("33.0"), Decimal("828000")), + ], + "V": [ # Serviços com fator R (Anexo V) + (Decimal("180000"), Decimal("15.5"), Decimal("0")), + (Decimal("360000"), Decimal("18.0"), Decimal("4500")), + (Decimal("720000"), Decimal("19.5"), Decimal("9900")), + (Decimal("1800000"), Decimal("20.5"), Decimal("17100")), + (Decimal("3600000"), Decimal("23.0"), Decimal("62100")), + (Decimal("4800000"), Decimal("30.5"), Decimal("540000")), + ], + } + + tabela = tabelas.get(anexo, tabelas["III"]) + aliq_nominal = Decimal("6.0") + deducao = Decimal("0") + + for limite, aliq, ded in tabela: + if receita_bruta_acumulada_12m <= limite: + aliq_nominal = aliq + deducao = ded + break + + aliq_efetiva = ( + arred((receita_bruta_acumulada_12m * aliq_nominal / 100 - deducao) / receita_bruta_acumulada_12m * 100) + if receita_bruta_acumulada_12m > 0 + else Decimal("0") + ) + + # ISS representa ~7-10% do DAS no Anexo III/IV + percentual_iss_das = { + "III": Decimal("33.50"), + "IV": Decimal("0"), # ISS pago separado no Anexo IV + "V": Decimal("33.50"), + } + + valor_das = arred(receita_servicos_mes * aliq_efetiva / 100) + valor_iss = arred(valor_das * percentual_iss_das.get(anexo, Decimal("33.5")) / 100) + + return { + "receita_bruta_12m": receita_bruta_acumulada_12m, + "aliq_nominal": aliq_nominal, + "deducao": deducao, + "aliq_efetiva": aliq_efetiva, + "valor_das_total": valor_das, + "valor_iss_estimado": valor_iss, + "anexo": anexo, + } diff --git a/src/calculators/pis_cofins.py b/src/calculators/pis_cofins.py new file mode 100644 index 0000000000000000000000000000000000000000..8b26077d064c55ffa1e1d6a8ee95d6476581226f --- /dev/null +++ b/src/calculators/pis_cofins.py @@ -0,0 +1,180 @@ +"""Calculadora de PIS e COFINS (cumulativo e não-cumulativo).""" +from __future__ import annotations + +from dataclasses import dataclass, field +from decimal import Decimal +from typing import Optional + +from src.calculators import ResultadoCalculo, arred +from src.fiscal.constants import ( + ALIQ_COFINS_LUCRO_PRESUMIDO, + ALIQ_COFINS_LUCRO_REAL, + ALIQ_PIS_LUCRO_PRESUMIDO, + ALIQ_PIS_LUCRO_REAL, +) +from src.fiscal.entities import RegimeTributario + + +@dataclass +class ReceitaBruta: + descricao: str + valor: Decimal + cst_pis: str = "01" + cst_cofins: str = "01" + aliq_pis_especifica: Optional[Decimal] = None + aliq_cofins_especifica: Optional[Decimal] = None + + +@dataclass +class CreditoPisCofins: + descricao: str + base_calculo: Decimal + aliquota_pis: Decimal + aliquota_cofins: Decimal + cst_pis: str = "50" + cst_cofins: str = "50" + + @property + def valor_pis(self) -> Decimal: + return arred(self.base_calculo * self.aliquota_pis / 100) + + @property + def valor_cofins(self) -> Decimal: + return arred(self.base_calculo * self.aliquota_cofins / 100) + + +@dataclass +class ResultadoPisCofins: + # PIS + base_pis: Decimal = Decimal("0") + aliq_pis: Decimal = Decimal("0") + valor_pis_debito: Decimal = Decimal("0") + creditos_pis: Decimal = Decimal("0") + pis_a_recolher: Decimal = Decimal("0") + # COFINS + base_cofins: Decimal = Decimal("0") + aliq_cofins: Decimal = Decimal("0") + valor_cofins_debito: Decimal = Decimal("0") + creditos_cofins: Decimal = Decimal("0") + cofins_a_recolher: Decimal = Decimal("0") + # Regime + regime: str = "" + detalhes: dict = field(default_factory=dict) + + +def calcular_pis_cofins( + receitas: list[ReceitaBruta], + regime: RegimeTributario, + creditos: list[CreditoPisCofins] | None = None, + saldo_anterior_pis: Decimal = Decimal("0"), + saldo_anterior_cofins: Decimal = Decimal("0"), +) -> ResultadoPisCofins: + creditos = creditos or [] + r = ResultadoPisCofins() + + is_nao_cumulativo = regime == RegimeTributario.LUCRO_REAL + aliq_pis = ALIQ_PIS_LUCRO_REAL if is_nao_cumulativo else ALIQ_PIS_LUCRO_PRESUMIDO + aliq_cofins = ALIQ_COFINS_LUCRO_REAL if is_nao_cumulativo else ALIQ_COFINS_LUCRO_PRESUMIDO + + r.regime = "Não-Cumulativo" if is_nao_cumulativo else "Cumulativo" + r.aliq_pis = aliq_pis + r.aliq_cofins = aliq_cofins + + # Calcular débitos por receita + pis_debito_total = Decimal("0") + cofins_debito_total = Decimal("0") + base_tributavel_pis = Decimal("0") + base_tributavel_cofins = Decimal("0") + + detalhes_receitas = [] + for rec in receitas: + # CSTs que geram débito: 01, 02, 03, 04, 05 + if rec.cst_pis in {"01", "02", "03", "04", "05"}: + ap = rec.aliq_pis_especifica if rec.aliq_pis_especifica is not None else aliq_pis + vp = arred(rec.valor * ap / 100) + pis_debito_total += vp + base_tributavel_pis += rec.valor + else: + vp = Decimal("0") + + if rec.cst_cofins in {"01", "02", "03", "04", "05"}: + ac = rec.aliq_cofins_especifica if rec.aliq_cofins_especifica is not None else aliq_cofins + vc = arred(rec.valor * ac / 100) + cofins_debito_total += vc + base_tributavel_cofins += rec.valor + else: + vc = Decimal("0") + + detalhes_receitas.append({ + "descricao": rec.descricao, + "valor": rec.valor, + "cst_pis": rec.cst_pis, + "cst_cofins": rec.cst_cofins, + "valor_pis": vp, + "valor_cofins": vc, + }) + + r.base_pis = arred(base_tributavel_pis) + r.base_cofins = arred(base_tributavel_cofins) + r.valor_pis_debito = arred(pis_debito_total) + r.valor_cofins_debito = arred(cofins_debito_total) + + # Créditos (apenas regime não-cumulativo) + creditos_pis = Decimal("0") + creditos_cofins = Decimal("0") + if is_nao_cumulativo: + for cred in creditos: + creditos_pis += cred.valor_pis + creditos_cofins += cred.valor_cofins + + creditos_pis = arred(creditos_pis + saldo_anterior_pis) + creditos_cofins = arred(creditos_cofins + saldo_anterior_cofins) + + r.creditos_pis = creditos_pis + r.creditos_cofins = creditos_cofins + r.pis_a_recolher = max(Decimal("0"), arred(r.valor_pis_debito - creditos_pis)) + r.cofins_a_recolher = max(Decimal("0"), arred(r.valor_cofins_debito - creditos_cofins)) + + r.detalhes = { + "receitas": detalhes_receitas, + "saldo_credor_pis": max(Decimal("0"), arred(creditos_pis - r.valor_pis_debito)), + "saldo_credor_cofins": max(Decimal("0"), arred(creditos_cofins - r.valor_cofins_debito)), + } + + return r + + +def calcular_pis_cofins_retencao( + valor_servico: Decimal, + aliq_pis: Decimal = Decimal("0.65"), + aliq_cofins: Decimal = Decimal("3.00"), + aliq_csll: Decimal = Decimal("1.00"), + aliq_ir: Decimal = Decimal("1.50"), +) -> dict[str, Decimal]: + """Retenção de PIS/COFINS/CSLL/IR na fonte para serviços (IN SRF 459/2004).""" + MIN_RETENCAO = Decimal("10.00") + + valor_pis = arred(valor_servico * aliq_pis / 100) + valor_cofins = arred(valor_servico * aliq_cofins / 100) + valor_csll = arred(valor_servico * aliq_csll / 100) + valor_ir = arred(valor_servico * aliq_ir / 100) + total = arred(valor_pis + valor_cofins + valor_csll + valor_ir) + + if total < MIN_RETENCAO: + return { + "valor_pis": Decimal("0"), + "valor_cofins": Decimal("0"), + "valor_csll": Decimal("0"), + "valor_ir": Decimal("0"), + "total_retido": Decimal("0"), + "observacao": f"Retenção dispensada: total R$ {total} < R$ {MIN_RETENCAO}", + } + + return { + "valor_pis": valor_pis, + "valor_cofins": valor_cofins, + "valor_csll": valor_csll, + "valor_ir": valor_ir, + "total_retido": total, + "observacao": "", + } diff --git a/src/calculators/simples_nacional.py b/src/calculators/simples_nacional.py new file mode 100644 index 0000000000000000000000000000000000000000..dc9ca33d148e3a727aabcc0f49595c7f5ed538fd --- /dev/null +++ b/src/calculators/simples_nacional.py @@ -0,0 +1,292 @@ + +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import date +from decimal import Decimal +from typing import Optional + +from src.calculators import ResultadoCalculo, arred + + +# --------------------------------------------------------------------------- +# Tabelas do Simples Nacional (LC 123/2006 com alterações da LC 155/2016) +# Vigência: a partir de 01/01/2018 +# Faixas: (limite_acumulado_12m, aliq_nominal, deducao) +# --------------------------------------------------------------------------- + +TABELA_SIMPLES: dict[str, list[tuple[Decimal, Decimal, Decimal]]] = { + "I": [ # Comércio + (Decimal("180000.00"), Decimal("4.00"), Decimal("0.00")), + (Decimal("360000.00"), Decimal("7.30"), Decimal("5940.00")), + (Decimal("720000.00"), Decimal("9.50"), Decimal("13860.00")), + (Decimal("1800000.00"), Decimal("10.70"), Decimal("22500.00")), + (Decimal("3600000.00"), Decimal("14.30"), Decimal("87300.00")), + (Decimal("4800000.00"), Decimal("19.00"), Decimal("378000.00")), + ], + "II": [ # Indústria + (Decimal("180000.00"), Decimal("4.50"), Decimal("0.00")), + (Decimal("360000.00"), Decimal("7.80"), Decimal("5940.00")), + (Decimal("720000.00"), Decimal("10.00"), Decimal("13860.00")), + (Decimal("1800000.00"), Decimal("11.20"), Decimal("22500.00")), + (Decimal("3600000.00"), Decimal("14.70"), Decimal("85500.00")), + (Decimal("4800000.00"), Decimal("30.00"), Decimal("720000.00")), + ], + "III": [ # Serviços (com CPP) + (Decimal("180000.00"), Decimal("6.00"), Decimal("0.00")), + (Decimal("360000.00"), Decimal("11.20"), Decimal("9360.00")), + (Decimal("720000.00"), Decimal("13.50"), Decimal("17640.00")), + (Decimal("1800000.00"), Decimal("16.00"), Decimal("35640.00")), + (Decimal("3600000.00"), Decimal("21.00"), Decimal("125640.00")), + (Decimal("4800000.00"), Decimal("33.00"), Decimal("648000.00")), + ], + "IV": [ # Serviços (sem CPP — INSS pago separado) + (Decimal("180000.00"), Decimal("4.50"), Decimal("0.00")), + (Decimal("360000.00"), Decimal("9.00"), Decimal("8100.00")), + (Decimal("720000.00"), Decimal("10.20"), Decimal("12420.00")), + (Decimal("1800000.00"), Decimal("14.00"), Decimal("39780.00")), + (Decimal("3600000.00"), Decimal("22.00"), Decimal("183780.00")), + (Decimal("4800000.00"), Decimal("33.00"), Decimal("828000.00")), + ], + "V": [ # Serviços com fator R (profissões regulamentadas) + (Decimal("180000.00"), Decimal("15.50"), Decimal("0.00")), + (Decimal("360000.00"), Decimal("18.00"), Decimal("4500.00")), + (Decimal("720000.00"), Decimal("19.50"), Decimal("9900.00")), + (Decimal("1800000.00"), Decimal("20.50"), Decimal("17100.00")), + (Decimal("3600000.00"), Decimal("23.00"), Decimal("62100.00")), + (Decimal("4800000.00"), Decimal("30.50"), Decimal("540000.00")), + ], +} + + +PARTILHA_DAS: dict[str, list[dict[str, Decimal]]] = { + "I": [ + {"IRPJ": Decimal("5.5"), "CSLL": Decimal("3.5"), "COFINS": Decimal("12.74"), "PIS": Decimal("2.76"), "CPP": Decimal("41.50"), "ICMS": Decimal("34.00")}, + {"IRPJ": Decimal("5.5"), "CSLL": Decimal("3.5"), "COFINS": Decimal("12.74"), "PIS": Decimal("2.76"), "CPP": Decimal("41.50"), "ICMS": Decimal("34.00")}, + {"IRPJ": Decimal("5.5"), "CSLL": Decimal("3.5"), "COFINS": Decimal("12.74"), "PIS": Decimal("2.76"), "CPP": Decimal("42.00"), "ICMS": Decimal("33.50")}, + {"IRPJ": Decimal("5.5"), "CSLL": Decimal("3.5"), "COFINS": Decimal("12.74"), "PIS": Decimal("2.76"), "CPP": Decimal("42.00"), "ICMS": Decimal("33.50")}, + {"IRPJ": Decimal("5.5"), "CSLL": Decimal("3.5"), "COFINS": Decimal("12.74"), "PIS": Decimal("2.76"), "CPP": Decimal("42.00"), "ICMS": Decimal("33.50")}, + {"IRPJ": Decimal("13.5"), "CSLL": Decimal("10.0"), "COFINS": Decimal("28.27"), "PIS": Decimal("6.13"), "CPP": Decimal("42.10"), "ICMS": Decimal("0.00")}, + ], + "II": [ + {"IRPJ": Decimal("5.5"), "CSLL": Decimal("3.5"), "COFINS": Decimal("11.51"), "PIS": Decimal("2.49"), "CPP": Decimal("37.50"), "IPI": Decimal("7.50"), "ICMS": Decimal("32.00")}, + {"IRPJ": Decimal("5.5"), "CSLL": Decimal("3.5"), "COFINS": Decimal("11.51"), "PIS": Decimal("2.49"), "CPP": Decimal("37.50"), "IPI": Decimal("7.50"), "ICMS": Decimal("32.00")}, + {"IRPJ": Decimal("5.5"), "CSLL": Decimal("3.5"), "COFINS": Decimal("11.51"), "PIS": Decimal("2.49"), "CPP": Decimal("37.50"), "IPI": Decimal("7.50"), "ICMS": Decimal("32.00")}, + {"IRPJ": Decimal("5.5"), "CSLL": Decimal("3.5"), "COFINS": Decimal("11.51"), "PIS": Decimal("2.49"), "CPP": Decimal("37.50"), "IPI": Decimal("7.50"), "ICMS": Decimal("32.00")}, + {"IRPJ": Decimal("5.5"), "CSLL": Decimal("3.5"), "COFINS": Decimal("11.51"), "PIS": Decimal("2.49"), "CPP": Decimal("37.50"), "IPI": Decimal("7.50"), "ICMS": Decimal("32.00")}, + {"IRPJ": Decimal("8.0"), "CSLL": Decimal("7.5"), "COFINS": Decimal("20.96"), "PIS": Decimal("4.54"), "CPP": Decimal("23.50"), "IPI": Decimal("35.50"), "ICMS": Decimal("0.00")}, + ], + "III": [ + {"IRPJ": Decimal("4.0"), "CSLL": Decimal("3.5"), "COFINS": Decimal("12.82"), "PIS": Decimal("2.78"), "CPP": Decimal("43.40"), "ISS": Decimal("33.50")}, + {"IRPJ": Decimal("4.0"), "CSLL": Decimal("3.5"), "COFINS": Decimal("14.05"), "PIS": Decimal("3.05"), "CPP": Decimal("43.40"), "ISS": Decimal("32.00")}, + {"IRPJ": Decimal("4.0"), "CSLL": Decimal("3.5"), "COFINS": Decimal("13.64"), "PIS": Decimal("2.96"), "CPP": Decimal("43.40"), "ISS": Decimal("32.50")}, + {"IRPJ": Decimal("4.0"), "CSLL": Decimal("3.5"), "COFINS": Decimal("13.64"), "PIS": Decimal("2.96"), "CPP": Decimal("43.40"), "ISS": Decimal("32.50")}, + {"IRPJ": Decimal("4.0"), "CSLL": Decimal("3.5"), "COFINS": Decimal("12.82"), "PIS": Decimal("2.78"), "CPP": Decimal("43.40"), "ISS": Decimal("33.50")}, + {"IRPJ": Decimal("35.0"), "CSLL": Decimal("15.0"), "COFINS": Decimal("16.03"), "PIS": Decimal("3.47"), "CPP": Decimal("30.50"), "ISS": Decimal("0.00")}, + ], + "IV": [ + {"IRPJ": Decimal("18.8"), "CSLL": Decimal("15.2"), "COFINS": Decimal("17.67"), "PIS": Decimal("3.83"), "ISS": Decimal("44.50")}, + {"IRPJ": Decimal("19.8"), "CSLL": Decimal("15.2"), "COFINS": Decimal("20.55"), "PIS": Decimal("4.45"), "ISS": Decimal("40.00")}, + {"IRPJ": Decimal("20.8"), "CSLL": Decimal("15.2"), "COFINS": Decimal("19.73"), "PIS": Decimal("4.27"), "ISS": Decimal("40.00")}, + {"IRPJ": Decimal("17.8"), "CSLL": Decimal("19.2"), "COFINS": Decimal("18.90"), "PIS": Decimal("4.10"), "ISS": Decimal("40.00")}, + {"IRPJ": Decimal("18.8"), "CSLL": Decimal("19.2"), "COFINS": Decimal("18.08"), "PIS": Decimal("3.92"), "ISS": Decimal("40.00")}, + {"IRPJ": Decimal("53.5"), "CSLL": Decimal("21.5"), "COFINS": Decimal("20.55"), "PIS": Decimal("4.45"), "ISS": Decimal("0.00")}, + ], +} + +LIMITE_SIMPLES = Decimal("4800000.00") +LIMITE_MEI = Decimal("81000.00") + + +@dataclass +class ResultadoSimples: + receita_bruta_mes: Decimal + receita_bruta_acumulada_12m: Decimal + anexo: str + faixa: int + aliq_nominal: Decimal + deducao: Decimal + aliq_efetiva: Decimal + valor_das: Decimal + partilha: dict[str, Decimal] = field(default_factory=dict) + periodo: str = "" + dentro_limite: bool = True + + +def _calcular_aliq_efetiva( + rec_12m: Decimal, + aliq_nominal: Decimal, + deducao: Decimal, +) -> Decimal: + if rec_12m <= 0: + return Decimal("0") + return arred(((rec_12m * aliq_nominal / 100) - deducao) / rec_12m * 100, 6) + + +def _obter_faixa(rec_12m: Decimal, tabela: list) -> tuple[int, Decimal, Decimal]: + """Retorna (índice_faixa, aliq_nominal, deducao).""" + for i, (limite, aliq, ded) in enumerate(tabela): + if rec_12m <= limite: + return i, aliq, ded + + return len(tabela) - 1, tabela[-1][1], tabela[-1][2] + + +def calcular_das( + receita_bruta_mes: Decimal, + receita_bruta_acumulada_12m: Decimal, + anexo: str = "I", + periodo: str = "", +) -> ResultadoSimples: + """ + Calcula o DAS (Documento de Arrecadação do Simples Nacional) mensal. + + Args: + receita_bruta_mes: Receita bruta do mês de referência + receita_bruta_acumulada_12m: Soma dos últimos 12 meses (ou desde o início) + anexo: "I", "II", "III", "IV" ou "V" + periodo: "YYYY-MM" + """ + dentro_limite = receita_bruta_acumulada_12m <= LIMITE_SIMPLES + tabela = TABELA_SIMPLES.get(anexo, TABELA_SIMPLES["I"]) + + faixa_idx, aliq_nominal, deducao = _obter_faixa(receita_bruta_acumulada_12m, tabela) + aliq_efetiva = _calcular_aliq_efetiva(receita_bruta_acumulada_12m, aliq_nominal, deducao) + valor_das = arred(receita_bruta_mes * aliq_efetiva / 100) + + + partilha_tabela = PARTILHA_DAS.get(anexo, []) + partilha: dict[str, Decimal] = {} + if partilha_tabela and faixa_idx < len(partilha_tabela): + perc_faixa = partilha_tabela[faixa_idx] + for tributo, perc in perc_faixa.items(): + partilha[tributo] = arred(valor_das * perc / 100) + + return ResultadoSimples( + receita_bruta_mes=receita_bruta_mes, + receita_bruta_acumulada_12m=receita_bruta_acumulada_12m, + anexo=anexo, + faixa=faixa_idx + 1, + aliq_nominal=aliq_nominal, + deducao=deducao, + aliq_efetiva=aliq_efetiva, + valor_das=valor_das, + partilha=partilha, + periodo=periodo, + dentro_limite=dentro_limite, + ) + + +def calcular_fator_r( + folha_12m: Decimal, + rec_12m: Decimal, +) -> Decimal: + """ + Fator R: relação folha de pagamento / receita bruta nos últimos 12 meses. + Se Fator R >= 28%, o contribuinte pode optar pelo Anexo III ao invés do V. + """ + if rec_12m <= 0: + return Decimal("0") + return arred(folha_12m / rec_12m * 100, 4) + + +def determinar_anexo_servicos( + fator_r: Decimal, + lista_servico: str, +) -> str: + """ + Determina o anexo aplicável para empresas de serviços. + Serviços do Anexo V com Fator R >= 28% podem usar o Anexo III. + """ + + anexo_iv = { + "construcao_civil", "servicos_vigilancia", "limpeza", + "transporte_cargas", "cuidados_pessoais", + } + + anexo_iii = { + "agencia_viagens", "academias", "agronegocio", + "arquitetura_engenharia_simples", + } + + fator_r_atividades = { + "medicina", "odontologia", "psicologia", "fisioterapia", + "advocacia", "contabilidade", "consultoria", "ti_software", + "engenharia", "arquitetura", + } + + if lista_servico in anexo_iv: + return "IV" + if lista_servico in anexo_iii: + return "III" + if lista_servico in fator_r_atividades: + return "III" if fator_r >= Decimal("28") else "V" + return "III" + + +@dataclass +class ResultadoPGDAS: + """Resultado do PGDAS-D mensal.""" + cnpj: str + periodo: str + receita_bruta_total: Decimal + das_total: Decimal + das_por_estabelecimento: list[ResultadoSimples] = field(default_factory=list) + data_vencimento: Optional[date] = None + + @property + def resumo_partilha(self) -> dict[str, Decimal]: + totais: dict[str, Decimal] = {} + for r in self.das_por_estabelecimento: + for tributo, valor in r.partilha.items(): + totais[tributo] = totais.get(tributo, Decimal("0")) + valor + return totais + + +def calcular_pgdas( + cnpj: str, + periodo: str, + receitas_por_atividade: list[dict], + receita_acumulada_12m: Decimal, +) -> ResultadoPGDAS: + """ + Calcula o PGDAS-D completo para um período. + + Args: + cnpj: CNPJ da empresa + periodo: "YYYY-MM" + receitas_por_atividade: [{"valor": ..., "anexo": "I/II/III/IV/V"}] + receita_acumulada_12m: Receita dos últimos 12 meses + """ + das_total = Decimal("0") + resultados = [] + + for ativ in receitas_por_atividade: + valor = Decimal(str(ativ.get("valor", 0))) + anexo = ativ.get("anexo", "I") + resultado = calcular_das(valor, receita_acumulada_12m, anexo, periodo) + das_total += resultado.valor_das + resultados.append(resultado) + + + ano, mes = map(int, periodo.split("-")) + mes_vcto = mes + 1 if mes < 12 else 1 + ano_vcto = ano if mes < 12 else ano + 1 + vcto = date(ano_vcto, mes_vcto, 20) + + return ResultadoPGDAS( + cnpj=cnpj, + periodo=periodo, + receita_bruta_total=sum(Decimal(str(a["valor"])) for a in receitas_por_atividade), + das_total=arred(das_total), + das_por_estabelecimento=resultados, + data_vencimento=vcto, + ) + + + +MEI_VALORES_FIXOS = { + "comercio_industria": Decimal("70.60"), # R$ 70,60/mês (2024) + "servicos": Decimal("79.60"), + "comercio_servicos": Decimal("80.60"), +} diff --git a/src/fiscal/__init__.py b/src/fiscal/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/fiscal/__pycache__/__init__.cpython-312.pyc b/src/fiscal/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4cc9844d430c71f284d3cf8dacbeca3c960a284d Binary files /dev/null and b/src/fiscal/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/fiscal/__pycache__/constants.cpython-312.pyc b/src/fiscal/__pycache__/constants.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b93b4715665638af07254713e0e3381e7b79be3f Binary files /dev/null and b/src/fiscal/__pycache__/constants.cpython-312.pyc differ diff --git a/src/fiscal/__pycache__/entities.cpython-312.pyc b/src/fiscal/__pycache__/entities.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b1913485fd31ee558e5c16ceef57a61384c80e34 Binary files /dev/null and b/src/fiscal/__pycache__/entities.cpython-312.pyc differ diff --git a/src/fiscal/constants.py b/src/fiscal/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..32b19fa4cfe96ecd61bcbd99d95c98338d035a7b --- /dev/null +++ b/src/fiscal/constants.py @@ -0,0 +1,338 @@ + +from decimal import Decimal + + +ALIQ_ICMS_INTERNA: dict[str, Decimal] = { + "AC": Decimal("17"), "AL": Decimal("17"), "AP": Decimal("18"), + "AM": Decimal("20"), "BA": Decimal("19"), "CE": Decimal("20"), + "DF": Decimal("20"), "ES": Decimal("17"), "GO": Decimal("17"), + "MA": Decimal("22"), "MT": Decimal("17"), "MS": Decimal("17"), + "MG": Decimal("18"), "PA": Decimal("19"), "PB": Decimal("18"), + "PR": Decimal("19"), "PE": Decimal("20.5"), "PI": Decimal("21"), + "RJ": Decimal("20"), "RN": Decimal("18"), "RS": Decimal("17"), + "RO": Decimal("19.5"), "RR": Decimal("20"), "SC": Decimal("17"), + "SP": Decimal("18"), "SE": Decimal("19"), "TO": Decimal("20"), +} + + +ALIQ_ICMS_INTERESTADUAL: dict[str, Decimal] = { + "SUL_SUDESTE_para_OUTROS": Decimal("7"), + "OUTROS_para_SUL_SUDESTE": Decimal("12"), + "SUL_SUDESTE_para_SUL_SUDESTE": Decimal("12"), + "EXTERIOR": Decimal("4"), +} + +UF_SUL_SUDESTE = {"SP", "RJ", "MG", "ES", "PR", "SC", "RS"} + + +def aliq_icms_interestadual(uf_origem: str, uf_destino: str) -> Decimal: + if uf_origem in UF_SUL_SUDESTE and uf_destino not in UF_SUL_SUDESTE: + return ALIQ_ICMS_INTERESTADUAL["SUL_SUDESTE_para_OUTROS"] + return ALIQ_ICMS_INTERESTADUAL["OUTROS_para_SUL_SUDESTE"] + + +ALIQ_PIS_LUCRO_REAL = Decimal("1.65") +ALIQ_COFINS_LUCRO_REAL = Decimal("7.60") +ALIQ_PIS_LUCRO_PRESUMIDO = Decimal("0.65") +ALIQ_COFINS_LUCRO_PRESUMIDO = Decimal("3.00") + +ALIQ_IRPJ_BASE = Decimal("15") +ALIQ_IRPJ_ADICIONAL = Decimal("10") +LIMITE_ADICIONAL_IRPJ_MENSAL = Decimal("20000") +LIMITE_ADICIONAL_IRPJ_TRIMESTRAL = Decimal("60000") +LIMITE_ADICIONAL_IRPJ_ANUAL = Decimal("240000") + + +PERCENTUAL_PRESUMIDO_IRPJ: dict[str, Decimal] = { + "revenda_combustiveis": Decimal("1.6"), + "venda_mercadorias": Decimal("8.0"), + "transporte_cargas": Decimal("8.0"), + "atividades_rurais": Decimal("8.0"), + "outros_negocios": Decimal("8.0"), + "transporte_passageiros": Decimal("16.0"), + "servicos_em_geral": Decimal("32.0"), + "intermediacao_negocios": Decimal("32.0"), + "administracao_locacao": Decimal("32.0"), + "profissoes_regulamentadas": Decimal("32.0"), +} + +PERCENTUAL_PRESUMIDO_CSLL: dict[str, Decimal] = { + "comercio_industria": Decimal("12.0"), + "servicos_em_geral": Decimal("32.0"), +} + +ALIQ_CSLL_PADRAO = Decimal("9") +ALIQ_CSLL_FINANCEIRAS = Decimal("15") + + +ALIQ_ISS_MINIMA = Decimal("2.0") +ALIQ_ISS_MAXIMA = Decimal("5.0") + + +ALIQ_ISS_SERVICOS: dict[str, Decimal] = { + "01": Decimal("5.0"), # Serviços de informática + "02": Decimal("5.0"), # Pesquisa e desenvolvimento + "03": Decimal("5.0"), # Serviços prestados mediante locação + "04": Decimal("5.0"), # Saúde, assistência médica + "05": Decimal("5.0"), # Medicina e assistência veterinária + "06": Decimal("5.0"), # Cuidados pessoais, estética, atividades físicas + "07": Decimal("5.0"), # Serviços relativos a engenharia + "08": Decimal("5.0"), # Pesquisa, vigilância, rastreamento + "09": Decimal("5.0"), # Serviços relativos a hospedagem, turismo, viagens + "10": Decimal("5.0"), # Serviços de intermediação + "11": Decimal("5.0"), # Serviços de guarda, estacionamento + "12": Decimal("5.0"), # Serviços de diversões, lazer, entretenimento + "13": Decimal("5.0"), # Serviços relativos à fonografia, fotografia, cinematografia + "14": Decimal("5.0"), # Serviços relativos a bens de terceiros + "15": Decimal("5.0"), # Serviços relacionados ao setor bancário ou financeiro + "16": Decimal("5.0"), # Serviços de transporte de natureza municipal + "17": Decimal("2.0"), # Serviços de apoio técnico, administrativo, jurídico + "18": Decimal("5.0"), # Serviços de regulação de sinistros + "19": Decimal("5.0"), # Serviços de distribuição e venda de bilhetes e demais produtos + "20": Decimal("5.0"), # Serviços portuários, aeroportuários + "21": Decimal("5.0"), # Serviços de registros públicos + "22": Decimal("5.0"), # Serviços de exploração de rodovias + "23": Decimal("5.0"), # Serviços de programação e comunicação visual + "24": Decimal("5.0"), # Serviços de chaveiros, confecção de carimbos + "25": Decimal("2.0"), # Serviços funerários + "26": Decimal("5.0"), # Serviços de coleta, remessa ou entrega de correspondências + "27": Decimal("5.0"), # Serviços de assistência social + "28": Decimal("5.0"), # Serviços de avaliação de bens e serviços + "29": Decimal("5.0"), # Serviços de biblioteconomia + "30": Decimal("5.0"), # Serviços de biologia, biotecnologia e química + "31": Decimal("5.0"), # Serviços técnicos em edificações + "32": Decimal("5.0"), # Serviços de desenho técnico + "33": Decimal("5.0"), # Serviços de desembaraço aduaneiro + "34": Decimal("5.0"), # Serviços de investigações particulares + "35": Decimal("5.0"), # Serviços de reportagem, assessoria de imprensa + "36": Decimal("5.0"), # Serviços de meteorologia + "37": Decimal("5.0"), # Serviços de artistas, atletas, modelos e manequins + "38": Decimal("5.0"), # Serviços de museologia + "39": Decimal("5.0"), # Serviços de ourivesaria e lapidação + "40": Decimal("5.0"), # Obras de arte sob encomenda +} + + +CST_ICMS: dict[str, str] = { + "000": "Tributada integralmente", + "010": "Tributada e com cobrança do ICMS por substituição tributária", + "020": "Com redução de base de cálculo", + "030": "Isenta ou não tributada e com cobrança do ICMS por substituição tributária", + "040": "Isenta", + "041": "Não tributada", + "050": "Suspensão", + "051": "Diferimento", + "060": "ICMS cobrado anteriormente por substituição tributária", + "070": "Com redução de base de cálculo e cobrança do ICMS por substituição tributária", + "090": "Outras", + # Simples Nacional + "101": "Tributada pelo Simples Nacional com permissão de crédito", + "102": "Tributada pelo Simples Nacional sem permissão de crédito", + "103": "Isenção do ICMS no Simples Nacional para faixa de receita bruta", + "201": "Tributada pelo Simples Nacional com permissão de crédito e com cobrança do ICMS por ST", + "202": "Tributada pelo Simples Nacional sem permissão de crédito e com cobrança do ICMS por ST", + "203": "Isenção do ICMS no Simples Nacional para faixa de receita bruta e com cobrança do ICMS por ST", + "300": "Imune", + "400": "Não tributada pelo Simples Nacional", + "500": "ICMS cobrado anteriormente por ST ou por antecipação", + "900": "Outros", +} + + +CST_IPI_ENTRADA: dict[str, str] = { + "00": "Entrada com recuperação de crédito", + "01": "Entrada tributada com alíquota zero", + "02": "Entrada isenta", + "03": "Entrada não-tributada", + "04": "Entrada imune", + "05": "Entrada com suspensão", + "49": "Outras entradas", +} + +CST_IPI_SAIDA: dict[str, str] = { + "50": "Saída tributada", + "51": "Saída tributável com alíquota zero", + "52": "Saída isenta", + "53": "Saída não-tributada", + "54": "Saída imune", + "55": "Saída com suspensão", + "99": "Outras saídas", +} + + +CST_PIS_COFINS_SAIDA: dict[str, str] = { + "01": "Operação tributável (base de cálculo = valor da operação alíquota normal)", + "02": "Operação tributável (base de cálculo = valor da operação alíquota diferenciada)", + "03": "Operação tributável (base de cálculo = quantidade vendida x alíquota por unidade de produto)", + "04": "Operação tributável (tributação monofásica - revenda a alíquota zero)", + "05": "Operação tributável (ST)", + "06": "Operação tributável (alíquota zero)", + "07": "Operação isenta da contribuição", + "08": "Operação sem incidência da contribuição", + "09": "Operação com suspensão da contribuição", + "49": "Outras operações de saída", +} + +CST_PIS_COFINS_ENTRADA: dict[str, str] = { + "50": "Operação com direito a crédito - vinculada exclusivamente a receita tributada no mercado interno", + "51": "Operação com direito a crédito - vinculada exclusivamente a receita não tributada no mercado interno", + "52": "Operação com direito a crédito - vinculada exclusivamente a receita de exportação", + "53": "Operação com direito a crédito - vinculada a receitas tributadas e não-tributadas no mercado interno", + "54": "Operação com direito a crédito - vinculada a receitas tributadas no mercado interno e de exportação", + "55": "Operação com direito a crédito - vinculada a receitas não-tributadas no mercado interno e de exportação", + "56": "Operação com direito a crédito - vinculada a receitas tributadas e não-tributadas no mercado interno e de exportação", + "60": "Crédito presumido - operação de aquisição vinculada exclusivamente a receita tributada no mercado interno", + "61": "Crédito presumido - operação de aquisição vinculada exclusivamente a receita não-tributada no mercado interno", + "62": "Crédito presumido - operação de aquisição vinculada exclusivamente a receita de exportação", + "63": "Crédito presumido - operação de aquisição vinculada a receitas tributadas e não-tributadas no mercado interno", + "64": "Crédito presumido - operação de aquisição vinculada a receitas tributadas no mercado interno e de exportação", + "65": "Crédito presumido - operação de aquisição vinculada a receitas não-tributadas no mercado interno e de exportação", + "66": "Crédito presumido - operação de aquisição vinculada a receitas tributadas e não-tributadas no mercado interno e de exportação", + "67": "Crédito presumido - outras operações", + "70": "Operação de aquisição sem direito a crédito", + "71": "Operação de aquisição com isenção", + "72": "Operação de aquisição com suspensão", + "73": "Operação de aquisição a alíquota zero", + "74": "Operação de aquisição (ST)", + "75": "Operação de aquisição sem incidência da contribuição", + "98": "Outras operações de entrada", + "99": "Outras operações", +} + + +CFOP: dict[str, str] = { + + "1101": "Compra para industrialização ou produção rural", + "1102": "Compra para comercialização", + "1111": "Compra para industrialização de mercadoria recebida anteriormente em consignação industrial", + "1113": "Compra para comercialização, de mercadoria recebida anteriormente em consignação mercantil", + "1116": "Compra para industrialização originada de encomenda para recebimento futuro", + "1117": "Compra para comercialização originada de encomenda para recebimento futuro", + "1122": "Compra para comercialização de energia elétrica", + "1124": "Industrialização efetuada por outra empresa", + "1126": "Compra para utilização na prestação de serviço sujeita ao ISSQN", + "1151": "Transferência para industrialização ou produção rural", + "1152": "Transferência para comercialização", + "1201": "Devolução de venda de produção do estabelecimento", + "1202": "Devolução de venda de mercadoria adquirida ou recebida de terceiros", + "1251": "Compra de energia elétrica para distribuição ou comercialização", + "1301": "Aquisição de serviço de comunicação para execução de serviço da mesma natureza", + "1302": "Aquisição de serviço de comunicação por estabelecimento industrial", + "1303": "Aquisição de serviço de comunicação por estabelecimento comercial", + "1352": "Aquisição de serviço de transporte por estabelecimento comercial", + "1353": "Aquisição de serviço de transporte por estabelecimento de prestador de serviço de comunicação", + "1401": "Compra para industrialização em operação com mercadoria sujeita ao regime de ST", + "1403": "Compra para comercialização em operação com mercadoria sujeita ao regime de ST", + "1407": "Compra de mercadoria para uso ou consumo em operação com mercadoria sujeita ao regime de ST", + "1501": "Entrada de mercadoria recebida com fim específico de exportação", + "1503": "Entrada de mercadoria recebida com fim específico de exportação, de estabelecimento do mesmo titular", + "1556": "Compra de material para uso ou consumo", + "1601": "Recebimento, por transferência, de saldo credor de ICMS", + "1651": "Compra de combustível ou lubrificante para industrialização subsequente", + "1652": "Compra de combustível ou lubrificante para comercialização", + "1653": "Compra de combustível ou lubrificante por consumidor ou usuário final", + "1658": "Transferência de combustível ou lubrificante recebido para industrialização subsequente", + "1659": "Transferência de combustível ou lubrificante recebido para comercialização", + "1660": "Saída de combustível ou lubrificante, de produção do estabelecimento", + "1661": "Saída de combustível ou lubrificante adquirido ou recebido de terceiros", + + "5101": "Venda de produção do estabelecimento", + "5102": "Venda de mercadoria adquirida ou recebida de terceiros", + "5103": "Venda de produção do estabelecimento, efetuada fora do estabelecimento", + "5104": "Venda de mercadoria adquirida ou recebida de terceiros, efetuada fora do estabelecimento", + "5105": "Venda de produção do estabelecimento que não deva por ele transitar", + "5106": "Venda de mercadoria adquirida ou recebida de terceiros, que não deva por ele transitar", + "5109": "Venda de produção do estabelecimento, destinada à Zona Franca de Manaus ou Áreas de Livre Comércio", + "5110": "Venda de mercadoria, adquirida ou recebida de terceiros, destinada à Zona Franca de Manaus ou Áreas de Livre Comércio", + "5111": "Venda de produção do estabelecimento remetida anteriormente em consignação industrial", + "5113": "Venda de mercadoria adquirida ou recebida de terceiros, remetida anteriormente em consignação mercantil", + "5116": "Venda de produção do estabelecimento originada de encomenda para entrega futura", + "5117": "Venda de mercadoria adquirida ou recebida de terceiros, originada de encomenda para entrega futura", + "5122": "Venda de energia elétrica no mercado interno", + "5124": "Industrialização efetuada para outra empresa", + "5125": "Industrialização efetuada para outra empresa quando a mercadoria recebida para utilização no processo de industrialização não transitar pelo estabelecimento adquirente da mercadoria", + "5151": "Transferência de produção do estabelecimento", + "5152": "Transferência de mercadoria adquirida ou recebida de terceiros", + "5201": "Devolução de compra para industrialização ou produção rural", + "5202": "Devolução de compra para comercialização", + "5401": "Venda de produção do estabelecimento em operação com produto sujeito ao regime de ST, como contribuinte substituto", + "5403": "Venda de mercadoria adquirida ou recebida de terceiros em operação com mercadoria sujeita ao regime de ST, como contribuinte substituto", + "5405": "Venda de mercadoria adquirida ou recebida de terceiros em operação com mercadoria sujeita ao regime de ST, como contribuinte substituído", + "5501": "Remessa de produção do estabelecimento, com fim específico de exportação", + "5503": "Remessa de mercadoria adquirida ou recebida de terceiros, com fim específico de exportação", + "5556": "Transferência de material para uso ou consumo", + "5601": "Transferência de saldo credor do ICMS para outro estabelecimento da mesma empresa, destinado à compensação de saldo devedor de ICMS", + "5602": "Transferência de saldo devedor de ICMS para outro estabelecimento da mesma empresa", + "5605": "Transferência de saldo credor de IPI para outro estabelecimento da mesma empresa, destinado à compensação de saldo devedor de IPI", + "5651": "Venda de combustível ou lubrificante de produção do estabelecimento destinados ao processo de industrialização", + "5652": "Venda de combustível ou lubrificante de produção do estabelecimento destinados à comercialização", + "5653": "Venda de combustível ou lubrificante de produção do estabelecimento destinados a consumidor ou usuário final", + # Saídas interestaduais + "6101": "Venda de produção do estabelecimento", + "6102": "Venda de mercadoria adquirida ou recebida de terceiros", + "6107": "Venda de produção do estabelecimento, destinada a não contribuinte", + "6108": "Venda de mercadoria adquirida ou recebida de terceiros, destinada a não contribuinte", + "6151": "Transferência de produção do estabelecimento", + "6152": "Transferência de mercadoria adquirida ou recebida de terceiros", + "6201": "Devolução de compra para industrialização ou produção rural", + "6202": "Devolução de compra para comercialização", +} + + +COD_PAISES: dict[str, str] = { + "1058": "BRASIL", + "0249": "ESTADOS UNIDOS DA AMERICA", + "0230": "ESPANHA", + "0232": "FRANCA", + "0276": "ALEMANHA", + "0040": "ARGENTINA", + "0119": "CHILE", + "0175": "COLOMBIA", + "0715": "REINO UNIDO", + "0538": "JAPAO", + "0628": "MEXICO", + "0765": "RUSSIA", + "0770": "SUECIA", + "0105": "CANADA", + "5760": "CHINA", + "0399": "ITALIA", + "0423": "HOLANDA", + "0773": "SUICA", + "0315": "INDIA", +} + + +CALENDARIO_OBRIGACOES: dict[str, list[str]] = { + "DCTF": ["Mensal - até o 15º dia útil do 2º mês subsequente"], + "EFD_ICMS_IPI": ["Mensal - até o 15º dia útil do mês subsequente"], + "EFD_CONTRIBUICOES": ["Mensal - até o 10º dia útil do 2º mês subsequente"], + "ECD": ["Anual - até o último dia útil de junho do ano seguinte"], + "ECF": ["Anual - até o último dia útil de julho do ano seguinte"], + "ESOCIAL": ["Mensal - folha até o dia 7 do mês seguinte"], + "EFD_REINF": ["Mensal - até o dia 15 do mês seguinte"], + "DEFIS": ["Anual - Simples Nacional - até 31 de março"], + "PGDAS": ["Mensal - Simples Nacional - até o dia 20"], + "DARF_PIS": ["Mensal - até o 25º dia do mês subsequente"], + "DARF_COFINS": ["Mensal - até o 25º dia do mês subsequente"], + "DARF_IRPJ_LP": ["Mensal - DARF estimativa até o último dia útil do mês subsequente"], + "DARF_CSLL_LP": ["Mensal - DARF estimativa até o último dia útil do mês subsequente"], + "DARF_IRPJ_LT": ["Trimestral - até o último dia útil do mês subsequente ao trimestre"], + "DARF_CSLL_LT": ["Trimestral - até o último dia útil do mês subsequente ao trimestre"], +} + + +COD_RECEITA_DARF: dict[str, str] = { + "6912": "IRPJ - Lucro Real - Estimativa Mensal", + "2362": "IRPJ - Lucro Real - Apuração Trimestral", + "2089": "IRPJ - Lucro Presumido - Apuração Trimestral", + "2372": "CSLL - Lucro Real - Estimativa Mensal", + "6773": "CSLL - Lucro Presumido - Apuração Trimestral", + "2484": "CSLL - Lucro Real - Apuração Trimestral", + "8109": "PIS/PASEP - Faturamento - Não-cumulativo", + "7987": "PIS/PASEP - Faturamento - Cumulativo", + "2172": "COFINS - Faturamento - Não-cumulativa", + "5856": "COFINS - Faturamento - Cumulativa", + "5253": "IPI - Geral", + "1038": "IRRF - Rendimentos do Trabalho", + "0561": "IRRF - Serviços Prestados por PJ", +} diff --git a/src/fiscal/entities.py b/src/fiscal/entities.py new file mode 100644 index 0000000000000000000000000000000000000000..7707a4a2c7b9e97bdddd6f11ae9ddb798eafb9b6 --- /dev/null +++ b/src/fiscal/entities.py @@ -0,0 +1,286 @@ + +from __future__ import annotations + +import re +from datetime import date +from decimal import Decimal +from enum import Enum +from typing import Optional + +from pydantic import BaseModel, Field, field_validator + + +class RegimeTributario(str, Enum): + LUCRO_REAL = "1" + LUCRO_PRESUMIDO = "2" + SIMPLES_NACIONAL = "3" + ARBITRADO = "4" + IMUNE = "5" + OPTANTE_SIMPLES = "6" + LUCRO_REAL_TRIMESTRAL = "7" + + +class TipoEmpresa(str, Enum): + INDUSTRIA = "industria" + COMERCIO = "comercio" + SERVICOS = "servicos" + MISTO = "misto" + + +class PerfilSPED(str, Enum): + A = "A" # Escrituração completa + B = "B" # Escrituração com dados simplificados + C = "C" # Escrituração com dados simplificados (ME/EPP Simples) + + +class UF(str, Enum): + AC = "AC"; AL = "AL"; AP = "AP"; AM = "AM" + BA = "BA"; CE = "CE"; DF = "DF"; ES = "ES" + GO = "GO"; MA = "MA"; MT = "MT"; MS = "MS" + MG = "MG"; PA = "PA"; PB = "PB"; PR = "PR" + PE = "PE"; PI = "PI"; RJ = "RJ"; RN = "RN" + RS = "RS"; RO = "RO"; RR = "RR"; SC = "SC" + SP = "SP"; SE = "SE"; TO = "TO" + + +def _limpar_cnpj(cnpj: str) -> str: + return re.sub(r"\D", "", cnpj) + + +def _validar_cnpj(cnpj: str) -> bool: + d = _limpar_cnpj(cnpj) + if len(d) != 14 or d == d[0] * 14: + return False + for i, j in [(12, 13), (13, 14)]: + s = sum(int(d[k]) * ((i - k - 1) % 8 + 2) for k in range(i)) + c = 0 if (r := s % 11) < 2 else 11 - r + if c != int(d[j - 1]): + return False + return True + + +def _validar_cpf(cpf: str) -> bool: + d = re.sub(r"\D", "", cpf) + if len(d) != 11 or d == d[0] * 11: + return False + for i in range(9, 11): + s = sum(int(d[k]) * (i + 1 - k) for k in range(i)) + if (0 if (r := (s * 10 % 11)) >= 10 else r) != int(d[i]): + return False + return True + + +class Endereco(BaseModel): + logradouro: str + numero: str + complemento: str = "" + bairro: str + municipio: str + uf: UF + cep: str + cod_municipio: str = "" + + @field_validator("cep") + @classmethod + def normalizar_cep(cls, v: str) -> str: + return re.sub(r"\D", "", v).zfill(8) + + +class Contato(BaseModel): + nome: str = "" + telefone: str = "" + fax: str = "" + email: str = "" + + +class Empresa(BaseModel): + cnpj: str + razao_social: str + nome_fantasia: str = "" + ie: str = "" + im: str = "" + suframa: str = "" + regime_tributario: RegimeTributario + tipo_empresa: TipoEmpresa + endereco: Endereco + contato: Contato = Field(default_factory=Contato) + perfil_sped: PerfilSPED = PerfilSPED.A + atividade_industrial: bool = False + data_inicio_atividade: Optional[date] = None + + @field_validator("cnpj") + @classmethod + def validar_cnpj(cls, v: str) -> str: + d = _limpar_cnpj(v) + if not _validar_cnpj(d): + raise ValueError(f"CNPJ inválido: {v}") + return d + + @property + def cnpj_formatado(self) -> str: + d = self.cnpj + return f"{d[:2]}.{d[2:5]}.{d[5:8]}/{d[8:12]}-{d[12:]}" + + +class Contador(BaseModel): + nome: str + cpf: str + crc: str + cnpj_escritorio: str = "" + endereco: Optional[Endereco] = None + contato: Contato = Field(default_factory=Contato) + + @field_validator("cpf") + @classmethod + def validar_cpf(cls, v: str) -> str: + d = re.sub(r"\D", "", v) + if not _validar_cpf(d): + raise ValueError(f"CPF inválido: {v}") + return d + + +class Produto(BaseModel): + codigo: str + descricao: str + ncm: str + ex_ipi: str = "" + unidade: str + tipo_item: str = "00" + cod_gen: str = "" + aliq_icms: Decimal = Decimal("0") + aliq_ipi: Decimal = Decimal("0") + cst_icms: str = "000" + cst_ipi: str = "50" + cst_pis: str = "01" + cst_cofins: str = "01" + cest: str = "" + cod_barras: str = "" + peso_bruto: Decimal = Decimal("0") + peso_liquido: Decimal = Decimal("0") + + +class ItemNotaFiscal(BaseModel): + numero_item: int + produto: Produto + cfop: str + quantidade: Decimal + valor_unitario: Decimal + desconto: Decimal = Decimal("0") + outras_despesas: Decimal = Decimal("0") + frete: Decimal = Decimal("0") + seguro: Decimal = Decimal("0") + + @property + def valor_produtos(self) -> Decimal: + return (self.quantidade * self.valor_unitario) - self.desconto + + @property + def base_icms(self) -> Decimal: + return self.valor_produtos + self.frete + self.seguro + self.outras_despesas + + @property + def valor_icms(self) -> Decimal: + return (self.base_icms * self.produto.aliq_icms / 100).quantize(Decimal("0.01")) + + @property + def base_ipi(self) -> Decimal: + return self.valor_produtos + + @property + def valor_ipi(self) -> Decimal: + return (self.base_ipi * self.produto.aliq_ipi / 100).quantize(Decimal("0.01")) + + @property + def valor_total(self) -> Decimal: + return self.valor_produtos + self.valor_ipi + self.frete + self.seguro + self.outras_despesas + + +class NotaFiscal(BaseModel): + numero: str + serie: str + modelo: str = "55" + data_emissao: date + data_saida_entrada: date + hora_saida_entrada: str = "00:00:00" + emitente: Empresa + destinatario: Empresa + natureza_operacao: str + tipo_operacao: str = "1" + ind_pagamento: str = "0" + chave_acesso: str = "" + protocolo: str = "" + itens: list[ItemNotaFiscal] = Field(default_factory=list) + info_complementar: str = "" + + @property + def valor_produtos(self) -> Decimal: + return sum(i.valor_produtos for i in self.itens) + + @property + def valor_frete(self) -> Decimal: + return sum(i.frete for i in self.itens) + + @property + def valor_seguro(self) -> Decimal: + return sum(i.seguro for i in self.itens) + + @property + def valor_desconto(self) -> Decimal: + return sum(i.desconto for i in self.itens) + + @property + def valor_outras_despesas(self) -> Decimal: + return sum(i.outras_despesas for i in self.itens) + + @property + def valor_icms(self) -> Decimal: + return sum(i.valor_icms for i in self.itens) + + @property + def valor_ipi(self) -> Decimal: + return sum(i.valor_ipi for i in self.itens) + + @property + def valor_total(self) -> Decimal: + return sum(i.valor_total for i in self.itens) + + +class LancamentoContabil(BaseModel): + data: date + numero_lancamento: str + historico: str + debito_conta: str + credito_conta: str + valor: Decimal + complemento: str = "" + + +class MovimentacaoEstoque(BaseModel): + data: date + produto: Produto + tipo_movimento: str + quantidade: Decimal + valor_unitario: Decimal + documento: str = "" + + @property + def valor_total(self) -> Decimal: + return self.quantidade * self.valor_unitario + + +class PeriodoApuracao(BaseModel): + data_inicio: date + data_fim: date + empresa: Empresa + notas_saida: list[NotaFiscal] = Field(default_factory=list) + notas_entrada: list[NotaFiscal] = Field(default_factory=list) + lancamentos: list[LancamentoContabil] = Field(default_factory=list) + movimentacoes: list[MovimentacaoEstoque] = Field(default_factory=list) + + @property + def total_vendas(self) -> Decimal: + return sum(nf.valor_total for nf in self.notas_saida) + + @property + def total_compras(self) -> Decimal: + return sum(nf.valor_total for nf in self.notas_entrada) diff --git a/src/generators/__init__.py b/src/generators/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/generators/__pycache__/__init__.cpython-312.pyc b/src/generators/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ac28109f21e4c918d54d6fb4b10d51d7f7fe6b66 Binary files /dev/null and b/src/generators/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/generators/__pycache__/cte.cpython-312.pyc b/src/generators/__pycache__/cte.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..024a91a88107935270db5c413902c4dafc79bd46 Binary files /dev/null and b/src/generators/__pycache__/cte.cpython-312.pyc differ diff --git a/src/generators/__pycache__/dctf.cpython-312.pyc b/src/generators/__pycache__/dctf.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d28460439241f5aa56221a49910fef479c3a396 Binary files /dev/null and b/src/generators/__pycache__/dctf.cpython-312.pyc differ diff --git a/src/generators/__pycache__/defis.cpython-312.pyc b/src/generators/__pycache__/defis.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cdb06b3be29f753250927eb570897a79a4329d61 Binary files /dev/null and b/src/generators/__pycache__/defis.cpython-312.pyc differ diff --git a/src/generators/__pycache__/destda.cpython-312.pyc b/src/generators/__pycache__/destda.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..976ff4df0dce742dfb158749f475c8ce08217931 Binary files /dev/null and b/src/generators/__pycache__/destda.cpython-312.pyc differ diff --git a/src/generators/__pycache__/dirf.cpython-312.pyc b/src/generators/__pycache__/dirf.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..098cb98cfc12c1199386c67546bf535500924aef Binary files /dev/null and b/src/generators/__pycache__/dirf.cpython-312.pyc differ diff --git a/src/generators/__pycache__/ecd.cpython-312.pyc b/src/generators/__pycache__/ecd.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3c084e11bc334758e7b381a913f0bdf677a43b5e Binary files /dev/null and b/src/generators/__pycache__/ecd.cpython-312.pyc differ diff --git a/src/generators/__pycache__/ecf.cpython-312.pyc b/src/generators/__pycache__/ecf.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e4f82c3d7bf3c73a33733a3e78b0e83d2d57796a Binary files /dev/null and b/src/generators/__pycache__/ecf.cpython-312.pyc differ diff --git a/src/generators/__pycache__/efd_contribuicoes.cpython-312.pyc b/src/generators/__pycache__/efd_contribuicoes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f79a6fa7edeab1811b110944e262f16bd4419b22 Binary files /dev/null and b/src/generators/__pycache__/efd_contribuicoes.cpython-312.pyc differ diff --git a/src/generators/__pycache__/efd_icms_ipi.cpython-312.pyc b/src/generators/__pycache__/efd_icms_ipi.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..61377f152661a60f34869e1cee3186fb3f795c82 Binary files /dev/null and b/src/generators/__pycache__/efd_icms_ipi.cpython-312.pyc differ diff --git a/src/generators/__pycache__/efd_reinf.cpython-312.pyc b/src/generators/__pycache__/efd_reinf.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5c4169a0e58242d3ebfd2d60751a085164b75965 Binary files /dev/null and b/src/generators/__pycache__/efd_reinf.cpython-312.pyc differ diff --git a/src/generators/__pycache__/esocial.cpython-312.pyc b/src/generators/__pycache__/esocial.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6f0ecc85a8b93a5756b5636d2a6a0600e11e4b15 Binary files /dev/null and b/src/generators/__pycache__/esocial.cpython-312.pyc differ diff --git a/src/generators/__pycache__/gia.cpython-312.pyc b/src/generators/__pycache__/gia.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d51216e169845cce25f0f56adcaf7ae9c91980d2 Binary files /dev/null and b/src/generators/__pycache__/gia.cpython-312.pyc differ diff --git a/src/generators/__pycache__/mdfe.cpython-312.pyc b/src/generators/__pycache__/mdfe.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..910e6ca51ea95dd6bdec00f3cc06a4f96168d209 Binary files /dev/null and b/src/generators/__pycache__/mdfe.cpython-312.pyc differ diff --git a/src/generators/__pycache__/nfce_xml.cpython-312.pyc b/src/generators/__pycache__/nfce_xml.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a86ecfb82da0575bcbffae66a2c405d17948729e Binary files /dev/null and b/src/generators/__pycache__/nfce_xml.cpython-312.pyc differ diff --git a/src/generators/__pycache__/nfe_xml.cpython-312.pyc b/src/generators/__pycache__/nfe_xml.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c98f2744eb07cfda59844845d6722b19e212ba6 Binary files /dev/null and b/src/generators/__pycache__/nfe_xml.cpython-312.pyc differ diff --git a/src/generators/__pycache__/sped_writer.cpython-312.pyc b/src/generators/__pycache__/sped_writer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..242329b9a376771979fe659cc61c13eb92c67bde Binary files /dev/null and b/src/generators/__pycache__/sped_writer.cpython-312.pyc differ diff --git a/src/generators/cte.py b/src/generators/cte.py new file mode 100644 index 0000000000000000000000000000000000000000..edf35a0ff5f68ee1e2794c25bc88563cc0ee7faf --- /dev/null +++ b/src/generators/cte.py @@ -0,0 +1,334 @@ + +from __future__ import annotations + +import hashlib +import re +from dataclasses import dataclass, field +from datetime import date, datetime +from decimal import Decimal +from pathlib import Path +from xml.etree import ElementTree as ET + +from src.fiscal.entities import Empresa + +NS_CTE = "http://www.portalfiscal.inf.br/cte" +VERSAO_CTE = "4.00" + + +def _sub(pai: ET.Element, tag: str, texto: str = "") -> ET.Element: + el = ET.SubElement(pai, tag) + if texto: + el.text = str(texto) + return el + + +def _fmt_dec(v: Decimal, casas: int = 2) -> str: + return f"{v:.{casas}f}" + + +def _limpar(s: str) -> str: + return re.sub(r"[^\w\s\-.,/@]", "", str(s))[:60] + + +# --------------------------------------------------------------------------- +# Dataclasses auxiliares +# --------------------------------------------------------------------------- + +@dataclass +class CargaCTe: + descricao: str + produto: str # código produto predominante + valor_carga: Decimal + peso_kg: Decimal = field(default_factory=lambda: Decimal("0")) + volume_m3: Decimal = field(default_factory=lambda: Decimal("0")) + + +@dataclass +class DocumentoReferenciado: + chave_nfe: str # chave NF-e 44 dígitos + + +@dataclass +class DadosTransporte: + rntrc: str # Registro Nacional de Transportadores Rodoviários de Cargas + placa_veiculo: str + uf_veiculo: str + data_prevista_entrega: date + + +@dataclass +class ParteCTe: # remetente, destinatário, etc. + cnpj: str + razao_social: str + endereco: str + municipio: str + uf: str + cep: str + cod_municipio: str + + +# --------------------------------------------------------------------------- +# Gerador principal +# --------------------------------------------------------------------------- + +class GeradorCTe: + """ + Gera o XML de CT-e conforme o leiaute 4.00 da SEFAZ. + + O XML gerado deve ser assinado digitalmente com certificado ICP-Brasil + antes da transmissão. + """ + + VERSAO = "4.00" + MOD = "57" + + _CUF_MAP = { + "AC": "12", "AL": "27", "AP": "16", "AM": "13", "BA": "29", + "CE": "23", "DF": "53", "ES": "32", "GO": "52", "MA": "21", + "MT": "51", "MS": "50", "MG": "31", "PA": "15", "PB": "25", + "PR": "41", "PE": "26", "PI": "22", "RJ": "33", "RN": "24", + "RS": "43", "RO": "11", "RR": "14", "SC": "42", "SP": "35", + "SE": "28", "TO": "17", + } + + def __init__( + self, + emitente: Empresa, + remetente: ParteCTe, + destinatario: ParteCTe, + carga: CargaCTe, + documentos: list[DocumentoReferenciado], + transporte: DadosTransporte, + numero: str = "1", + serie: str = "001", + cfop: str = "6352", + natureza_operacao: str = "PRESTAÇÃO DE SERVIÇO DE TRANSPORTE", + aliq_icms: Decimal = Decimal("12"), + ambiente: str = "2", + ): + self.emitente = emitente + self.remetente = remetente + self.destinatario = destinatario + self.carga = carga + self.documentos = documentos + self.transporte = transporte + self.numero = numero + self.serie = serie + self.cfop = cfop + self.natureza_operacao = natureza_operacao + self.aliq_icms = aliq_icms + self.ambiente = ambiente # 1=Produção, 2=Homologação + self._data_emissao: date = date.today() + + # ------------------------------------------------------------------ + # Utilitários internos + # ------------------------------------------------------------------ + + def _cuf(self, uf: str) -> str: + return self._CUF_MAP.get(uf.upper(), "35") + + def _gerar_chave(self) -> str: + """ + Gera a chave de acesso de 44 dígitos do CT-e. + Estrutura: cUF(2) + AAMM(4) + CNPJ(14) + mod(2=57) + serie(3) + + nCT(9) + tpEmis(1) + cCT(8) + cDV(1) + """ + uf = self._cuf(self.emitente.endereco.uf.value) + aamm = self._data_emissao.strftime("%y%m") + cnpj = self.emitente.cnpj.zfill(14) + mod = self.MOD.zfill(2) + serie = self.serie.zfill(3) + numero = self.numero.zfill(9) + tp_emis = "1" + c_ct = hashlib.md5(f"{cnpj}{numero}".encode()).hexdigest()[:8].upper() + c_ct_digits = "".join(str(int(c, 16) % 10) for c in c_ct)[:8] + + chave_sem_dv = f"{uf}{aamm}{cnpj}{mod}{serie}{numero}{tp_emis}{c_ct_digits}" + + + pesos = [2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, + 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4] + soma = sum(int(chave_sem_dv[i]) * pesos[i] for i in range(43)) + resto = soma % 11 + dv = 0 if resto in (0, 1) else 11 - resto + + return chave_sem_dv + str(dv) + + def _fmt_dhemi(self) -> str: + return ( + datetime.combine(self._data_emissao, datetime.min.time()) + .strftime("%Y-%m-%dT%H:%M:%S") + "-03:00" + ) + + # ------------------------------------------------------------------ + # Blocos XML + # ------------------------------------------------------------------ + + def _ide(self, inf_cte: ET.Element, chave: str) -> None: + emit_uf = self.emitente.endereco.uf.value + cuf = self._cuf(emit_uf) + ide = _sub(inf_cte, "ide") + _sub(ide, "cUF", cuf) + _sub(ide, "cCT", chave[35:43]) + _sub(ide, "CFOP", self.cfop) + _sub(ide, "natOp", _limpar(self.natureza_operacao)) + _sub(ide, "mod", self.MOD) + _sub(ide, "serie", self.serie.zfill(3)) + _sub(ide, "nCT", self.numero.zfill(9)) + _sub(ide, "dhEmi", self._fmt_dhemi()) + _sub(ide, "tpImp", "1") # 1=DACTE normal retrato + _sub(ide, "tpEmis", "1") # 1=Emissão normal + _sub(ide, "cDV", chave[-1]) + _sub(ide, "tpAmb", self.ambiente) + _sub(ide, "tpCT", "0") # 0=CT-e normal + _sub(ide, "tpServ", "0") # 0=Normal + cod_mun_emit = self.emitente.endereco.cod_municipio or "3550308" + _sub(ide, "cMunEnv", cod_mun_emit) + _sub(ide, "xMunEnv", _limpar(self.emitente.endereco.municipio)) + _sub(ide, "UFEnv", emit_uf) + _sub(ide, "modal", "01") # 01=Rodoviário + _sub(ide, "dhSaidaOrig", self._fmt_dhemi()) + _sub(ide, "tpMultimodal", "0") + _sub(ide, "UFIni", self.remetente.uf) + _sub(ide, "UFFim", self.destinatario.uf) + + def _compl(self, inf_cte: ET.Element) -> None: + compl = _sub(inf_cte, "compl") + _sub(compl, "xObs", _limpar(self.carga.descricao)[:160] if self.carga.descricao else "") + + def _emit(self, inf_cte: ET.Element) -> None: + emp = self.emitente + end = emp.endereco + emit = _sub(inf_cte, "emit") + _sub(emit, "CNPJ", emp.cnpj) + if emp.ie: + _sub(emit, "IE", re.sub(r"\D", "", emp.ie)) + _sub(emit, "xNome", _limpar(emp.razao_social)[:60]) + if emp.nome_fantasia: + _sub(emit, "xFant", _limpar(emp.nome_fantasia)[:60]) + ender_emit = _sub(emit, "enderEmit") + _sub(ender_emit, "xLgr", _limpar(end.logradouro)) + _sub(ender_emit, "nro", end.numero) + if end.complemento: + _sub(ender_emit, "xCpl", _limpar(end.complemento)) + _sub(ender_emit, "xBairro", _limpar(end.bairro)) + _sub(ender_emit, "cMun", end.cod_municipio or "3550308") + _sub(ender_emit, "xMun", _limpar(end.municipio)) + _sub(ender_emit, "CEP", re.sub(r"\D", "", end.cep)) + _sub(ender_emit, "UF", end.uf.value) + _sub(ender_emit, "cPais", "1058") + _sub(ender_emit, "xPais", "Brasil") + if emp.contato.telefone: + _sub(emit, "fone", re.sub(r"\D", "", emp.contato.telefone)) + + def _parte(self, inf_cte: ET.Element, tag: str, ender_tag: str, parte: ParteCTe) -> None: + el = _sub(inf_cte, tag) + _sub(el, "CNPJ", re.sub(r"\D", "", parte.cnpj).zfill(14)) + _sub(el, "xNome", _limpar(parte.razao_social)[:60]) + ender = _sub(el, ender_tag) + _sub(ender, "xLgr", _limpar(parte.endereco)) + _sub(ender, "nro", "S/N") + _sub(ender, "xBairro", "CENTRO") + _sub(ender, "cMun", parte.cod_municipio) + _sub(ender, "xMun", _limpar(parte.municipio)) + _sub(ender, "CEP", re.sub(r"\D", "", parte.cep).zfill(8)) + _sub(ender, "UF", parte.uf) + _sub(ender, "cPais", "1058") + _sub(ender, "xPais", "Brasil") + + def _v_prest(self, inf_cte: ET.Element) -> None: + v_prest = _sub(inf_cte, "vPrest") + _sub(v_prest, "vTPrest", _fmt_dec(self.carga.valor_carga)) + _sub(v_prest, "vRec", _fmt_dec(self.carga.valor_carga)) + + def _imp(self, inf_cte: ET.Element) -> None: + imp = _sub(inf_cte, "imp") + icms = _sub(imp, "ICMS") + icms00 = _sub(icms, "ICMS00") + _sub(icms00, "CST", "00") + v_bc = self.carga.valor_carga + v_icms = (v_bc * self.aliq_icms / Decimal("100")).quantize(Decimal("0.01")) + _sub(icms00, "vBC", _fmt_dec(v_bc)) + _sub(icms00, "pICMS", _fmt_dec(self.aliq_icms, 4)) + _sub(icms00, "vICMS", _fmt_dec(v_icms)) + + def _inf_cte_norm(self, inf_cte: ET.Element) -> None: + inf_cte_norm = _sub(inf_cte, "infCTeNorm") + + + inf_carga = _sub(inf_cte_norm, "infCarga") + _sub(inf_carga, "vCarga", _fmt_dec(self.carga.valor_carga)) + _sub(inf_carga, "proPred", _limpar(self.carga.produto)[:60]) + inf_q = _sub(inf_carga, "infQ") + _sub(inf_q, "cUnid", "01") # 01=KG + _sub(inf_q, "tpMed", "PESO BRUTO") + _sub(inf_q, "qCarga", _fmt_dec(self.carga.peso_kg, 4)) + + + if self.documentos: + inf_doc = _sub(inf_cte_norm, "infDoc") + for doc in self.documentos: + inf_nfe = _sub(inf_doc, "infNFe") + _sub(inf_nfe, "chave", doc.chave_nfe) + _sub(inf_nfe, "pin", "") + + + inf_modal = _sub(inf_cte_norm, "infModal") + inf_modal.set("versaoModal", "3.00") + rodo = _sub(inf_modal, "rodo") + _sub(rodo, "RNTRC", self.transporte.rntrc) + veic = _sub(rodo, "veicTracao") + _sub(veic, "placa", self.transporte.placa_veiculo.upper()) + _sub(veic, "UF", self.transporte.uf_veiculo.upper()) + _sub(veic, "prop") # tag vazia + occ = _sub(rodo, "occ") + _sub(occ, "serie", "001") + _sub(occ, "nOcc", "1") + _sub(occ, "dEmi", self._data_emissao.strftime("%Y-%m-%d")) + emi_occ = _sub(occ, "emiOcc") + _sub(emi_occ, "CNPJ", self.emitente.cnpj) + _sub(emi_occ, "xNome", _limpar(self.emitente.razao_social)[:60]) + _sub(emi_occ, "cInt", "") + _sub(emi_occ, "IE", "") + + # ------------------------------------------------------------------ + # API pública + # ------------------------------------------------------------------ + + def gerar_xml(self) -> str: + """Retorna o XML do CT-e não assinado como string.""" + chave = self._gerar_chave() + + cte_proc = ET.Element("cteProc") + cte_proc.set("xmlns", NS_CTE) + cte_proc.set("versao", self.VERSAO) + + cte = _sub(cte_proc, "CTe") + cte.set("xmlns", NS_CTE) + + inf_cte = _sub(cte, "infCte") + inf_cte.set("versao", self.VERSAO) + inf_cte.set("Id", f"CTe{chave}") + + self._ide(inf_cte, chave) + self._compl(inf_cte) + self._emit(inf_cte) + self._parte(inf_cte, "rem", "enderReme", self.remetente) + self._parte(inf_cte, "dest", "enderDest", self.destinatario) + self._v_prest(inf_cte) + self._imp(inf_cte) + self._inf_cte_norm(inf_cte) + + + _sub(cte, "Signature") + + ET.indent(cte_proc, space=" ") + return '\n' + ET.tostring(cte_proc, encoding="unicode") + + def salvar(self, diretorio: str | Path = ".") -> Path: + """Salva o XML em diretório informado e retorna o caminho do arquivo.""" + cnpj = self.emitente.cnpj + caminho = Path(diretorio) / f"CTe_{cnpj}_{self.numero.zfill(9)}.xml" + caminho.parent.mkdir(parents=True, exist_ok=True) + caminho.write_text(self.gerar_xml(), encoding="utf-8") + return caminho diff --git a/src/generators/dctf.py b/src/generators/dctf.py new file mode 100644 index 0000000000000000000000000000000000000000..5f1b4e49440321d512af064d771927469ea81aad --- /dev/null +++ b/src/generators/dctf.py @@ -0,0 +1,271 @@ + +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import date +from decimal import Decimal +from pathlib import Path +from typing import Optional +from xml.etree import ElementTree as ET + +from src.fiscal.constants import COD_RECEITA_DARF +from src.fiscal.entities import Empresa + + +def _sub(pai: ET.Element, tag: str, texto: str = "") -> ET.Element: + el = ET.SubElement(pai, tag) + if texto: + el.text = str(texto) + return el + + +def _fmt_dec(v: Decimal) -> str: + return f"{v:.2f}" + + +@dataclass +class DebitoDCTF: + """Representa um débito tributário na DCTF.""" + codigo_receita: str + periodo_apuracao: str # YYYY-MM ou YYYY-TT (trimestre) + valor_debito: Decimal + valor_suspenso: Decimal = Decimal("0") + numero_processo: str = "" + tipo_suspensao: str = "" # "D"=Decisão Judicial, "A"=Administrativo + + +@dataclass +class PagamentoDCTF: + """Pagamento (DARF) vinculado a um débito.""" + numero_darf: str + data_pagamento: date + valor_principal: Decimal + valor_multa: Decimal = Decimal("0") + valor_juros: Decimal = Decimal("0") + + @property + def valor_total(self) -> Decimal: + return self.valor_principal + self.valor_multa + self.valor_juros + + +@dataclass +class CompensacaoDCTF: + """Compensação de crédito vinculada a um débito.""" + numero_per_comp: str # Número do PER/COMP + data_transmissao: date + valor_compensado: Decimal + codigo_receita_credito: str + + +@dataclass +class ItemDCTF: + """Item de débito com seus pagamentos e compensações.""" + debito: DebitoDCTF + pagamentos: list[PagamentoDCTF] = field(default_factory=list) + compensacoes: list[CompensacaoDCTF] = field(default_factory=list) + + @property + def total_pago(self) -> Decimal: + return sum(p.valor_total for p in self.pagamentos) + + @property + def total_compensado(self) -> Decimal: + return sum(c.valor_compensado for c in self.compensacoes) + + @property + def saldo_a_pagar(self) -> Decimal: + return max( + Decimal("0"), + self.debito.valor_debito - self.debito.valor_suspenso + - self.total_pago - self.total_compensado, + ) + + +@dataclass +class ConfigDCTF: + finalidade: str = "0" # 0=Original, 1=Retificadora + situacao_especial: str = "" # Em branco=sem situação especial + declarante_pj_inativa: bool = False + + +class GeradorDCTF: + """ + Gera o arquivo XML da DCTF para importação no PGD DCTF Web. + + Periodicidade: mensal. + Prazo: 15º dia útil do 2º mês subsequente ao período de apuração. + Obrigação: pessoas jurídicas com débitos de tributos federais. + """ + + VERSAO = "1.0" + + def __init__( + self, + empresa: Empresa, + periodo: str, # "YYYY-MM" + itens: Optional[list[ItemDCTF]] = None, + cfg: Optional[ConfigDCTF] = None, + ): + self.empresa = empresa + self.periodo = periodo + self.itens = itens or [] + self.cfg = cfg or ConfigDCTF() + + def _resumo_por_codigo(self) -> dict[str, dict]: + resumo: dict[str, dict] = {} + for item in self.itens: + cod = item.debito.codigo_receita + if cod not in resumo: + resumo[cod] = { + "debito": Decimal("0"), + "suspenso": Decimal("0"), + "pago": Decimal("0"), + "compensado": Decimal("0"), + "saldo": Decimal("0"), + "nome": COD_RECEITA_DARF.get(cod, cod), + } + resumo[cod]["debito"] += item.debito.valor_debito + resumo[cod]["suspenso"] += item.debito.valor_suspenso + resumo[cod]["pago"] += item.total_pago + resumo[cod]["compensado"] += item.total_compensado + resumo[cod]["saldo"] += item.saldo_a_pagar + return resumo + + def gerar_xml(self) -> str: + """Gera o XML da DCTF para importação no PGD DCTF Web.""" + root = ET.Element("DCTF") + root.set("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") + root.set("versao", self.VERSAO) + + ide = _sub(root, "ideDeclarante") + _sub(ide, "CNPJ", self.empresa.cnpj) + _sub(ide, "nome", self.empresa.razao_social[:100]) + _sub(ide, "perApuracao", self.periodo) + _sub(ide, "dtInicio", f"{self.periodo}-01") + _sub(ide, "dtFim", f"{self.periodo}-{self._ultimo_dia()}") + _sub(ide, "situacaoEspecial", self.cfg.situacao_especial) + _sub(ide, "indDeclaranteInativo", "S" if self.cfg.declarante_pj_inativa else "N") + _sub(ide, "finalidade", self.cfg.finalidade) + + + for item in self.itens: + deb_el = _sub(root, "debito") + _sub(deb_el, "codReceita", item.debito.codigo_receita) + _sub(deb_el, "perApuracao", item.debito.periodo_apuracao) + _sub(deb_el, "vlrDebito", _fmt_dec(item.debito.valor_debito)) + + if item.debito.valor_suspenso > 0: + susp = _sub(deb_el, "suspensao") + _sub(susp, "tipoSuspensao", item.debito.tipo_suspensao or "D") + _sub(susp, "nrProcesso", item.debito.numero_processo) + _sub(susp, "vlrSuspenso", _fmt_dec(item.debito.valor_suspenso)) + + for pag in item.pagamentos: + pag_el = _sub(deb_el, "pagamento") + _sub(pag_el, "nrDarf", pag.numero_darf) + _sub(pag_el, "dtPagamento", pag.data_pagamento.strftime("%Y-%m-%d")) + _sub(pag_el, "vlrPrincipal", _fmt_dec(pag.valor_principal)) + _sub(pag_el, "vlrMulta", _fmt_dec(pag.valor_multa)) + _sub(pag_el, "vlrJuros", _fmt_dec(pag.valor_juros)) + _sub(pag_el, "vlrTotal", _fmt_dec(pag.valor_total)) + + for comp in item.compensacoes: + comp_el = _sub(deb_el, "compensacao") + _sub(comp_el, "nrPERCOMP", comp.numero_per_comp) + _sub(comp_el, "dtTransmissao", comp.data_transmissao.strftime("%Y-%m-%d")) + _sub(comp_el, "vlrCompensado", _fmt_dec(comp.valor_compensado)) + _sub(comp_el, "codReceitaCredito", comp.codigo_receita_credito) + + _sub(deb_el, "vlrSaldoPagar", _fmt_dec(item.saldo_a_pagar)) + + + resumo = self._resumo_por_codigo() + total_el = _sub(root, "totalDebitos") + _sub(total_el, "vlrTotalDebitos", _fmt_dec(sum(r["debito"] for r in resumo.values()))) + _sub(total_el, "vlrTotalSuspenso", _fmt_dec(sum(r["suspenso"] for r in resumo.values()))) + _sub(total_el, "vlrTotalPago", _fmt_dec(sum(r["pago"] for r in resumo.values()))) + _sub(total_el, "vlrTotalCompensado", _fmt_dec(sum(r["compensado"] for r in resumo.values()))) + _sub(total_el, "vlrTotalSaldo", _fmt_dec(sum(r["saldo"] for r in resumo.values()))) + + ET.indent(root, space=" ") + return '\n' + ET.tostring(root, encoding="unicode") + + def _ultimo_dia(self) -> str: + ano, mes = map(int, self.periodo.split("-")) + if mes == 12: + return "31" + ultimo = (date(ano, mes + 1, 1) - date(ano, mes, 1)).days + return str(ultimo).zfill(2) + + def salvar(self, diretorio: str | Path = ".") -> Path: + caminho = Path(diretorio) / f"DCTF_{self.empresa.cnpj}_{self.periodo}.xml" + caminho.parent.mkdir(parents=True, exist_ok=True) + caminho.write_text(self.gerar_xml(), encoding="utf-8") + return caminho + + def relatorio_resumo(self) -> str: + resumo = self._resumo_por_codigo() + linhas = [ + f"DCTF — {self.empresa.razao_social} — Período: {self.periodo}", + "=" * 70, + ] + total_geral = Decimal("0") + for cod, dados in resumo.items(): + linhas.append( + f"{cod} — {dados['nome']}" + ) + linhas.append(f" Débito: R$ {dados['debito']:>12,.2f}") + if dados["suspenso"] > 0: + linhas.append(f" Suspenso: R$ {dados['suspenso']:>12,.2f}") + if dados["pago"] > 0: + linhas.append(f" Pago (DARF): R$ {dados['pago']:>12,.2f}") + if dados["compensado"] > 0: + linhas.append(f" Compensado: R$ {dados['compensado']:>12,.2f}") + linhas.append(f" Saldo: R$ {dados['saldo']:>12,.2f}") + total_geral += dados["saldo"] + + linhas += [ + "=" * 70, + f"TOTAL A PAGAR: R$ {total_geral:>12,.2f}", + ] + return "\n".join(linhas) + + +def montar_dctf_do_periodo( + empresa: Empresa, + periodo: str, + valor_irpj: Decimal = Decimal("0"), + valor_csll: Decimal = Decimal("0"), + valor_pis: Decimal = Decimal("0"), + valor_cofins: Decimal = Decimal("0"), + valor_cpp: Decimal = Decimal("0"), + valor_irrf: Decimal = Decimal("0"), + darfs_pagos: Optional[list[PagamentoDCTF]] = None, +) -> GeradorDCTF: + """ + Função auxiliar: monta a DCTF automaticamente a partir dos valores apurados. + """ + darfs_pagos = darfs_pagos or [] + itens: list[ItemDCTF] = [] + + pares = [ + ("6912", valor_irpj), + ("2484", valor_csll), + ("8109", valor_pis), + ("2172", valor_cofins), + ("2100", valor_cpp), + ("1038", valor_irrf), + ] + + for cod, valor in pares: + if valor > 0: + itens.append(ItemDCTF( + debito=DebitoDCTF( + codigo_receita=cod, + periodo_apuracao=periodo, + valor_debito=valor, + ), + pagamentos=[p for p in darfs_pagos], + )) + + return GeradorDCTF(empresa=empresa, periodo=periodo, itens=itens) diff --git a/src/generators/defis.py b/src/generators/defis.py new file mode 100644 index 0000000000000000000000000000000000000000..2ca22cd1b8fb0221cf979475708bfe281695f165 --- /dev/null +++ b/src/generators/defis.py @@ -0,0 +1,200 @@ + +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import date +from decimal import Decimal +from pathlib import Path +from xml.etree import ElementTree as ET + +from src.fiscal.entities import Empresa + + +def _sub(pai: ET.Element, tag: str, texto: str = "") -> ET.Element: + el = ET.SubElement(pai, tag) + if texto: + el.text = str(texto) + return el + + +def _fmt_dec(v: Decimal) -> str: + return f"{v:.2f}" + + +@dataclass +class ReceitaMensalDEFIS: + mes: int # 1-12 + receita_bruta_total: Decimal + receita_bruta_exportacao: Decimal = field(default_factory=lambda: Decimal("0")) + receita_bruta_isenta: Decimal = field(default_factory=lambda: Decimal("0")) + + +@dataclass +class SocioDEFIS: + cpf_cnpj: str + nome: str + percentual_capital: Decimal # ex: Decimal("50.00") = 50% + tipo: str = "PF" # "PF" ou "PJ" + pais_domicilio: str = "105" # 105=Brasil (código BACEN) + + +@dataclass +class EmpregadoDEFIS: + competencia: str # "YYYY-MM" + quantidade: int + + +class GeradorDEFIS: + """ + Gera o arquivo DEFIS para declaração anual do Simples Nacional. + + Prazo: 31 de março do ano seguinte ao período de apuração. + """ + + VERSAO = "1.0" + + def __init__( + self, + empresa: Empresa, + ano_calendario: int, + receitas_mensais: list[ReceitaMensalDEFIS], + socios: list[SocioDEFIS], + empregados: list[EmpregadoDEFIS] | None = None, + retificadora: bool = False, + numero_recibo_retificada: str = "", + ): + self.empresa = empresa + self.ano_calendario = ano_calendario + self.receitas_mensais = receitas_mensais + self.socios = socios + self.empregados = empregados or [] + self.retificadora = retificadora + self.numero_recibo_retificada = numero_recibo_retificada + + # ------------------------------------------------------------------ + # Totais auxiliares + # ------------------------------------------------------------------ + + @property + def _total_receita(self) -> Decimal: + return sum(r.receita_bruta_total for r in self.receitas_mensais) + + @property + def _total_exportacao(self) -> Decimal: + return sum(r.receita_bruta_exportacao for r in self.receitas_mensais) + + @property + def _total_isenta(self) -> Decimal: + return sum(r.receita_bruta_isenta for r in self.receitas_mensais) + + # ------------------------------------------------------------------ + # Geração do XML + # ------------------------------------------------------------------ + + def gerar_xml(self) -> str: + """Gera o XML DEFIS para transmissão.""" + root = ET.Element("DEFIS") + root.set("versao", self.VERSAO) + + # Identificação do declarante + ide = _sub(root, "ideDeclarante") + _sub(ide, "CNPJ", self.empresa.cnpj) + _sub(ide, "anoCalendario", str(self.ano_calendario)) + _sub(ide, "indRetificadora", "S" if self.retificadora else "N") + nrec = _sub(ide, "nrecRetificadora") + if self.retificadora and self.numero_recibo_retificada: + nrec.text = self.numero_recibo_retificada + _sub(ide, "dtInicio", f"{self.ano_calendario}-01-01") + _sub(ide, "dtFim", f"{self.ano_calendario}-12-31") + + + inf = _sub(root, "infEmpresa") + _sub(inf, "xNome", self.empresa.razao_social[:150]) + _sub(inf, "xFantasia", self.empresa.nome_fantasia[:60] if self.empresa.nome_fantasia else "") + _sub(inf, "fone", self.empresa.contato.telefone) + _sub(inf, "email", self.empresa.contato.email) + + + rec_bruta = _sub(root, "receitaBruta") + for rm in sorted(self.receitas_mensais, key=lambda r: r.mes): + mes_el = _sub(rec_bruta, "mes") + _sub(mes_el, "nMes", str(rm.mes)) + _sub(mes_el, "vlrRecBruta", _fmt_dec(rm.receita_bruta_total)) + _sub(mes_el, "vlrRecBrutaExportacao", _fmt_dec(rm.receita_bruta_exportacao)) + _sub(mes_el, "vlrRecBrutaIsenta", _fmt_dec(rm.receita_bruta_isenta)) + + + total = _sub(root, "totalAnual") + _sub(total, "vlrRecBrutaTotal", _fmt_dec(self._total_receita)) + _sub(total, "vlrRecBrutaExportTotal", _fmt_dec(self._total_exportacao)) + _sub(total, "vlrRecBrutaIsentaTotal", _fmt_dec(self._total_isenta)) + + + qs = _sub(root, "quadroSocietario") + for socio in self.socios: + s_el = _sub(qs, "socio") + _sub(s_el, "CPF_CNPJ", socio.cpf_cnpj) + _sub(s_el, "xNome", socio.nome) + _sub(s_el, "pctCapital", _fmt_dec(socio.percentual_capital)) + _sub(s_el, "tpSocio", socio.tipo) + _sub(s_el, "paisDomicilio", socio.pais_domicilio) + + + emp_el = _sub(root, "empregados") + for emp in self.empregados: + comp_el = _sub(emp_el, "competencia") + _sub(comp_el, "perApur", emp.competencia) + _sub(comp_el, "qtdEmpregados", str(emp.quantidade)) + + ET.indent(root, space=" ") + return '\n' + ET.tostring(root, encoding="unicode") + + # ------------------------------------------------------------------ + # Persistência + # ------------------------------------------------------------------ + + def salvar(self, diretorio: str | Path = ".") -> Path: + """Salva como DEFIS_CNPJ_ANO.xml""" + caminho = Path(diretorio) / f"DEFIS_{self.empresa.cnpj}_{self.ano_calendario}.xml" + caminho.parent.mkdir(parents=True, exist_ok=True) + caminho.write_text(self.gerar_xml(), encoding="utf-8") + return caminho + + # ------------------------------------------------------------------ + # Relatório + # ------------------------------------------------------------------ + + def relatorio_resumo(self) -> str: + """Resumo anual: receita total, maior mês, menores meses.""" + linhas = [ + f"DEFIS — {self.empresa.razao_social} — Ano: {self.ano_calendario}", + "=" * 70, + ] + + if not self.receitas_mensais: + linhas.append("Sem receitas informadas.") + return "\n".join(linhas) + + nomes_mes = [ + "", "Jan", "Fev", "Mar", "Abr", "Mai", "Jun", + "Jul", "Ago", "Set", "Out", "Nov", "Dez", + ] + + for rm in sorted(self.receitas_mensais, key=lambda r: r.mes): + nome = nomes_mes[rm.mes] if 1 <= rm.mes <= 12 else str(rm.mes) + linhas.append(f" {nome}: R$ {rm.receita_bruta_total:>12,.2f}") + + maior = max(self.receitas_mensais, key=lambda r: r.receita_bruta_total) + menor = min(self.receitas_mensais, key=lambda r: r.receita_bruta_total) + + linhas += [ + "=" * 70, + f"Receita Total Anual: R$ {self._total_receita:>12,.2f}", + f"Receita Exportação: R$ {self._total_exportacao:>12,.2f}", + f"Receita Isenta: R$ {self._total_isenta:>12,.2f}", + f"Maior mês: {nomes_mes[maior.mes]} — R$ {maior.receita_bruta_total:,.2f}", + f"Menor mês: {nomes_mes[menor.mes]} — R$ {menor.receita_bruta_total:,.2f}", + f"Prazo de entrega: 31/03/{self.ano_calendario + 1}", + f"Retificadora: {'Sim' if self.retificadora else 'Não'}", + ] + return "\n".join(linhas) diff --git a/src/generators/destda.py b/src/generators/destda.py new file mode 100644 index 0000000000000000000000000000000000000000..a64da79ba1f1bac0e8e3619fe75834e56adf300d --- /dev/null +++ b/src/generators/destda.py @@ -0,0 +1,186 @@ + +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import date +from decimal import Decimal +from pathlib import Path +from xml.etree import ElementTree as ET + +from src.fiscal.entities import Empresa + + +COD_UF = { + "AC": "12", "AL": "27", "AM": "13", "AP": "16", "BA": "29", + "CE": "23", "DF": "53", "ES": "32", "GO": "52", "MA": "21", + "MG": "31", "MS": "50", "MT": "51", "PA": "15", "PB": "25", + "PE": "26", "PI": "22", "PR": "41", "RJ": "33", "RN": "24", + "RO": "11", "RR": "14", "RS": "43", "SC": "42", "SE": "28", + "SP": "35", "TO": "17", +} + + +def _sub(pai: ET.Element, tag: str, texto: str = "") -> ET.Element: + el = ET.SubElement(pai, tag) + if texto: + el.text = str(texto) + return el + + +def _fmt(v: Decimal) -> str: + return f"{v:.2f}" + + +@dataclass +class OperacaoSTDeSTDA: + """Operação de ICMS-ST, DIFAL ou antecipação do Simples Nacional.""" + uf_origem: str + uf_destino: str + tipo: str # "ST" | "DIFAL" | "ANTEC" + base_calculo: Decimal + aliquota: Decimal # percentual, ex: Decimal("18") + valor_imposto: Decimal + valor_pago: Decimal = Decimal("0") + + def __post_init__(self): + if self.valor_pago == Decimal("0"): + self.valor_pago = self.valor_imposto + + +class GeradorDeSTDA: + + + VERSAO = "1.1" + + def __init__( + self, + empresa: Empresa, + periodo: str, # "YYYY-MM" + operacoes: list[OperacaoSTDeSTDA], + uf_declarante: str = "SP", + retificadora: bool = False, + ): + self.empresa = empresa + self.periodo = periodo + self.operacoes = operacoes + self.uf_declarante = uf_declarante.upper() + self.retificadora = retificadora + + + def data_vencimento(self) -> date: + ano, mes = map(int, self.periodo.split("-")) + if mes == 12: + return date(ano + 1, 1, 20) + return date(ano, mes + 1, 20) + + @property + def _ops_st(self) -> list[OperacaoSTDeSTDA]: + return [o for o in self.operacoes if o.tipo == "ST"] + + @property + def _ops_difal(self) -> list[OperacaoSTDeSTDA]: + return [o for o in self.operacoes if o.tipo == "DIFAL"] + + @property + def _ops_antec(self) -> list[OperacaoSTDeSTDA]: + return [o for o in self.operacoes if o.tipo == "ANTEC"] + + def _total(self, ops: list[OperacaoSTDeSTDA], campo: str) -> Decimal: + return sum(getattr(o, campo) for o in ops) + + + def gerar_xml(self) -> str: + ano, mes = self.periodo.split("-") + ultimo_dia = self._ultimo_dia(int(ano), int(mes)) + + root = ET.Element("DeSTDA") + root.set("versao", self.VERSAO) + root.set("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") + + + inf = _sub(root, "infDeSTDA") + _sub(inf, "CNPJ", self.empresa.cnpj) + _sub(inf, "cUF", COD_UF.get(self.uf_declarante, "35")) + _sub(inf, "dtInicio", f"{ano}-{mes}-01") + _sub(inf, "dtFim", f"{ano}-{mes}-{ultimo_dia}") + _sub(inf, "indRetificadora", "S" if self.retificadora else "N") + _sub(inf, "dtEnvio", date.today().strftime("%Y-%m-%d")) + + + self._bloco_st(root) + self._bloco_difal(root) + self._bloco_antec(root) + + + tot = _sub(root, "totais") + vst = self._total(self._ops_st, "valor_imposto") + vdifal = self._total(self._ops_difal, "valor_imposto") + vantec = self._total(self._ops_antec, "valor_imposto") + vpago = self._total(self.operacoes, "valor_pago") + _sub(tot, "vST_Total", _fmt(vst)) + _sub(tot, "vDIFAL_Total", _fmt(vdifal)) + _sub(tot, "vANTEC_Total", _fmt(vantec)) + _sub(tot, "vTotal", _fmt(vst + vdifal + vantec)) + _sub(tot, "vPago_Total", _fmt(vpago)) + + ET.indent(root, space=" ") + return '\n' + ET.tostring(root, encoding="unicode") + + def _bloco_st(self, root: ET.Element) -> None: + for op in self._ops_st: + el = _sub(root, "ST") + _sub(el, "UF_ST", op.uf_destino) + _sub(el, "vBC_ST", _fmt(op.base_calculo)) + _sub(el, "pICMS_ST", _fmt(op.aliquota)) + _sub(el, "vICMS_ST", _fmt(op.valor_imposto)) + _sub(el, "vICMS_ST_Pago", _fmt(op.valor_pago)) + + def _bloco_difal(self, root: ET.Element) -> None: + for op in self._ops_difal: + el = _sub(root, "DIFAL") + _sub(el, "UF_DIFAL", op.uf_destino) + _sub(el, "vBC_DIFAL", _fmt(op.base_calculo)) + _sub(el, "pICMS_DIFAL", _fmt(op.aliquota)) + _sub(el, "vICMS_DIFAL", _fmt(op.valor_imposto)) + _sub(el, "vICMS_DIFAL_Pago", _fmt(op.valor_pago)) + + def _bloco_antec(self, root: ET.Element) -> None: + for op in self._ops_antec: + el = _sub(root, "ANTEC") + _sub(el, "UF_ANTEC", op.uf_origem) + _sub(el, "vBC_ANTEC", _fmt(op.base_calculo)) + _sub(el, "pICMS_ANTEC", _fmt(op.aliquota)) + _sub(el, "vICMS_ANTEC", _fmt(op.valor_imposto)) + _sub(el, "vICMS_ANTEC_Pago", _fmt(op.valor_pago)) + + def salvar(self, diretorio: str | Path = ".") -> Path: + caminho = Path(diretorio) / f"DeSTDA_{self.empresa.cnpj}_{self.periodo}.xml" + caminho.parent.mkdir(parents=True, exist_ok=True) + caminho.write_text(self.gerar_xml(), encoding="utf-8") + return caminho + + def relatorio_resumo(self) -> str: + vst = self._total(self._ops_st, "valor_imposto") + vdifal = self._total(self._ops_difal, "valor_imposto") + vantec = self._total(self._ops_antec, "valor_imposto") + total = vst + vdifal + vantec + venc = self.data_vencimento() + linhas = [ + f"DeSTDA — {self.empresa.razao_social} — Período: {self.periodo}", + "=" * 60, + f"ICMS-ST: R$ {vst:>12,.2f} ({len(self._ops_st)} operação/ões)", + f"DIFAL: R$ {vdifal:>12,.2f} ({len(self._ops_difal)} operação/ões)", + f"Antecipação: R$ {vantec:>12,.2f} ({len(self._ops_antec)} operação/ões)", + "=" * 60, + f"TOTAL A PAGAR: R$ {total:>12,.2f}", + f"Vencimento: {venc.strftime('%d/%m/%Y')}", + ] + return "\n".join(linhas) + + @staticmethod + def _ultimo_dia(ano: int, mes: int) -> str: + if mes == 12: + return "31" + from datetime import date as d + dias = (d(ano, mes + 1, 1) - d(ano, mes, 1)).days + return str(dias).zfill(2) diff --git a/src/generators/dirf.py b/src/generators/dirf.py new file mode 100644 index 0000000000000000000000000000000000000000..53d830df15e10f718728db0b5969c726e49b6807 --- /dev/null +++ b/src/generators/dirf.py @@ -0,0 +1,186 @@ + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from datetime import date +from decimal import Decimal +from pathlib import Path +from typing import Optional + + +# --------------------------------------------------------------------------- +# Dataclasses +# --------------------------------------------------------------------------- + +@dataclass +class BeneficiarioDIRF: + """Beneficiário de rendimentos sujeitos à retenção na fonte.""" + cpf_cnpj: str + nome: str + tipo: str # "PF" ou "PJ" + cod_receita: str # ex: "0561" (trabalho assalariado), "1708" (serviços PJ) + rendimentos_por_mes: dict[int, Decimal] = field(default_factory=dict) # {mes(1-12): valor} + ir_retido_por_mes: dict[int, Decimal] = field(default_factory=dict) # {mes(1-12): valor} + + data_admissao: Optional[date] = None # para assalariados + data_demissao: Optional[date] = None + natureza: str = "A" # "A"=assalariado, "B"=autônomo, "C"=outros + + +@dataclass +class ResponsavelDIRF: + cpf: str + nome: str + cargo: str + ddd: str + telefone: str + + +# --------------------------------------------------------------------------- +# Gerador +# --------------------------------------------------------------------------- + +class GeradorDIRF: + """ + Gera o arquivo DIRF no formato texto para importação no PGD DIRF. + + Periodicidade: anual. + Prazo: último dia útil de fevereiro do ano seguinte. + Obrigação: toda PJ que pagou rendimentos sujeitos ao IRRF. + """ + + def __init__( + self, + empresa, # Empresa (src.fiscal.entities.Empresa) + ano_calendario: int, + beneficiarios: list[BeneficiarioDIRF], + responsavel: ResponsavelDIRF, + retificadora: bool = False, + numero_recibo_retificada: str = "", + ): + self.empresa = empresa + self.ano_calendario = ano_calendario + self.beneficiarios = beneficiarios + self.responsavel = responsavel + self.retificadora = retificadora + self.numero_recibo_retificada = numero_recibo_retificada + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + @staticmethod + def _apenas_digitos(s: str) -> str: + return re.sub(r"\D", "", s) + + def _linha_cabecalho(self) -> str: + """Linha DIRF — cabeçalho da declaração.""" + ano_exercicio = self.ano_calendario + 1 + ret = "S" if self.retificadora else "N" + partes = ["DIRF", str(ano_exercicio), ret] + if self.retificadora and self.numero_recibo_retificada: + partes.append(self.numero_recibo_retificada) + return " ".join(partes) + + def _linha_respo(self) -> str: + """Linha RESPO — identificação do responsável pela declaração.""" + r = self.responsavel + cpf = self._apenas_digitos(r.cpf).zfill(11) + ddd = self._apenas_digitos(r.ddd) + tel = self._apenas_digitos(r.telefone) + return f"RESPO {cpf} {r.nome} {r.cargo} {ddd}{tel}" + + def _linha_decpj(self) -> str: + """Linha DECPJ — identificação da empresa declarante.""" + emp = self.empresa + cnpj = self._apenas_digitos(emp.cnpj).zfill(14) + return f"DECPJ {cnpj} {emp.razao_social} 2062 N" + + def _blocos_beneficiarios(self) -> list[str]: + """Gera os blocos de beneficiários agrupados por código de receita.""" + linhas: list[str] = [] + + + por_receita: dict[str, list[BeneficiarioDIRF]] = {} + for ben in self.beneficiarios: + por_receita.setdefault(ben.cod_receita, []).append(ben) + + for cod_receita, grupo in por_receita.items(): + linhas.append(f"IDREC {cod_receita}") + for ben in grupo: + doc = self._apenas_digitos(ben.cpf_cnpj) + if ben.tipo == "PF": + doc = doc.zfill(11) + linhas.append(f"BPFDEC {doc} {ben.nome} {cod_receita}") + else: + doc = doc.zfill(14) + linhas.append(f"BPJDEC {doc} {ben.nome} {cod_receita}") + + + meses = sorted( + set(ben.rendimentos_por_mes.keys()) | set(ben.ir_retido_por_mes.keys()) + ) + for mes in meses: + rend = ben.rendimentos_por_mes.get(mes, Decimal("0")) + ir = ben.ir_retido_por_mes.get(mes, Decimal("0")) + linhas.append( + f"{self.ano_calendario} {mes:02d} {rend:.2f} {ir:.2f}" + ) + + return linhas + + # ------------------------------------------------------------------ + # Interface pública + # ------------------------------------------------------------------ + + def gerar_txt(self) -> str: + """Gera o arquivo texto no layout PGD DIRF.""" + linhas: list[str] = [] + linhas.append(self._linha_cabecalho()) + linhas.append(self._linha_respo()) + linhas.append(self._linha_decpj()) + linhas.extend(self._blocos_beneficiarios()) + linhas.append("FIMDIRF") + return "\n".join(linhas) + "\n" + + def salvar(self, diretorio: str | Path = ".") -> Path: + """Salva o arquivo como DIRF_CNPJ_ANO.txt.""" + cnpj = self._apenas_digitos(self.empresa.cnpj).zfill(14) + caminho = Path(diretorio) / f"DIRF_{cnpj}_{self.ano_calendario}.txt" + caminho.parent.mkdir(parents=True, exist_ok=True) + caminho.write_text(self.gerar_txt(), encoding="utf-8") + return caminho + + def relatorio_resumo(self) -> str: + """Retorna totais de rendimentos e IR retido agrupados por código de receita.""" + totais: dict[str, dict[str, Decimal]] = {} + for ben in self.beneficiarios: + cod = ben.cod_receita + if cod not in totais: + totais[cod] = {"rendimentos": Decimal("0"), "ir_retido": Decimal("0"), "beneficiarios": 0} + totais[cod]["rendimentos"] += sum(ben.rendimentos_por_mes.values()) + totais[cod]["ir_retido"] += sum(ben.ir_retido_por_mes.values()) + totais[cod]["beneficiarios"] += 1 # type: ignore[operator] + + emp = self.empresa + linhas = [ + f"DIRF {self.ano_calendario} — {emp.razao_social} — CNPJ: {emp.cnpj}", + "=" * 70, + ] + total_rend = Decimal("0") + total_ir = Decimal("0") + for cod, dados in sorted(totais.items()): + linhas.append(f"Cód. Receita: {cod}") + linhas.append(f" Beneficiários: {dados['beneficiarios']:>6}") + linhas.append(f" Rendimentos: R$ {dados['rendimentos']:>12,.2f}") + linhas.append(f" IR Retido: R$ {dados['ir_retido']:>12,.2f}") + total_rend += dados["rendimentos"] + total_ir += dados["ir_retido"] + + linhas += [ + "=" * 70, + f"TOTAL RENDIMENTOS: R$ {total_rend:>12,.2f}", + f"TOTAL IR RETIDO: R$ {total_ir:>12,.2f}", + ] + return "\n".join(linhas) diff --git a/src/generators/ecd.py b/src/generators/ecd.py new file mode 100644 index 0000000000000000000000000000000000000000..87de100a166a65067052b0348e7f42aa96238698 --- /dev/null +++ b/src/generators/ecd.py @@ -0,0 +1,294 @@ + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import date +from decimal import Decimal +from pathlib import Path +from typing import Optional + +from src.fiscal.entities import Empresa, LancamentoContabil, PeriodoApuracao +from src.generators.sped_writer import ArquivoSPED, criar_registro + +VERSAO_LEIAUTE = "010" + + +@dataclass +class PlanoContas: + codigo: str + descricao: str + nivel: int + tipo: str # A=Analítica, S=Sintética + natureza: str # D=Devedora, C=Credora + + +@dataclass +class ConfigECD: + versao: str = VERSAO_LEIAUTE + finalidade: str = "G" # G=Livro Diário, R=Razão Auxiliar + hash_anterior: str = "" + tipo_escrituracao: str = "G" # G=Geral + indicador_sit_especial: str = "" + + +class GeradorECD: + + + def __init__( + self, + periodo: PeriodoApuracao, + plano_contas: Optional[list[PlanoContas]] = None, + cfg: Optional[ConfigECD] = None, + contador: Optional[object] = None, + ): + self.periodo = periodo + self.plano_contas = plano_contas or self._plano_contas_padrao() + self.cfg = cfg or ConfigECD() + self.contador = contador + self._arquivo = ArquivoSPED( + f"ECD_{periodo.empresa.cnpj}_{periodo.data_inicio.year}.txt" + ) + + def _plano_contas_padrao(self) -> list[PlanoContas]: + return [ + PlanoContas("1", "ATIVO", 1, "S", "D"), + PlanoContas("1.1", "ATIVO CIRCULANTE", 2, "S", "D"), + PlanoContas("1.1.1", "CAIXA E EQUIVALENTES", 3, "S", "D"), + PlanoContas("1.1.1.01", "CAIXA", 4, "A", "D"), + PlanoContas("1.1.1.02", "BANCOS CONTA MOVIMENTO", 4, "A", "D"), + PlanoContas("1.1.2", "CONTAS A RECEBER", 3, "S", "D"), + PlanoContas("1.1.2.01", "CLIENTES", 4, "A", "D"), + PlanoContas("1.1.3", "ESTOQUES", 3, "S", "D"), + PlanoContas("1.1.3.01", "MERCADORIAS", 4, "A", "D"), + PlanoContas("1.2", "ATIVO NÃO CIRCULANTE", 2, "S", "D"), + PlanoContas("1.2.1", "IMOBILIZADO", 3, "S", "D"), + PlanoContas("1.2.1.01", "MÓVEIS E UTENSÍLIOS", 4, "A", "D"), + PlanoContas("1.2.1.02", "EQUIPAMENTOS", 4, "A", "D"), + PlanoContas("2", "PASSIVO", 1, "S", "C"), + PlanoContas("2.1", "PASSIVO CIRCULANTE", 2, "S", "C"), + PlanoContas("2.1.1", "FORNECEDORES", 3, "S", "C"), + PlanoContas("2.1.1.01", "FORNECEDORES NACIONAIS", 4, "A", "C"), + PlanoContas("2.1.2", "OBRIGAÇÕES FISCAIS", 3, "S", "C"), + PlanoContas("2.1.2.01", "ICMS A RECOLHER", 4, "A", "C"), + PlanoContas("2.1.2.02", "PIS A RECOLHER", 4, "A", "C"), + PlanoContas("2.1.2.03", "COFINS A RECOLHER", 4, "A", "C"), + PlanoContas("2.1.2.04", "IRPJ A RECOLHER", 4, "A", "C"), + PlanoContas("2.1.2.05", "CSLL A RECOLHER", 4, "A", "C"), + PlanoContas("2.1.3", "OBRIGAÇÕES TRABALHISTAS", 3, "S", "C"), + PlanoContas("2.1.3.01", "SALÁRIOS A PAGAR", 4, "A", "C"), + PlanoContas("2.2", "PASSIVO NÃO CIRCULANTE", 2, "S", "C"), + PlanoContas("3", "PATRIMÔNIO LÍQUIDO", 1, "S", "C"), + PlanoContas("3.1", "CAPITAL SOCIAL", 2, "S", "C"), + PlanoContas("3.1.01", "CAPITAL SUBSCRITO E INTEGRALIZADO", 3, "A", "C"), + PlanoContas("3.2", "RESERVAS E LUCROS", 2, "S", "C"), + PlanoContas("3.2.01", "LUCROS ACUMULADOS", 3, "A", "C"), + PlanoContas("4", "RECEITAS", 1, "S", "C"), + PlanoContas("4.1", "RECEITA OPERACIONAL BRUTA", 2, "S", "C"), + PlanoContas("4.1.01", "VENDAS DE MERCADORIAS", 3, "A", "C"), + PlanoContas("4.1.02", "PRESTAÇÃO DE SERVIÇOS", 3, "A", "C"), + PlanoContas("4.2", "DEDUÇÕES DA RECEITA", 2, "S", "D"), + PlanoContas("4.2.01", "DEVOLUÇÕES DE VENDAS", 3, "A", "D"), + PlanoContas("4.2.02", "IMPOSTOS SOBRE VENDAS", 3, "A", "D"), + PlanoContas("5", "CUSTOS", 1, "S", "D"), + PlanoContas("5.1", "CUSTO DE MERCADORIAS VENDIDAS", 2, "S", "D"), + PlanoContas("5.1.01", "CMV - MERCADORIAS", 3, "A", "D"), + PlanoContas("6", "DESPESAS OPERACIONAIS", 1, "S", "D"), + PlanoContas("6.1", "DESPESAS ADMINISTRATIVAS", 2, "S", "D"), + PlanoContas("6.1.01", "SALÁRIOS E ORDENADOS", 3, "A", "D"), + PlanoContas("6.1.02", "ENCARGOS SOCIAIS", 3, "A", "D"), + PlanoContas("6.1.03", "ALUGUÉIS", 3, "A", "D"), + PlanoContas("6.1.04", "SERVIÇOS DE TERCEIROS", 3, "A", "D"), + PlanoContas("6.2", "DESPESAS COMERCIAIS", 2, "S", "D"), + PlanoContas("6.2.01", "FRETES E CARRETOS", 3, "A", "D"), + PlanoContas("6.2.02", "COMISSÕES", 3, "A", "D"), + PlanoContas("6.3", "DESPESAS FINANCEIRAS", 2, "S", "D"), + PlanoContas("6.3.01", "JUROS E ENCARGOS", 3, "A", "D"), + ] + + def _bloco_0(self) -> None: + emp = self.periodo.empresa + end = emp.endereco + + self._arquivo.adicionar_linha_raw(criar_registro( + "0000", + self.cfg.versao, + self.cfg.tipo_escrituracao, + "G", # ind_sit_ini_pj (G=Geral) + emp.cnpj, + emp.regime_tributario.value, + emp.razao_social, + end.uf.value, + emp.ie, + end.cod_municipio, + self.cfg.indicador_sit_especial, + "", # nr_rec_anterior + self.periodo.data_inicio, + self.periodo.data_fim, + "N", # ind_grandes_contribuintes + self.cfg.finalidade, + "1", # ind_pub_soc_emp + "", # ind_ecd_complem + "", # hash_ecd_substituto + )) + + self._arquivo.adicionar_linha_raw(criar_registro("0001", "0")) + + # 0007 - Identificação dos Signatários da ECD + if self.contador: + c = self.contador + self._arquivo.adicionar_linha_raw(criar_registro( + "0007", + c.cpf, + "2", # qualif_assig (2=Contabilista) + )) + + self._arquivo.adicionar_linha_raw(criar_registro( + "0990", self._arquivo.total_linhas() + 1 + )) + + def _bloco_i(self) -> None: + + self._arquivo.adicionar_linha_raw(criar_registro("I001", "0")) + + + self._arquivo.adicionar_linha_raw(criar_registro( + "I010", + "G", # ind_lc (G=Livro Diário) + "", # nr_livro + "", # nr_pag_ini + "", # qt_lin + "", # nr_pag_fin + "", # nome_livro + self.periodo.data_inicio, + self.periodo.data_fim, + "", # hash_lc + "", # nr_pag_ini_cert + "", # nr_pag_fin_cert + "1", # dt_cert + "1", # nm_cart + "1", # nr_off_cart + "1", # tp_assinante + "1", # tratamento_diff + )) + + + self._arquivo.adicionar_linha_raw(criar_registro( + "I020", + "EMPRESA", + self.periodo.empresa.cnpj, + "1", # cod_ccp + "1", # cpf + )) + + + for conta in self.plano_contas: + self._arquivo.adicionar_linha_raw(criar_registro( + "I050", + conta.codigo, + conta.codigo, # cod_cta_ref + conta.nivel, + conta.tipo, + conta.natureza, + conta.descricao, + "", # cod_cta_cnt_sup + "", # cod_cta_cnt_cpl + "", # tipo_cta_ref + )) + + + for conta in [c for c in self.plano_contas if c.tipo == "A"]: + self._arquivo.adicionar_linha_raw(criar_registro( + "I100", + conta.codigo, + "0", # ind_dc_ini + Decimal("0"), # vl_saldo_ini + Decimal("0"), # vl_deb + Decimal("0"), # vl_cred + "0", # ind_dc_fin + Decimal("0"), # vl_saldo_fin + )) + + + for i, lanc in enumerate(self.periodo.lancamentos, 1): + self._arquivo.adicionar_linha_raw(criar_registro( + "I200", + str(i).zfill(6), # num_lanc + lanc.data, + "01", # cod_hist_padrao + lanc.historico, + Decimal("0"), # vl_lanc (total do lançamento) + )) + + + self._arquivo.adicionar_linha_raw(criar_registro( + "I250", + lanc.debito_conta, + "D", + lanc.valor, + "01", # cod_hist_padrao + lanc.historico, + "", # num_doc + "", # cod_part + )) + self._arquivo.adicionar_linha_raw(criar_registro( + "I250", + lanc.credito_conta, + "C", + lanc.valor, + "01", + lanc.historico, + "", + "", + )) + + self._arquivo.adicionar_linha_raw(criar_registro( + "I990", self._arquivo.total_linhas() + )) + + def _bloco_j(self) -> None: + + self._arquivo.adicionar_linha_raw(criar_registro("J001", "0")) + + + self._arquivo.adicionar_linha_raw(criar_registro( + "J005", + self.periodo.data_fim, + "0", # cod_qualif_cp (0=não referenciado) + "0", # cod_pl_ref + "0", # cod_ver_pl_ref + )) + + + for conta in self.plano_contas[:10]: # Primeiras contas como exemplo + self._arquivo.adicionar_linha_raw(criar_registro( + "J100", + conta.codigo, + conta.codigo, + conta.descricao, + conta.nivel, + conta.tipo, + conta.natureza, + Decimal("0"), # vl_saldo_ini + "0", # ind_vl_cta + Decimal("0"), # vl_saldo_fin + "0", + "", # cod_cta_ref_conv + )) + + self._arquivo.adicionar_linha_raw(criar_registro( + "J990", self._arquivo.total_linhas() + )) + + def _bloco_9(self) -> None: + self._arquivo.adicionar_linha_raw(criar_registro("9001", "0")) + qt = self._arquivo.total_linhas() + 3 + self._arquivo.adicionar_linha_raw(criar_registro("9900", "0000", "1")) + self._arquivo.adicionar_linha_raw(criar_registro("9990", qt)) + self._arquivo.adicionar_linha_raw(criar_registro("9999", qt + 1)) + + def gerar(self, diretorio: str | Path = ".") -> Path: + self._bloco_0() + self._bloco_i() + self._bloco_j() + self._bloco_9() + return self._arquivo.salvar(diretorio) diff --git a/src/generators/ecf.py b/src/generators/ecf.py new file mode 100644 index 0000000000000000000000000000000000000000..b6ec09665d24036c347dde8a8b192cb257a3536f --- /dev/null +++ b/src/generators/ecf.py @@ -0,0 +1,474 @@ + +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import date +from decimal import Decimal +from pathlib import Path +from typing import Optional + +from src.calculators.irpj_csll import ( + ResultadoCSLL, + ResultadoIRPJ, + calcular_csll_lucro_presumido, + calcular_csll_lucro_real, + calcular_irpj_lucro_presumido, + calcular_irpj_lucro_real, +) +from src.fiscal.entities import Empresa, PeriodoApuracao, RegimeTributario +from src.generators.sped_writer import ArquivoSPED, criar_registro + +VERSAO_LEIAUTE = "009" + + +@dataclass +class DadosLucroReal: + lucro_contabil: Decimal + adicoes: Decimal = Decimal("0") + exclusoes: Decimal = Decimal("0") + compensacoes_prejuizo: Decimal = Decimal("0") + incentivos_fiscais: Decimal = Decimal("0") + csll_indedutivel: Decimal = Decimal("0") + multas_indedutíveis: Decimal = Decimal("0") + provisoes_indedutíveis: Decimal = Decimal("0") + + +@dataclass +class DadosLucroPresumido: + receita_venda_mercadorias: Decimal = Decimal("0") + receita_prestacao_servicos: Decimal = Decimal("0") + receita_outras_atividades: Decimal = Decimal("0") + ganhos_capital: Decimal = Decimal("0") + rendimentos_aplicacoes: Decimal = Decimal("0") + outras_receitas: Decimal = Decimal("0") + atividade_principal: str = "venda_mercadorias" + + +@dataclass +class ConfigECF: + versao: str = VERSAO_LEIAUTE + finalidade: str = "0" # 0=Original, 1=Retificadora + situacao_especial: str = "" + ano_referencia: int = field(default_factory=lambda: date.today().year - 1) + + +class GeradorECF: + """ + Gera o arquivo ECF conforme leiaute da Receita Federal. + Obrigação anual: Lucro Real, Lucro Presumido e Lucro Arbitrado. + Prazo: último dia útil de julho do ano seguinte. + """ + + def __init__( + self, + empresa: Empresa, + ano: int, + dados_lr: Optional[DadosLucroReal] = None, + dados_lp: Optional[DadosLucroPresumido] = None, + cfg: Optional[ConfigECF] = None, + contador: Optional[object] = None, + ): + self.empresa = empresa + self.ano = ano + self.dados_lr = dados_lr + self.dados_lp = dados_lp + self.cfg = cfg or ConfigECF(ano_referencia=ano) + self.contador = contador + self._arquivo = ArquivoSPED(f"ECF_{empresa.cnpj}_{ano}.txt") + + def _bloco_0(self) -> None: + emp = self.empresa + end = emp.endereco + + self._arquivo.adicionar_linha_raw(criar_registro( + "0000", + self.cfg.versao, + self.cfg.finalidade, + emp.cnpj, + emp.regime_tributario.value, + emp.razao_social, + end.uf.value, + emp.ie, + end.cod_municipio, + self.cfg.situacao_especial, + "", # nr_rec_anterior (retificadora) + date(self.ano, 1, 1), + date(self.ano, 12, 31), + "N", # ind_grandes_contribuintes + self.cfg.finalidade, + "1", # ind_pub_soc_emp + )) + + self._arquivo.adicionar_linha_raw(criar_registro("0001", "0")) + + # 0010 - Parâmetros da Tributação + ind_sit_ini = "0" # 0=Regular + if emp.regime_tributario == RegimeTributario.LUCRO_REAL: + forma_trib = "3" # 3=Lucro Real Anual com Estimativas + elif emp.regime_tributario == RegimeTributario.LUCRO_PRESUMIDO: + forma_trib = "6" # 6=Lucro Presumido + else: + forma_trib = "6" + + self._arquivo.adicionar_linha_raw(criar_registro( + "0010", + ind_sit_ini, + forma_trib, + "0", # ind_calculo_csll + "N", # ind_part_consol + "N", # ind_part_spe + "N", # ind_rep_eco + "N", # ind_ativ_imob + "0", # ind_reidi + "N", # ind_outras_deducoes + "N", # ind_prev_privada + "N", # ind_pat + "N", # ind_simples_concomitante + )) + + # 0020 - Parâmetros complementares + self._arquivo.adicionar_linha_raw(criar_registro( + "0020", + "0", # ind_calc_pl_pj + "0", # ind_opc_irpj_ativ_rural + "0", # ind_cnpj_prep + "0", # ind_lucro_infl + "0", # ind_ativ_preponderante + "0", # ind_sat + "1", # ind_tipo_entidade + )) + + if self.contador: + c = self.contador + self._arquivo.adicionar_linha_raw(criar_registro( + "0930", + c.nome, + c.cpf, + c.crc, + c.cnpj_escritorio if hasattr(c, "cnpj_escritorio") else "", + c.contato.email if hasattr(c, "contato") else "", + c.contato.telefone if hasattr(c, "contato") else "", + )) + + self._arquivo.adicionar_linha_raw(criar_registro( + "0990", self._arquivo.total_linhas() + 1 + )) + + def _bloco_c(self) -> None: + """Bloco C — Identificação e Caracterização da PJ.""" + self._arquivo.adicionar_linha_raw(criar_registro("C001", "0")) + + emp = self.empresa + end = emp.endereco + + # C010 - Identificação + self._arquivo.adicionar_linha_raw(criar_registro( + "C010", + emp.cnpj, + emp.razao_social, + emp.nome_fantasia or emp.razao_social, + end.logradouro, + end.numero, + end.complemento, + end.bairro, + end.cep, + end.municipio, + end.uf.value, + emp.contato.telefone, + emp.contato.email, + emp.ie, + end.cod_municipio, + )) + + # C050 - Atividade Econômica + self._arquivo.adicionar_linha_raw(criar_registro( + "C050", + "6201-5/00", # CNAE principal (exemplo: desenvolvimento software) + "1", # ind_ativ_principal + date(self.ano, 1, 1), + date(self.ano, 12, 31), + )) + + self._arquivo.adicionar_linha_raw(criar_registro( + "C990", self._arquivo.total_linhas() + )) + + def _bloco_e(self) -> None: + """Bloco E — IRPJ e CSLL por tipo de receita.""" + self._arquivo.adicionar_linha_raw(criar_registro("E001", "0")) + + # E010 - Receitas por atividade + receita_total = Decimal("0") + if self.dados_lp: + receita_total = ( + self.dados_lp.receita_venda_mercadorias + + self.dados_lp.receita_prestacao_servicos + + self.dados_lp.receita_outras_atividades + ) + elif self.dados_lr: + pass # No Lucro Real, a receita vem da contabilidade + + self._arquivo.adicionar_linha_raw(criar_registro( + "E010", + receita_total, # vl_rec_brt_ativ + Decimal("0"), # vl_rec_brt_ativ_nao_fin + Decimal("0"), # vl_dev_trib + Decimal("0"), # vl_dev_nao_trib + Decimal("0"), # vl_descto_trib + Decimal("0"), # vl_descto_nao_trib + receita_total, # vl_rec_liq_ativ + Decimal("0"), # vl_rec_liq_ativ_nao_fin + )) + + self._arquivo.adicionar_linha_raw(criar_registro( + "E990", self._arquivo.total_linhas() + )) + + def _bloco_l(self, irpj: ResultadoIRPJ, csll: ResultadoCSLL) -> None: + """Bloco L — Lucro Real (LALUR/LACS).""" + self._arquivo.adicionar_linha_raw(criar_registro("L001", "0")) + + if not self.dados_lr: + self._arquivo.adicionar_linha_raw(criar_registro("L990", "2")) + return + + d = self.dados_lr + + # L010 - Identificação do Período + self._arquivo.adicionar_linha_raw(criar_registro( + "L010", + date(self.ano, 1, 1), + date(self.ano, 12, 31), + "0", # ind_periodo + )) + + # L020 - IRPJ — Lucro Real (LALUR) + self._arquivo.adicionar_linha_raw(criar_registro( + "L020", + d.lucro_contabil, # VL_REC_LIQ + d.lucro_contabil, # VL_LUC_LIQ_ANT_CSLL + d.csll_indedutivel, # VL_CSLL_IND + d.lucro_contabil + d.csll_indedutivel, # VL_LUC_LIQ + d.adicoes, # VL_TOTAL_ADICOES + d.exclusoes, # VL_TOTAL_EXCLUSOES + irpj.lucro_real_ajustado, # VL_LUC_REAL_ANT_COMP + irpj.lucro_real_ajustado - irpj.base_calculo, # VL_COMP_PREJ_PERIODO + irpj.base_calculo, # VL_LUC_REAL + )) + + # L030 - CSLL — Lucro Real (LACS) + self._arquivo.adicionar_linha_raw(criar_registro( + "L030", + d.lucro_contabil, + d.adicoes, + d.exclusoes, + csll.base_calculo, + csll.base_calculo, + csll.base_calculo, + )) + + # L100 - Adições ao Lucro Líquido + if d.multas_indedutíveis > 0: + self._arquivo.adicionar_linha_raw(criar_registro( + "L100", "054", "Multas Indedutíveis", d.multas_indedutíveis + )) + if d.provisoes_indedutíveis > 0: + self._arquivo.adicionar_linha_raw(criar_registro( + "L100", "012", "Provisões Indedutíveis", d.provisoes_indedutíveis + )) + + self._arquivo.adicionar_linha_raw(criar_registro( + "L990", self._arquivo.total_linhas() + )) + + def _bloco_n(self, irpj: ResultadoIRPJ, csll: ResultadoCSLL) -> None: + """Bloco N — Cálculo do IRPJ e da CSLL.""" + self._arquivo.adicionar_linha_raw(criar_registro("N001", "0")) + + # N010 — IRPJ + self._arquivo.adicionar_linha_raw(criar_registro( + "N010", + date(self.ano, 1, 1), + date(self.ano, 12, 31), + )) + + + self._arquivo.adicionar_linha_raw(criar_registro( + "N620", + irpj.base_calculo, # VL_BC_IRPJ + irpj.valor_base, # VL_IRPJ_ALIQ_15 + irpj.base_adicional, # VL_BC_ADIC_IRPJ + irpj.valor_adicional, # VL_ADIC_IRPJ + irpj.valor_irpj_total, # VL_IRPJ_DEVIDO + irpj.incentivos_fiscais, # VL_IRPJ_INCENTIVOS + Decimal("0"), # VL_IRPJ_ESTIM_SUSP + irpj.irpj_a_recolher, # VL_IRPJ_A_REC + Decimal("0"), # VL_IRPJ_A_COMP + Decimal("0"), # VL_IRPJ_A_REC_FINAL + )) + + + self._arquivo.adicionar_linha_raw(criar_registro( + "N630", + csll.base_calculo, # VL_BC_CSLL + csll.aliquota, # ALIQ_CSLL + csll.valor_csll, # VL_CSLL_DEVIDA + Decimal("0"), # VL_CSLL_ESTIM_SUSP + csll.csll_a_recolher, # VL_CSLL_A_REC + Decimal("0"), # VL_CSLL_A_COMP + csll.csll_a_recolher, # VL_CSLL_A_REC_FINAL + )) + + self._arquivo.adicionar_linha_raw(criar_registro( + "N990", self._arquivo.total_linhas() + )) + + def _bloco_p(self, irpj: ResultadoIRPJ, csll: ResultadoCSLL) -> None: + """Bloco P — Lucro Presumido.""" + self._arquivo.adicionar_linha_raw(criar_registro("P001", "0")) + + if not self.dados_lp: + self._arquivo.adicionar_linha_raw(criar_registro("P990", "2")) + return + + d = self.dados_lp + # Cada trimestre do Lucro Presumido + for trim, (di, df) in enumerate([ + (date(self.ano, 1, 1), date(self.ano, 3, 31)), + (date(self.ano, 4, 1), date(self.ano, 6, 30)), + (date(self.ano, 7, 1), date(self.ano, 9, 30)), + (date(self.ano, 10, 1), date(self.ano, 12, 31)), + ], 1): + # P010 - Período + self._arquivo.adicionar_linha_raw(criar_registro("P010", di, df)) + + + fator = Decimal("0.25") + rec_merc = d.receita_venda_mercadorias * fator + rec_serv = d.receita_prestacao_servicos * fator + rec_outras = d.receita_outras_atividades * fator + + + self._arquivo.adicionar_linha_raw(criar_registro( + "P020", + rec_merc, # receita mercadorias (8%) + rec_serv, # receita serviços (32%) + rec_outras, # outras receitas (8%) + d.ganhos_capital * fator, # ganhos de capital + d.rendimentos_aplicacoes * fator, # rendimentos de aplicações + d.outras_receitas * fator, # outras receitas financeiras + )) + + + lp_merc = rec_merc * Decimal("0.08") + lp_serv = rec_serv * Decimal("0.32") + lp_outras = rec_outras * Decimal("0.08") + lp_total = lp_merc + lp_serv + lp_outras + d.ganhos_capital * fator + d.rendimentos_aplicacoes * fator + + self._arquivo.adicionar_linha_raw(criar_registro( + "P030", + lp_merc, # vl_rec_liq_ativ_3 + lp_serv, # vl_rec_liq_ativ_32 + lp_outras, # vl_rec_liq_ativ_8 + lp_total, # vl_lp_irpj + lp_total, # vl_bc_irpj + )) + + + lp_csll_merc = rec_merc * Decimal("0.12") + lp_csll_serv = rec_serv * Decimal("0.32") + lp_csll_total = lp_csll_merc + lp_csll_serv + d.ganhos_capital * fator + + self._arquivo.adicionar_linha_raw(criar_registro( + "P100", + lp_csll_merc, + lp_csll_serv, + lp_csll_total, + lp_csll_total, + )) + + self._arquivo.adicionar_linha_raw(criar_registro( + "P990", self._arquivo.total_linhas() + )) + + def _bloco_y(self) -> None: + """Bloco Y — Informações Gerais.""" + self._arquivo.adicionar_linha_raw(criar_registro("Y001", "0")) + + # Y520 — Bens e Direitos no Exterior (simplificado) + # Y540 — Dívidas no Exterior + # Y570 — Pagamentos/Remessas ao Exterior + # Y600 — Sócios/Titulares da PJ + self._arquivo.adicionar_linha_raw(criar_registro( + "Y600", + self.empresa.cnpj, + "", # CPF (quando PF) + "SOCIO", # nome + Decimal("100"), # percentual participação + date(self.ano, 1, 1), + date(self.ano, 12, 31), + )) + + self._arquivo.adicionar_linha_raw(criar_registro( + "Y990", self._arquivo.total_linhas() + )) + + def _bloco_9(self) -> None: + self._arquivo.adicionar_linha_raw(criar_registro("9001", "0")) + qt = self._arquivo.total_linhas() + 3 + self._arquivo.adicionar_linha_raw(criar_registro("9900", "0000", "1")) + self._arquivo.adicionar_linha_raw(criar_registro("9990", qt)) + self._arquivo.adicionar_linha_raw(criar_registro("9999", qt + 1)) + + def gerar(self, diretorio: str | Path = ".") -> Path: + emp = self.empresa + dados_lr = self.dados_lr + dados_lp = self.dados_lp + + if emp.regime_tributario == RegimeTributario.LUCRO_REAL and dados_lr: + irpj = calcular_irpj_lucro_real( + lucro_antes_ir=dados_lr.lucro_contabil, + adicoes=dados_lr.adicoes, + exclusoes=dados_lr.exclusoes, + compensacoes_prejuizo=dados_lr.compensacoes_prejuizo, + incentivos_fiscais=dados_lr.incentivos_fiscais, + ) + csll = calcular_csll_lucro_real( + lucro_antes_csll=dados_lr.lucro_contabil, + adicoes=dados_lr.adicoes, + exclusoes=dados_lr.exclusoes, + ) + else: + d = dados_lp or DadosLucroPresumido() + receita = ( + d.receita_venda_mercadorias + + d.receita_prestacao_servicos + + d.receita_outras_atividades + ) + irpj = calcular_irpj_lucro_presumido( + receita_bruta=receita, + outras_receitas=d.ganhos_capital + d.rendimentos_aplicacoes + d.outras_receitas, + atividade=d.atividade_principal, + ) + csll = calcular_csll_lucro_presumido( + receita_bruta=receita, + outras_receitas=d.ganhos_capital + d.rendimentos_aplicacoes + d.outras_receitas, + atividade="comercio_industria" if d.atividade_principal == "venda_mercadorias" else "servicos_em_geral", + ) + + self._bloco_0() + self._bloco_c() + self._bloco_e() + + if emp.regime_tributario == RegimeTributario.LUCRO_REAL: + self._bloco_l(irpj, csll) + else: + self._arquivo.adicionar_linha_raw(criar_registro("L001", "1")) + self._arquivo.adicionar_linha_raw(criar_registro("L990", "2")) + self._bloco_p(irpj, csll) + + self._bloco_n(irpj, csll) + self._bloco_y() + self._bloco_9() + + return self._arquivo.salvar(diretorio) diff --git a/src/generators/efd_contribuicoes.py b/src/generators/efd_contribuicoes.py new file mode 100644 index 0000000000000000000000000000000000000000..936354eddd9d6b97946ae2abecd928347f72ea54 --- /dev/null +++ b/src/generators/efd_contribuicoes.py @@ -0,0 +1,253 @@ + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import date +from decimal import Decimal +from pathlib import Path +from typing import Optional + +from src.calculators.pis_cofins import ( + CreditoPisCofins, + ReceitaBruta, + ResultadoPisCofins, + calcular_pis_cofins, +) +from src.fiscal.entities import Empresa, NotaFiscal, PeriodoApuracao, RegimeTributario +from src.generators.sped_writer import ArquivoSPED, criar_registro + +VERSAO_LEIAUTE = "006" + + +@dataclass +class ConfigEFDContrib: + versao: str = VERSAO_LEIAUTE + finalidade: str = "0" + ind_situacao_especial: str = "" + + +class GeradorEFDContribuicoes: + + + def __init__( + self, + periodo: PeriodoApuracao, + creditos: Optional[list[CreditoPisCofins]] = None, + cfg: Optional[ConfigEFDContrib] = None, + ): + self.periodo = periodo + self.creditos = creditos or [] + self.cfg = cfg or ConfigEFDContrib() + self._arquivo = ArquivoSPED( + f"EFD_CONTRIB_{periodo.empresa.cnpj}_{periodo.data_inicio.strftime('%Y%m')}.txt" + ) + + def _bloco_0(self) -> None: + emp = self.periodo.empresa + end = emp.endereco + + self._arquivo.adicionar_linha_raw(criar_registro( + "0000", + self.cfg.versao, + self.cfg.finalidade, + self.periodo.data_inicio, + self.periodo.data_fim, + emp.razao_social, + emp.cnpj, + emp.cnpj[:8], # cod_atividade (raiz CNPJ simplificado) + emp.ie, + emp.im, + emp.suframa, + self.cfg.ind_situacao_especial, + emp.regime_tributario.value, + emp.endereco.uf.value, + )) + + self._arquivo.adicionar_linha_raw(criar_registro("0001", "0")) + + + ind_metodo_apropriacao = "1" # 1=Método de Apropriação Direta + if emp.regime_tributario == RegimeTributario.LUCRO_REAL: + cod_inc_trib = "1" # Lucro Real + elif emp.regime_tributario == RegimeTributario.LUCRO_PRESUMIDO: + cod_inc_trib = "2" # Lucro Presumido + else: + cod_inc_trib = "3" # Imunes/Isentas + + self._arquivo.adicionar_linha_raw(criar_registro( + "0110", + cod_inc_trib, + ind_metodo_apropriacao, + "1", # cod_cred_pis_cofins_nao_aprop + )) + + + self._arquivo.adicionar_linha_raw(criar_registro( + "0140", + emp.cnpj, + emp.nome_fantasia or emp.razao_social, + end.uf.value, + emp.ie, + end.cod_municipio, + emp.im, + emp.suframa, + )) + + self._arquivo.adicionar_linha_raw(criar_registro( + "0990", self._arquivo.total_linhas() + 1 + )) + + def _bloco_c(self) -> None: + + self._arquivo.adicionar_linha_raw(criar_registro("C001", "0")) + + for nf in self.periodo.notas_saida: + tipo_oper = "1" # Saída + self._arquivo.adicionar_linha_raw(criar_registro( + "C100", + tipo_oper, + "0", # ind_emit (0=emissão própria) + nf.destinatario.cnpj, + nf.modelo, + nf.serie, + nf.numero, + nf.chave_acesso, + nf.data_emissao, + nf.data_saida_entrada, + nf.valor_produtos, + Decimal("0"), # vl_desc + nf.valor_ipi, + nf.valor_total, + )) + + for item in nf.itens: + self._arquivo.adicionar_linha_raw(criar_registro( + "C170", + item.numero_item, + item.produto.codigo, + item.produto.descricao, + item.quantidade, + item.produto.unidade, + item.valor_unitario, + item.desconto, + item.cfop, + item.produto.cst_pis, + Decimal("0"), # vl_bc_pis + Decimal("0"), # aliq_pis + Decimal("0"), # quant_bc_pis + Decimal("0"), # aliq_pis_quant + Decimal("0"), # vl_pis + item.produto.cst_cofins, + Decimal("0"), # vl_bc_cofins + Decimal("0"), # aliq_cofins + Decimal("0"), # quant_bc_cofins + Decimal("0"), # aliq_cofins_quant + Decimal("0"), # vl_cofins + "", # cod_cta + )) + + self._arquivo.adicionar_linha_raw(criar_registro( + "C990", self._arquivo.total_linhas() + )) + + def _bloco_m(self, resultado: ResultadoPisCofins) -> None: + + self._arquivo.adicionar_linha_raw(criar_registro("M001", "0")) + + + self._arquivo.adicionar_linha_raw(criar_registro( + "M100", + "01", # cod_cred_aprop + "1", # ind_cred_ori (1=apurado no período) + resultado.base_pis, # vl_bc_pis + resultado.aliq_pis, # aliq_pis_aplic + Decimal("0"), # quant_bc_pis + Decimal("0"), # aliq_pis_quant + resultado.creditos_pis, # vl_cred + Decimal("0"), # vl_ajustes_a_cred + resultado.creditos_pis, # vl_cred_dif + Decimal("0"), # vl_cred_disp + resultado.creditos_pis, # vl_cred_desc_pa_ant + Decimal("0"), # vl_cred_per_pa_ant + Decimal("0"), # vl_cred_dcomp_pa_ant + resultado.creditos_pis, # vl_saldo_cred_retornar + )) + + + self._arquivo.adicionar_linha_raw(criar_registro( + "M200", + resultado.valor_pis_debito, # vl_tot_cont_nc_per + Decimal("0"), # vl_tot_cred_desc + Decimal("0"), # vl_tot_cred_desc_ant + resultado.creditos_pis, # vl_tot_cred_desc_per + resultado.pis_a_recolher, # vl_tot_cont_nc_dev + Decimal("0"), # vl_ret_nc + resultado.pis_a_recolher, # vl_out_ded_nc + resultado.pis_a_recolher, # vl_cont_nc_rec + Decimal("0"), # vl_tot_cont_cum_per + Decimal("0"), # vl_ret_cum + Decimal("0"), # vl_out_ded_cum + Decimal("0"), # vl_cont_cum_rec + resultado.pis_a_recolher, # vl_tot_cont_rec + )) + + + self._arquivo.adicionar_linha_raw(criar_registro( + "M600", + resultado.valor_cofins_debito, # vl_tot_cont_nc_per + Decimal("0"), # vl_tot_cred_desc + Decimal("0"), # vl_tot_cred_desc_ant + resultado.creditos_cofins, # vl_tot_cred_desc_per + resultado.cofins_a_recolher, # vl_tot_cont_nc_dev + Decimal("0"), # vl_ret_nc + resultado.cofins_a_recolher, # vl_out_ded_nc + resultado.cofins_a_recolher, # vl_cont_nc_rec + Decimal("0"), # vl_tot_cont_cum_per + Decimal("0"), # vl_ret_cum + Decimal("0"), # vl_out_ded_cum + Decimal("0"), # vl_cont_cum_rec + resultado.cofins_a_recolher, # vl_tot_cont_rec + )) + + self._arquivo.adicionar_linha_raw(criar_registro( + "M990", self._arquivo.total_linhas() + )) + + def _bloco_1(self) -> None: + + self._arquivo.adicionar_linha_raw(criar_registro("1001", "1")) + self._arquivo.adicionar_linha_raw(criar_registro( + "1990", self._arquivo.total_linhas() + )) + + def _bloco_9(self) -> None: + self._arquivo.adicionar_linha_raw(criar_registro("9001", "0")) + qt = self._arquivo.total_linhas() + 3 + self._arquivo.adicionar_linha_raw(criar_registro("9900", "0000", "1")) + self._arquivo.adicionar_linha_raw(criar_registro("9990", qt)) + self._arquivo.adicionar_linha_raw(criar_registro("9999", qt + 1)) + + def gerar(self, diretorio: str | Path = ".") -> Path: + receitas = [ + ReceitaBruta( + descricao=f"Vendas NF {nf.numero}", + valor=nf.valor_produtos, + cst_pis="01", + cst_cofins="01", + ) + for nf in self.periodo.notas_saida + ] + + resultado = calcular_pis_cofins( + receitas=receitas, + regime=self.periodo.empresa.regime_tributario, + creditos=self.creditos, + ) + + self._bloco_0() + self._bloco_c() + self._bloco_m(resultado) + self._bloco_1() + self._bloco_9() + + return self._arquivo.salvar(diretorio) diff --git a/src/generators/efd_icms_ipi.py b/src/generators/efd_icms_ipi.py new file mode 100644 index 0000000000000000000000000000000000000000..8ea736d09c4d44f48931bd241f539de6d72b2e62 --- /dev/null +++ b/src/generators/efd_icms_ipi.py @@ -0,0 +1,438 @@ + +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import date +from decimal import Decimal +from pathlib import Path +from typing import Optional + +from src.fiscal.entities import Empresa, NotaFiscal, PeriodoApuracao, Produto +from src.generators.sped_writer import ArquivoSPED, RegistroSPED, criar_registro + +VERSAO_LEIAUTE = "017" +CODIGO_FINALIDADE = "0" # 0=Original + + +@dataclass +class ConfigEFD: + ind_perfil: str = "A" + ind_atividade: str = "0" + versao: str = VERSAO_LEIAUTE + finalidade: str = CODIGO_FINALIDADE + + +class GeradorEFDICMSIPI: + + + def __init__(self, periodo: PeriodoApuracao, contador: Optional[object] = None, cfg: Optional[ConfigEFD] = None): + self.periodo = periodo + self.contador = contador + self.cfg = cfg or ConfigEFD() + self._arquivo = ArquivoSPED( + f"EFD_ICMS_IPI_{periodo.empresa.cnpj}_{periodo.data_inicio.strftime('%Y%m')}.txt" + ) + self._qt_linhas_bloco: dict[str, int] = {} + self._participantes: dict[str, Empresa] = {} + self._produtos: dict[str, Produto] = {} + + def _coletar_participantes_produtos(self) -> None: + for nf in self.periodo.notas_saida + self.periodo.notas_entrada: + cnpj_dest = nf.destinatario.cnpj + if cnpj_dest not in self._participantes: + self._participantes[cnpj_dest] = nf.destinatario + for item in nf.itens: + cod = item.produto.codigo + if cod not in self._produtos: + self._produtos[cod] = item.produto + + def _bloco_0(self) -> None: + emp = self.periodo.empresa + end = emp.endereco + + + self._arquivo.adicionar_linha_raw(criar_registro( + "0000", + self.cfg.versao, + self.cfg.finalidade, + self.periodo.data_inicio, + self.periodo.data_fim, + emp.razao_social, + emp.cnpj, + end.uf.value, + emp.ie, + end.cod_municipio, + emp.im, + emp.suframa, + self.cfg.ind_perfil, + self.cfg.ind_atividade, + )) + + + self._arquivo.adicionar_linha_raw(criar_registro("0001", "0")) + + self._arquivo.adicionar_linha_raw(criar_registro( + "0005", + emp.nome_fantasia or emp.razao_social, + end.cep, + end.logradouro, + end.numero, + end.complemento, + end.bairro, + end.municipio, + end.uf.value, + emp.contato.telefone, + emp.contato.fax, + emp.contato.email, + )) + + + if self.contador: + c = self.contador + self._arquivo.adicionar_linha_raw(criar_registro( + "0100", + c.nome, + c.cpf, + c.crc, + c.cnpj_escritorio, + getattr(c.endereco, "cep", "") if c.endereco else "", + getattr(c.endereco, "logradouro", "") if c.endereco else "", + getattr(c.endereco, "numero", "") if c.endereco else "", + getattr(c.endereco, "complemento", "") if c.endereco else "", + getattr(c.endereco, "bairro", "") if c.endereco else "", + c.contato.telefone, + c.contato.fax, + c.contato.email, + getattr(c.endereco, "cod_municipio", "") if c.endereco else "", + )) + + + for cnpj, part in self._participantes.items(): + self._arquivo.adicionar_linha_raw(criar_registro( + "0150", + cnpj, + part.razao_social, + "1058", # Brasil + cnpj, + "", # CPF + part.ie, + part.endereco.cod_municipio, + part.suframa, + part.endereco.logradouro, + part.endereco.numero, + part.endereco.complemento, + part.endereco.bairro, + )) + + + for cod, prod in self._produtos.items(): + self._arquivo.adicionar_linha_raw(criar_registro( + "0200", + cod, + prod.descricao, + prod.cod_barras, + "", # cod anterior + prod.unidade, + prod.tipo_item, + prod.ncm, + prod.ex_ipi, + prod.cod_gen, + "", # cod_lst + prod.aliq_icms, + prod.cest, + )) + + + qt = self._arquivo.total_linhas() + 1 + self._arquivo.adicionar_linha_raw(criar_registro("0990", qt)) + + def _bloco_c(self) -> None: + self._arquivo.adicionar_linha_raw(criar_registro("C001", "0")) + qt_inicio = self._arquivo.total_linhas() + + for nf in self.periodo.notas_saida + self.periodo.notas_entrada: + ind_oper = "1" if nf in self.periodo.notas_saida else "0" + ind_emit = "0" if nf in self.periodo.notas_saida else "1" + + + self._arquivo.adicionar_linha_raw(criar_registro( + "C100", + ind_oper, + ind_emit, + nf.destinatario.cnpj if ind_oper == "1" else nf.emitente.cnpj, + nf.modelo, + nf.serie, + nf.numero, + nf.chave_acesso, + nf.data_emissao, + nf.data_saida_entrada, + nf.valor_produtos, + Decimal("0"), # bc_icms + nf.valor_icms, + Decimal("0"), # bc_icms_st + Decimal("0"), # vl_icms_st + nf.valor_ipi, + Decimal("0"), # vl_pis + Decimal("0"), # vl_cofins + Decimal("0"), # vl_pis_st + Decimal("0"), # vl_cofins_st + )) + + + if nf.info_complementar: + self._arquivo.adicionar_linha_raw(criar_registro( + "C110", "001", nf.info_complementar + )) + + + for item in nf.itens: + self._arquivo.adicionar_linha_raw(criar_registro( + "C170", + item.numero_item, + item.produto.codigo, + item.produto.descricao, + item.quantidade, + item.produto.unidade, + item.valor_unitario, + item.desconto, + item.cfop, + item.produto.ncm, + Decimal("0"), # ex_ipi + item.produto.cst_icms, + item.base_icms, + item.produto.aliq_icms, + item.valor_icms, + Decimal("0"), # bc_icms_st + Decimal("0"), # aliq_icms_st + Decimal("0"), # vl_icms_st + Decimal("0"), # ind_apur + item.produto.cst_ipi, + item.produto.ex_ipi, + item.base_ipi, + item.produto.aliq_ipi, + item.valor_ipi, + item.produto.cst_pis, + Decimal("0"), # bc_pis + item.produto.aliq_icms, # aliq_pis (simplificado) + Decimal("0"), # vl_pis + item.produto.cst_cofins, + Decimal("0"), # bc_cofins + Decimal("0"), # aliq_cofins + Decimal("0"), # vl_cofins + item.outras_despesas, + )) + + + self._arquivo.adicionar_linha_raw(criar_registro( + "C190", + item.produto.cst_icms if nf.itens else "000", + item.cfop if nf.itens else "", + item.produto.aliq_icms if nf.itens else Decimal("0"), + nf.valor_produtos, + nf.valor_icms, + Decimal("0"), # vl_bc_icms_st + Decimal("0"), # vl_icms_st + Decimal("0"), # vl_red_bc + nf.valor_ipi, + "0", # cod_obs + )) + + qt_fim = self._arquivo.total_linhas() + 1 + self._arquivo.adicionar_linha_raw(criar_registro("C990", qt_fim - qt_inicio + 1)) + + def _bloco_e(self) -> None: + """Bloco E - Apuração do ICMS e IPI.""" + self._arquivo.adicionar_linha_raw(criar_registro("E001", "0")) + + + self._arquivo.adicionar_linha_raw(criar_registro( + "E100", + self.periodo.data_inicio, + self.periodo.data_fim, + )) + + + total_debitos = sum(nf.valor_icms for nf in self.periodo.notas_saida) + total_creditos = sum(nf.valor_icms for nf in self.periodo.notas_entrada) + saldo_devedor = max(Decimal("0"), total_debitos - total_creditos) + saldo_credor = max(Decimal("0"), total_creditos - total_debitos) + + self._arquivo.adicionar_linha_raw(criar_registro( + "E110", + total_debitos, # VL_TOT_DEBITOS + Decimal("0"), # VL_AJ_DEBITOS + Decimal("0"), # VL_TOT_AJ_DEBITOS + Decimal("0"), # VL_ESTORNOS_CRED + total_creditos, # VL_TOT_CREDITOS + Decimal("0"), # VL_AJ_CREDITOS + Decimal("0"), # VL_TOT_AJ_CREDITOS + Decimal("0"), # VL_ESTORNOS_DEB + Decimal("0"), # VL_SLD_CREDOR_ANT + saldo_devedor, # VL_SLD_APURADO + Decimal("0"), # VL_TOT_DED + saldo_devedor, # VL_ICMS_RECOLHER + saldo_credor, # VL_SLD_CREDOR_TRANSPORTAR + Decimal("0"), # DEB_ESP + )) + + + self._arquivo.adicionar_linha_raw(criar_registro( + "E500", + self.periodo.data_inicio, + self.periodo.data_fim, + "999", # cod_ind_apur - outros + )) + + total_ipi_debitos = sum(nf.valor_ipi for nf in self.periodo.notas_saida) + total_ipi_creditos = sum(nf.valor_ipi for nf in self.periodo.notas_entrada) + saldo_ipi = max(Decimal("0"), total_ipi_debitos - total_ipi_creditos) + + + self._arquivo.adicionar_linha_raw(criar_registro( + "E510", + "50", # CST_IPI + total_ipi_debitos, # VL_BC_CONS + Decimal("0"), # VL_BC_PROD + )) + + + self._arquivo.adicionar_linha_raw(criar_registro( + "E520", + Decimal("0"), # VL_SD_ANT_IPI + total_ipi_debitos, # VL_DEBITOS_IPI + total_ipi_creditos, # VL_CREDITOS_IPI + Decimal("0"), # VL_OD_IPI + saldo_ipi, # VL_OC_IPI + saldo_ipi, # VL_IPI_RECOLHER + Decimal("0"), # VL_SD_CRED_TRANSPORTAR + Decimal("0"), # VL_DEB_ESP + )) + + self._arquivo.adicionar_linha_raw(criar_registro("E990", "0")) + + def _bloco_h(self) -> None: + """Bloco H - Inventário Físico.""" + self._arquivo.adicionar_linha_raw(criar_registro("H001", "1")) + + data_inv = self.periodo.data_fim + total_inv = sum(m.valor_total for m in self.periodo.movimentacoes) + self._arquivo.adicionar_linha_raw(criar_registro( + "H005", data_inv, total_inv, "01" + )) + for mov in self.periodo.movimentacoes: + self._arquivo.adicionar_linha_raw(criar_registro( + "H010", + mov.produto.codigo, + mov.produto.unidade, + mov.quantidade, + mov.valor_unitario, + mov.valor_total, + "00", # ind_prop + "", # cod_part + "", # txt_compl + "", # cod_cta + "", # vl_item_ir + )) + self._arquivo.adicionar_linha_raw(criar_registro("H990", "0")) + + def _bloco_g(self) -> None: + """ + Bloco G — CIAP: Controle de Crédito do ICMS do Ativo Permanente. + Escritura os créditos de ICMS sobre bens do ativo imobilizado (1/48 avos). + IND_MOV=1 (sem movimento) quando não há ativos com ICMS a creditar. + """ + + ind_mov = "1" + self._arquivo.adicionar_linha_raw(criar_registro("G001", ind_mov)) + + if ind_mov == "0": + # G110 — período de apuração do CIAP + self._arquivo.adicionar_linha_raw(criar_registro( + "G110", + self.periodo.data_inicio, + self.periodo.data_fim, + "0,00", # VL_ICMS_ANT_P (saldo inicial) + "48", # NUM_PARC (número de parcelas — bens do ativo: 48 meses) + )) + + self._arquivo.adicionar_linha_raw(criar_registro( + "G125", + "", # COD_IND_BEM + self.periodo.data_inicio, + "AT", # COD_CCUS (centro de custo) + "0,00", # VL_ICMS_OP + "0,00", # VL_ICMS_ST + "0,00", # VL_ICMS_FRT + "0,00", # VL_ICMS_DIF + "48", # NUM_PARC + "0,00", # VL_PARC_PASS + )) + + qt = self._arquivo.total_linhas() + 1 + self._arquivo.adicionar_linha_raw(criar_registro("G990", qt)) + + def _bloco_k(self) -> None: + """ + Bloco K — Controle da Produção e do Estoque. + Obrigatório para industriais/atacadistas acima do limite (IN RFB 1.600/2015). + IND_MOV=1 quando não há movimentação de produção no período. + """ + # IND_MOV: 0=com dados, 1=sem movimento de produção + ind_mov = "0" if self.periodo.movimentacoes else "1" + self._arquivo.adicionar_linha_raw(criar_registro("K001", ind_mov)) + + if ind_mov == "0": + # K010 — estabelecimento industrial + # IND_TIPO_CONTRIBUINTE: 0=industrial, 1=industrial misto, 2=atacadista + self._arquivo.adicionar_linha_raw(criar_registro("K010", "0")) + + # K100 — período de apuração + self._arquivo.adicionar_linha_raw(criar_registro( + "K100", + self.periodo.data_inicio, + self.periodo.data_fim, + )) + + # K200 — estoque escriturado por produto + for mov in self.periodo.movimentacoes: + if hasattr(mov, "produto") and hasattr(mov, "quantidade"): + self._arquivo.adicionar_linha_raw(criar_registro( + "K200", + self.periodo.data_fim, # DT_EST + mov.produto.codigo, # COD_ITEM + mov.quantidade, # QTD + mov.produto.unidade, # UNID + "0", # IND_EST: 0=estoque real + )) + + qt = self._arquivo.total_linhas() + 1 + self._arquivo.adicionar_linha_raw(criar_registro("K990", qt)) + + def _bloco_1(self) -> None: + """Bloco 1 - Outras Informações.""" + self._arquivo.adicionar_linha_raw(criar_registro("1001", "1")) + self._arquivo.adicionar_linha_raw(criar_registro("1990", "2")) + + def _bloco_9(self) -> None: + """Bloco 9 - Controle e Encerramento do Arquivo.""" + self._arquivo.adicionar_linha_raw(criar_registro("9001", "0")) + qt_total = self._arquivo.total_linhas() + 4 + # registros de controle por bloco + for reg, qt in [("0000", 1), ("C001", 1), ("E001", 1), + ("G001", 1), ("H001", 1), ("K001", 1), + ("1001", 1), ("9001", 1)]: + self._arquivo.adicionar_linha_raw(criar_registro("9900", reg, qt)) + self._arquivo.adicionar_linha_raw(criar_registro("9990", qt_total)) + self._arquivo.adicionar_linha_raw(criar_registro("9999", qt_total + 1)) + + def gerar(self, diretorio: str | Path = ".") -> Path: + self._coletar_participantes_produtos() + self._bloco_0() + self._bloco_c() + self._bloco_e() + self._bloco_g() + self._bloco_h() + self._bloco_k() + self._bloco_1() + self._bloco_9() + return self._arquivo.salvar(diretorio) diff --git a/src/generators/efd_reinf.py b/src/generators/efd_reinf.py new file mode 100644 index 0000000000000000000000000000000000000000..6f25351817d69fd96567c30799aacef6e45b7e70 --- /dev/null +++ b/src/generators/efd_reinf.py @@ -0,0 +1,362 @@ + +from __future__ import annotations + +import hashlib +import re +from dataclasses import dataclass, field +from datetime import date, datetime +from decimal import Decimal +from pathlib import Path +from typing import Optional +from xml.etree import ElementTree as ET + +NS_REINF = "http://www.reinf.esocial.gov.br/schema/evt" + + +VERSAO = "2.01.02" + + +def _sub(pai: ET.Element, tag: str, texto: str = "") -> ET.Element: + el = ET.SubElement(pai, tag) + if texto: + el.text = str(texto) + return el + + +def _fmt_dec(v: Decimal, casas: int = 2) -> str: + return f"{v:.{casas}f}" + + +@dataclass +class PrestadorServico: + cnpj_cpf: str + nome: str + tipo: str = "J" # J=PJ, F=PF + valor_bruto: Decimal = Decimal("0") + valor_bc_csll: Decimal = Decimal("0") + valor_bc_irrf: Decimal = Decimal("0") + valor_bc_pis: Decimal = Decimal("0") + valor_bc_cofins: Decimal = Decimal("0") + aliq_csll: Decimal = Decimal("1.0") + aliq_irrf: Decimal = Decimal("1.5") + aliq_pis: Decimal = Decimal("0.65") + aliq_cofins: Decimal = Decimal("3.0") + cod_servico: str = "0001" + nota_fiscal: str = "" + data_pagamento: Optional[date] = None + + @property + def valor_csll(self) -> Decimal: + return (self.valor_bc_csll * self.aliq_csll / 100).quantize(Decimal("0.01")) + + @property + def valor_irrf(self) -> Decimal: + return (self.valor_bc_irrf * self.aliq_irrf / 100).quantize(Decimal("0.01")) + + @property + def valor_pis(self) -> Decimal: + return (self.valor_bc_pis * self.aliq_pis / 100).quantize(Decimal("0.01")) + + @property + def valor_cofins(self) -> Decimal: + return (self.valor_bc_cofins * self.aliq_cofins / 100).quantize(Decimal("0.01")) + + @property + def total_retencoes(self) -> Decimal: + return self.valor_csll + self.valor_irrf + self.valor_pis + self.valor_cofins + + +@dataclass +class EventoR2010: + + cnpj_tomador: str + periodo: str # YYYY-MM + prestadores: list[PrestadorServico] = field(default_factory=list) + numero_evento: int = 1 + + +@dataclass +class EventoR2020: + """R-2020: Retenções na fonte — Serviços prestados para PJ.""" + cnpj_prestador: str + periodo: str # YYYY-MM + tomadores: list[PrestadorServico] = field(default_factory=list) + numero_evento: int = 1 + + +@dataclass +class EventoR2060: + """R-2060: Contribuição Previdenciária sobre a Receita Bruta (CPRB).""" + cnpj: str + periodo: str + receita_bruta_total: Decimal = Decimal("0") + receita_bruta_excluida: Decimal = Decimal("0") + aliq_apur: Decimal = Decimal("0") + numero_evento: int = 1 + + @property + def base_calculo(self) -> Decimal: + return self.receita_bruta_total - self.receita_bruta_excluida + + @property + def valor_contribuicao(self) -> Decimal: + return (self.base_calculo * self.aliq_apur / 100).quantize(Decimal("0.01")) + + +class GeradorEFDReinf: + """ + Gera os eventos da EFD-Reinf em XML conforme os schemas da Receita Federal. + + Eventos implementados: + - R-1000: Informações do contribuinte + - R-2010: Retenções — serviços tomados de PJ + - R-2020: Retenções — serviços prestados para PJ + - R-2060: CPRB (Contribuição Previdenciária sobre Receita Bruta) + - R-2099: Fechamento dos eventos periódicos + """ + + def __init__( + self, + cnpj: str, + ambiente: str = "2", # 1=Produção, 2=Testes + transmissor_cnpj: Optional[str] = None, + ): + self.cnpj = re.sub(r"\D", "", cnpj) + self.ambiente = ambiente + self.transmissor_cnpj = transmissor_cnpj or self.cnpj + self._numero_lote = 1 + + def _cabecalho(self, evt: ET.Element, tp_inscricao: str = "1") -> None: + """Gera o elemento ideContri (identificação do contribuinte).""" + ide = _sub(evt, "ideContri") + _sub(ide, "tpInsc", tp_inscricao) # 1=CNPJ + _sub(ide, "nrInsc", self.cnpj[:8]) # Raiz do CNPJ + + def _ide_evento(self, evt: ET.Element, nr_evento: int, periodo: str) -> None: + ide = _sub(evt, "ideEvento") + _sub(ide, "nrRec", "") # Número do recibo (preenchido após envio) + _sub(ide, "perApur", periodo) + _sub(ide, "tpAmb", self.ambiente) + _sub(ide, "procEmi", "1") # 1=Aplicativo do contribuinte + _sub(ide, "verProc", "1.0") + + def gerar_r1000(self, periodo_inicio: date, periodo_fim: date) -> str: + """R-1000: Informações do Contribuinte (envio único ou atualização).""" + root = ET.Element("Reinf") + root.set("xmlns", NS_REINF) + root.set("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") + + evt = _sub(root, "evtInfoContri") + evt.set("id", f"ID1{self.cnpj}{datetime.now().strftime('%Y%m%d%H%M%S')}00001") + + ide = _sub(evt, "ideEvento") + _sub(ide, "tpAmb", self.ambiente) + _sub(ide, "procEmi", "1") + _sub(ide, "verProc", "1.0") + + self._cabecalho(evt) + + info_contrib = _sub(evt, "infoContri") + inclusao = _sub(info_contrib, "inclusao") + ide_contrib = _sub(inclusao, "ideContri") + _sub(ide_contrib, "tpInsc", "1") + _sub(ide_contrib, "nrInsc", self.cnpj) + + dados = _sub(inclusao, "dadosContri") + _sub(dados, "classTrib", "99") # 99=Outros + _sub(dados, "indEscrituracao", "1") # 1=Lucro Real + _sub(dados, "indDesoneracao", "0") # 0=Não desonerado + _sub(dados, "indAcordoIsenMulta", "N") + _sub(dados, "indSitPJ", "0") # 0=Normal + _sub(dados, "dtTrans11096", periodo_inicio.strftime("%Y-%m-%d")) + + ET.indent(root, space=" ") + return ET.tostring(root, encoding="unicode", xml_declaration=True) + + def gerar_r2010(self, evento: EventoR2010) -> str: + """R-2010: Retenções na fonte — serviços tomados de PJ.""" + root = ET.Element("Reinf") + root.set("xmlns", NS_REINF) + + evt = _sub(root, "evtServTom") + evt.set("id", f"ID1{self.cnpj}{datetime.now().strftime('%Y%m%d%H%M%S')}{str(evento.numero_evento).zfill(5)}") + + self._ide_evento(evt, evento.numero_evento, evento.periodo) + self._cabecalho(evt) + + ide_tomador = _sub(evt, "ideTomador") + _sub(ide_tomador, "tpInsc", "1") + _sub(ide_tomador, "nrInsc", re.sub(r"\D", "", evento.cnpj_tomador)[:14]) + + for prest in evento.prestadores: + ide_estab = _sub(evt, "ideEstab") + _sub(ide_estab, "tpInsc", "1") + _sub(ide_estab, "nrInsc", re.sub(r"\D", "", evento.cnpj_tomador)[:14]) + + ide_prest = _sub(ide_estab, "idePrestServ") + _sub(ide_prest, "tpInsc", "1" if prest.tipo == "J" else "2") + _sub(ide_prest, "nrInsc", re.sub(r"\D", "", prest.cnpj_cpf)[:14]) + + if prest.nota_fiscal: + docs = _sub(ide_prest, "ideDocs") + doc = _sub(docs, "ideDoc") + _sub(doc, "nrDoc", prest.nota_fiscal) + _sub(doc, "vlrBruto", _fmt_dec(prest.valor_bruto)) + + if prest.total_retencoes > 0: + info_ret = _sub(ide_prest, "infoRet") + if prest.valor_csll > 0: + _sub(info_ret, "vlrBaseCSLL", _fmt_dec(prest.valor_bc_csll)) + _sub(info_ret, "vlrCSLL", _fmt_dec(prest.valor_csll)) + if prest.valor_irrf > 0: + _sub(info_ret, "vlrBaseIRRF", _fmt_dec(prest.valor_bc_irrf)) + _sub(info_ret, "vlrIRRF", _fmt_dec(prest.valor_irrf)) + if prest.valor_pis > 0: + _sub(info_ret, "vlrBasePIS", _fmt_dec(prest.valor_bc_pis)) + _sub(info_ret, "vlrPIS", _fmt_dec(prest.valor_pis)) + if prest.valor_cofins > 0: + _sub(info_ret, "vlrBaseCOFINS", _fmt_dec(prest.valor_bc_cofins)) + _sub(info_ret, "vlrCOFINS", _fmt_dec(prest.valor_cofins)) + + ET.indent(root, space=" ") + return ET.tostring(root, encoding="unicode", xml_declaration=True) + + def gerar_r2020(self, evento: EventoR2020) -> str: + """R-2020: Retenções na fonte — serviços prestados para PJ.""" + root = ET.Element("Reinf") + root.set("xmlns", NS_REINF) + + evt = _sub(root, "evtServPrest") + evt.set("id", f"ID1{self.cnpj}{datetime.now().strftime('%Y%m%d%H%M%S')}{str(evento.numero_evento).zfill(5)}") + + self._ide_evento(evt, evento.numero_evento, evento.periodo) + self._cabecalho(evt) + + ide_prest = _sub(evt, "idePrestServ") + _sub(ide_prest, "tpInsc", "1") + _sub(ide_prest, "nrInsc", re.sub(r"\D", "", evento.cnpj_prestador)[:14]) + + for tom in evento.tomadores: + ide_tom = _sub(evt, "ideTomador") + _sub(ide_tom, "tpInsc", "1" if tom.tipo == "J" else "2") + _sub(ide_tom, "nrInsc", re.sub(r"\D", "", tom.cnpj_cpf)[:14]) + + if tom.total_retencoes > 0: + info_ret = _sub(ide_tom, "infoRet") + _sub(info_ret, "vlrBruto", _fmt_dec(tom.valor_bruto)) + if tom.valor_csll > 0: + _sub(info_ret, "vlrBaseCSLL", _fmt_dec(tom.valor_bc_csll)) + _sub(info_ret, "vlrCSLL", _fmt_dec(tom.valor_csll)) + if tom.valor_irrf > 0: + _sub(info_ret, "vlrBaseIRRF", _fmt_dec(tom.valor_bc_irrf)) + _sub(info_ret, "vlrIRRF", _fmt_dec(tom.valor_irrf)) + if tom.valor_pis > 0: + _sub(info_ret, "vlrBasePIS", _fmt_dec(tom.valor_bc_pis)) + _sub(info_ret, "vlrPIS", _fmt_dec(tom.valor_pis)) + if tom.valor_cofins > 0: + _sub(info_ret, "vlrBaseCOFINS", _fmt_dec(tom.valor_bc_cofins)) + _sub(info_ret, "vlrCOFINS", _fmt_dec(tom.valor_cofins)) + + ET.indent(root, space=" ") + return ET.tostring(root, encoding="unicode", xml_declaration=True) + + def gerar_r2060(self, evento: EventoR2060) -> str: + """R-2060: Contribuição Previdenciária sobre a Receita Bruta (CPRB).""" + root = ET.Element("Reinf") + root.set("xmlns", NS_REINF) + + evt = _sub(root, "evtCPRB") + evt.set("id", f"ID1{self.cnpj}{datetime.now().strftime('%Y%m%d%H%M%S')}{str(evento.numero_evento).zfill(5)}") + + self._ide_evento(evt, evento.numero_evento, evento.periodo) + self._cabecalho(evt) + + ide_estab = _sub(evt, "ideEstab") + _sub(ide_estab, "tpInsc", "1") + _sub(ide_estab, "nrInsc", re.sub(r"\D", "", evento.cnpj)[:14]) + + info_cprb = _sub(ide_estab, "infoCPRB") + _sub(info_cprb, "indAcConstitucionalidade", "N") + _sub(info_cprb, "vlrRecBrtTotal", _fmt_dec(evento.receita_bruta_total)) + _sub(info_cprb, "vlrRecBrtExclTotal", _fmt_dec(evento.receita_bruta_excluida)) + _sub(info_cprb, "vlrRecBrtBase", _fmt_dec(evento.base_calculo)) + _sub(info_cprb, "aliqApur", _fmt_dec(evento.aliq_apur, 4)) + _sub(info_cprb, "vlrCPRBApur", _fmt_dec(evento.valor_contribuicao)) + _sub(info_cprb, "vlrCPRBSusp", "0.00") + _sub(info_cprb, "vlrCPRBRec", _fmt_dec(evento.valor_contribuicao)) + + ET.indent(root, space=" ") + return ET.tostring(root, encoding="unicode", xml_declaration=True) + + def gerar_r2099(self, periodo: str, hash_total: str = "") -> str: + """R-2099: Fechamento dos Eventos Periódicos.""" + root = ET.Element("Reinf") + root.set("xmlns", NS_REINF) + + evt = _sub(root, "evtFechamento") + evt.set("id", f"ID1{self.cnpj}{datetime.now().strftime('%Y%m%d%H%M%S')}99999") + + self._ide_evento(evt, 99999, periodo) + self._cabecalho(evt) + + ideRespInf = _sub(evt, "ideRespInf") + _sub(ideRespInf, "nrInsc", self.transmissor_cnpj[:14]) + _sub(ideRespInf, "nome", "RESPONSAVEL INFORMACAO") + + fechamento = _sub(evt, "fechamento") + _sub(fechamento, "evtServTom", "S") # Tem R-2010? + _sub(fechamento, "evtServPrest", "N") # Tem R-2020? + _sub(fechamento, "evtAssocDespRec", "N") + _sub(fechamento, "evtCPRB", "N") + _sub(fechamento, "evtRRA", "N") + _sub(fechamento, "evtRecurso", "N") + _sub(fechamento, "evtComProd", "N") + _sub(fechamento, "evtInfoComplPer", "N") + + ET.indent(root, space=" ") + return ET.tostring(root, encoding="unicode", xml_declaration=True) + + def salvar_eventos( + self, + diretorio: str | Path, + eventos_r2010: Optional[list[EventoR2010]] = None, + eventos_r2020: Optional[list[EventoR2020]] = None, + eventos_r2060: Optional[list[EventoR2060]] = None, + periodo: str = "", + ) -> list[Path]: + diretorio = Path(diretorio) + diretorio.mkdir(parents=True, exist_ok=True) + arquivos = [] + + + r1000 = diretorio / f"R1000_{self.cnpj}_{periodo}.xml" + r1000.write_text(self.gerar_r1000(date.today(), date.today()), encoding="utf-8") + arquivos.append(r1000) + + + for i, evt in enumerate(eventos_r2010 or [], 1): + evt.numero_evento = i + p = diretorio / f"R2010_{self.cnpj}_{periodo}_{i:04d}.xml" + p.write_text(self.gerar_r2010(evt), encoding="utf-8") + arquivos.append(p) + + + for i, evt in enumerate(eventos_r2020 or [], 1): + evt.numero_evento = i + p = diretorio / f"R2020_{self.cnpj}_{periodo}_{i:04d}.xml" + p.write_text(self.gerar_r2020(evt), encoding="utf-8") + arquivos.append(p) + + + for i, evt in enumerate(eventos_r2060 or [], 1): + evt.numero_evento = i + p = diretorio / f"R2060_{self.cnpj}_{periodo}_{i:04d}.xml" + p.write_text(self.gerar_r2060(evt), encoding="utf-8") + arquivos.append(p) + + + r2099 = diretorio / f"R2099_{self.cnpj}_{periodo}.xml" + r2099.write_text(self.gerar_r2099(periodo), encoding="utf-8") + arquivos.append(r2099) + + return arquivos diff --git a/src/generators/esocial.py b/src/generators/esocial.py new file mode 100644 index 0000000000000000000000000000000000000000..f4c7b5e281651eebc6d4aded9e3e44ba4864fe09 --- /dev/null +++ b/src/generators/esocial.py @@ -0,0 +1,429 @@ + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from datetime import date, datetime +from decimal import Decimal +from pathlib import Path +from typing import Optional +from xml.etree import ElementTree as ET + +NS_ESOCIAL = "http://www.esocial.gov.br/schema/evt" +VERSAO = "S-1.2" + + +def _sub(pai: ET.Element, tag: str, texto: str = "") -> ET.Element: + el = ET.SubElement(pai, tag) + if texto: + el.text = str(texto) + return el + + +def _fmt_dec(v: Decimal, casas: int = 2) -> str: + return f"{v:.{casas}f}" + + +@dataclass +class Trabalhador: + cpf: str + nome: str + data_nascimento: date + sexo: str = "M" # M=Masculino, F=Feminino + raca_cor: str = "1" # 1=Branca + grau_instrucao: str = "07" # 07=Fundamental Completo + pis_pasep: str = "" + endereco: str = "" + municipio: str = "" + uf: str = "SP" + cep: str = "" + email: str = "" + telefone: str = "" + nacionalidade: str = "10" # 10=Brasileiro + + +@dataclass +class VinculoEmpregaticio: + trabalhador: Trabalhador + matricula: str + data_admissao: date + tipo_regime: str = "1" # 1=CLT + tipo_vinculo: str = "10" # 10=Empregado + cargo: str = "" + funcao: str = "" + salario_base: Decimal = Decimal("0") + tipo_salario: str = "11" # 11=Mensal + categoria: str = "101" # 101=Empregado geral + cbo: str = "2521-05" + jornada_semanal: Decimal = Decimal("44") + tipo_jornada: str = "2" # 2=12x36 + fgts_optante: bool = True + data_opcao_fgts: Optional[date] = None + + +@dataclass +class EventoRescisao: + trabalhador: Trabalhador + matricula: str + data_desligamento: date + motivo: str = "01" # 01=Rescisão sem justa causa + data_aviso_previo: Optional[date] = None + verba_saldo_salario: Decimal = Decimal("0") + verba_ferias_prop: Decimal = Decimal("0") + verba_13_prop: Decimal = Decimal("0") + verba_aviso_previo: Decimal = Decimal("0") + verba_multa_fgts: Decimal = Decimal("0") + inss_retido: Decimal = Decimal("0") + irrf_retido: Decimal = Decimal("0") + + +@dataclass +class RubricaFolha: + codigo: str + descricao: str + natureza: str # 1=Vencimento, 2=Desconto, 3=Informativo + tipo_incidencia_cp: str = "11" + tipo_incidencia_irrf: str = "11" + tipo_incidencia_fgts: str = "11" + + +@dataclass +class ItemFolha: + rubrica: RubricaFolha + valor: Decimal + quantidade: Decimal = Decimal("0") + + +@dataclass +class FolhaPagamento: + trabalhador: Trabalhador + matricula: str + periodo: str # YYYY-MM + itens: list[ItemFolha] = field(default_factory=list) + base_cp: Decimal = Decimal("0") + base_irrf: Decimal = Decimal("0") + base_fgts: Decimal = Decimal("0") + valor_cp: Decimal = Decimal("0") + valor_irrf: Decimal = Decimal("0") + valor_fgts: Decimal = Decimal("0") + + @property + def total_vencimentos(self) -> Decimal: + return sum(i.valor for i in self.itens if i.rubrica.natureza == "1") + + @property + def total_descontos(self) -> Decimal: + return sum(i.valor for i in self.itens if i.rubrica.natureza == "2") + + @property + def liquido(self) -> Decimal: + return self.total_vencimentos - self.total_descontos + + +class GeradorESocial: + """ + Gera eventos do e-Social em XML. + + Eventos implementados: + - S-1000: Informações do empregador/contribuinte + - S-1010: Tabela de rubricas + - S-2200: Cadastramento inicial de vínculo (admissão) + - S-2206: Alteração de contrato de trabalho + - S-2299: Desligamento + - S-1200: Remuneração do trabalhador (folha de pagamento) + - S-1299: Fechamento dos eventos periódicos + """ + + def __init__(self, cnpj: str, ambiente: str = "2"): + self.cnpj = re.sub(r"\D", "", cnpj) + self.ambiente = ambiente + + def _id_evento(self, nr: int) -> str: + ts = datetime.now().strftime("%Y%m%d%H%M%S") + return f"ID1{self.cnpj}{ts}{str(nr).zfill(5)}" + + def _ide_empregador(self, pai: ET.Element) -> None: + ide = _sub(pai, "ideEmpregador") + _sub(ide, "tpInsc", "1") # 1=CNPJ + _sub(ide, "nrInsc", self.cnpj[:8]) + + def _ide_evento(self, pai: ET.Element, nr: int, periodo: str = "") -> None: + ide = _sub(pai, "ideEvento") + _sub(ide, "indRetif", "1") # 1=Original + _sub(ide, "nrRec", "") + if periodo: + _sub(ide, "perApur", periodo) + _sub(ide, "tpAmb", self.ambiente) + _sub(ide, "procEmi", "1") + _sub(ide, "verProc", "1.0") + + def gerar_s1000(self, razao_social: str, data_inicio: date) -> str: + """S-1000: Informações do empregador/contribuinte.""" + root = ET.Element("eSocial") + root.set("xmlns", NS_ESOCIAL) + root.set("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") + + evt = _sub(root, "evtInfoEmpregador") + evt.set("Id", self._id_evento(1)) + + self._ide_evento(evt, 1) + self._ide_empregador(evt) + + info_emp = _sub(evt, "infoEmpregador") + inclusao = _sub(info_emp, "inclusao") + + ide_emp = _sub(inclusao, "ideEmpregador") + _sub(ide_emp, "tpInsc", "1") + _sub(ide_emp, "nrInsc", self.cnpj) + + dados_emp = _sub(inclusao, "dadosEmpregador") + _sub(dados_emp, "razaoSocial", razao_social[:100]) + _sub(dados_emp, "classTrib", "99") + _sub(dados_emp, "natJurid", "2062") # Sociedade Empresária Limitada + _sub(dados_emp, "indOptRegEletron", "0") + _sub(dados_emp, "cnpjOrgaoPublico", "") + _sub(dados_emp, "dtIni", data_inicio.strftime("%Y-%m-%d")) + + ET.indent(root, space=" ") + return ET.tostring(root, encoding="unicode", xml_declaration=True) + + def gerar_s1010(self, rubricas: list[RubricaFolha]) -> str: + """S-1010: Tabela de rubricas da folha de pagamento.""" + root = ET.Element("eSocial") + root.set("xmlns", NS_ESOCIAL) + + evt = _sub(root, "evtTabRubrica") + evt.set("Id", self._id_evento(10)) + + self._ide_evento(evt, 10) + self._ide_empregador(evt) + + for rub in rubricas: + rubrica = _sub(evt, "rubrica") + ide = _sub(rubrica, "ideRubrica") + _sub(ide, "codRubr", rub.codigo) + _sub(ide, "ideTabRubr", "S") # S=Própria + _sub(ide, "dtIniValid", date.today().strftime("%Y-%m")) + + dados = _sub(rubrica, "dadosRubrica") + _sub(dados, "dscRubr", rub.descricao[:100]) + _sub(dados, "natRubr", rub.natureza) + _sub(dados, "tpRubr", "1") # 1=Proventos/descontos + _sub(dados, "codIncCP", rub.tipo_incidencia_cp) + _sub(dados, "codIncIRRF", rub.tipo_incidencia_irrf) + _sub(dados, "codIncFGTS", rub.tipo_incidencia_fgts) + _sub(dados, "codIncSIND", "00") + + ET.indent(root, space=" ") + return ET.tostring(root, encoding="unicode", xml_declaration=True) + + def gerar_s2200(self, vinculo: VinculoEmpregaticio) -> str: + """S-2200: Cadastramento inicial do vínculo — admissão.""" + root = ET.Element("eSocial") + root.set("xmlns", NS_ESOCIAL) + + evt = _sub(root, "evtAdmissao") + evt.set("Id", self._id_evento(2200)) + + self._ide_evento(evt, 2200) + self._ide_empregador(evt) + + trab = vinculo.trabalhador + ide_vinculo = _sub(evt, "ideVinculo") + _sub(ide_vinculo, "cpfTrab", re.sub(r"\D", "", trab.cpf)) + _sub(ide_vinculo, "matricula", vinculo.matricula) + + dados_trab = _sub(evt, "trabalhador") + _sub(dados_trab, "nmTrab", trab.nome[:70]) + _sub(dados_trab, "sexo", trab.sexo) + _sub(dados_trab, "racaCor", trab.raca_cor) + _sub(dados_trab, "estCiv", "0") # 0=Não informado + _sub(dados_trab, "grauInstr", trab.grau_instrucao) + _sub(dados_trab, "nmSoc", "") + + nascimento = _sub(dados_trab, "nascimento") + _sub(nascimento, "dtNascto", trab.data_nascimento.strftime("%Y-%m-%d")) + _sub(nascimento, "codMunic", "3550308") + _sub(nascimento, "uf", trab.uf) + _sub(nascimento, "paisNascto", "105") # Brasil + _sub(nascimento, "paisNac", "105") + + doc_trab = _sub(dados_trab, "documentos") + ctps = _sub(doc_trab, "ctps") + _sub(ctps, "nrCtps", "000001") + _sub(ctps, "dvCtps", "0") + _sub(ctps, "serieCTPS", "001") + _sub(ctps, "ufCTPS", trab.uf) + _sub(ctps, "dtExped", trab.data_nascimento.replace(year=trab.data_nascimento.year + 18).strftime("%Y-%m-%d")) + + vinc = _sub(evt, "vinculo") + ide_empregador_vinc = _sub(vinc, "ideEmpregador") + _sub(ide_empregador_vinc, "tpInsc", "1") + _sub(ide_empregador_vinc, "nrInsc", self.cnpj) + _sub(ide_empregador_vinc, "vlrDtAdm", vinculo.data_admissao.strftime("%Y-%m-%d")) + + dados_vinc = _sub(vinc, "dadosVinculo") + _sub(dados_vinc, "matricula", vinculo.matricula) + _sub(dados_vinc, "tpRegTrab", vinculo.tipo_regime) + _sub(dados_vinc, "tpRegPrev", "1") # 1=RGPS + _sub(dados_vinc, "dtAdm", vinculo.data_admissao.strftime("%Y-%m-%d")) + _sub(dados_vinc, "tpAdmissao", "1") # 1=Admissão + _sub(dados_vinc, "indicAdmissao", "1") + _sub(dados_vinc, "nrProcTrab", "") + _sub(dados_vinc, "tpRegJor", vinculo.tipo_jornada) + _sub(dados_vinc, "natAtividade", "1") # 1=Trabalho urbano + _sub(dados_vinc, "dtPrevTerm", "") + + cargo_func = _sub(dados_vinc, "cargoFuncao") + _sub(cargo_func, "codCargo", "0001") + _sub(cargo_func, "codFuncao", "0001") + _sub(cargo_func, "CBOCargo", re.sub(r"\D", "", vinculo.cbo)[:6]) + _sub(cargo_func, "CBOFuncao", re.sub(r"\D", "", vinculo.cbo)[:6]) + _sub(cargo_func, "natAtividade", "1") + _sub(cargo_func, "dtIniCargo", vinculo.data_admissao.strftime("%Y-%m-%d")) + + rem = _sub(dados_vinc, "remuneracao") + _sub(rem, "vrSalFx", _fmt_dec(vinculo.salario_base)) + _sub(rem, "undSalFixo", vinculo.tipo_salario) + _sub(rem, "dscSalVar", "") + + fgts = _sub(dados_vinc, "FGTS") + _sub(fgts, "dtOpcFGTS", (vinculo.data_opcao_fgts or vinculo.data_admissao).strftime("%Y-%m-%d")) + + ET.indent(root, space=" ") + return ET.tostring(root, encoding="unicode", xml_declaration=True) + + def gerar_s2299(self, rescisao: EventoRescisao) -> str: + """S-2299: Desligamento (rescisão de contrato).""" + root = ET.Element("eSocial") + root.set("xmlns", NS_ESOCIAL) + + evt = _sub(root, "evtDeslig") + evt.set("Id", self._id_evento(2299)) + + self._ide_evento(evt, 2299) + self._ide_empregador(evt) + + ide_vinculo = _sub(evt, "ideVinculo") + _sub(ide_vinculo, "cpfTrab", re.sub(r"\D", "", rescisao.trabalhador.cpf)) + _sub(ide_vinculo, "matricula", rescisao.matricula) + + info_deslig = _sub(evt, "infoDeslig") + _sub(info_deslig, "dtDeslig", rescisao.data_desligamento.strftime("%Y-%m-%d")) + _sub(info_deslig, "mtvDeslig", rescisao.motivo) + _sub(info_deslig, "dtProjFimAPI", (rescisao.data_aviso_previo or rescisao.data_desligamento).strftime("%Y-%m-%d")) + _sub(info_deslig, "indPensBR", "N") + _sub(info_deslig, "indCessao", "N") + _sub(info_deslig, "pensAlim", "N") + _sub(info_deslig, "dtIniAfast", "") + + verba = _sub(info_deslig, "verbasResc") + _sub(verba, "dtEfetDeslig", rescisao.data_desligamento.strftime("%Y-%m-%d")) + _sub(verba, "vrSalAnt", _fmt_dec(rescisao.verba_saldo_salario)) + _sub(verba, "vrSalFer", _fmt_dec(rescisao.verba_ferias_prop)) + _sub(verba, "vr13", _fmt_dec(rescisao.verba_13_prop)) + _sub(verba, "vrAvPrevio", _fmt_dec(rescisao.verba_aviso_previo)) + _sub(verba, "vrMtFGTS", _fmt_dec(rescisao.verba_multa_fgts)) + _sub(verba, "vrINSS", _fmt_dec(rescisao.inss_retido)) + _sub(verba, "vrIRRF", _fmt_dec(rescisao.irrf_retido)) + + ET.indent(root, space=" ") + return ET.tostring(root, encoding="unicode", xml_declaration=True) + + def gerar_s1200(self, folha: FolhaPagamento) -> str: + """S-1200: Remuneração do trabalhador (folha mensal).""" + root = ET.Element("eSocial") + root.set("xmlns", NS_ESOCIAL) + + evt = _sub(root, "evtRemun") + evt.set("Id", self._id_evento(1200)) + + self._ide_evento(evt, 1200, folha.periodo) + self._ide_empregador(evt) + + ide_vinculo = _sub(evt, "ideVinculo") + _sub(ide_vinculo, "cpfTrab", re.sub(r"\D", "", folha.trabalhador.cpf)) + _sub(ide_vinculo, "matricula", folha.matricula) + + rem = _sub(evt, "remunPerApur") + ide_estab = _sub(rem, "ideEstabLot") + _sub(ide_estab, "tpInsc", "1") + _sub(ide_estab, "nrInsc", self.cnpj) + _sub(ide_estab, "codLotacao", "01") + + for item in folha.itens: + verba = _sub(ide_estab, "verbasRemuneracaoOuDescontos") + _sub(verba, "codRubr", item.rubrica.codigo) + _sub(verba, "ideTabRubr", "S") + _sub(verba, "qtdRubr", _fmt_dec(item.quantidade, 2)) + _sub(verba, "fatorRubr", "") + _sub(verba, "vrRubr", _fmt_dec(item.valor)) + _sub(verba, "indApurIR", "0") + + info_cp = _sub(rem, "infoSaudeColetiva") + _sub(info_cp, "vrBcCp00", _fmt_dec(folha.base_cp)) + _sub(info_cp, "vrBcCpMen", _fmt_dec(folha.base_cp)) + _sub(info_cp, "vrBcFgts", _fmt_dec(folha.base_fgts)) + _sub(info_cp, "vrFgts", _fmt_dec(folha.valor_fgts)) + _sub(info_cp, "vrBcFgtsGuia", _fmt_dec(folha.base_fgts)) + _sub(info_cp, "vrFgtsGuia", _fmt_dec(folha.valor_fgts)) + + ET.indent(root, space=" ") + return ET.tostring(root, encoding="unicode", xml_declaration=True) + + def gerar_s1299(self, periodo: str) -> str: + """S-1299: Fechamento dos eventos periódicos.""" + root = ET.Element("eSocial") + root.set("xmlns", NS_ESOCIAL) + + evt = _sub(root, "evtFechamento") + evt.set("Id", self._id_evento(1299)) + + self._ide_evento(evt, 1299, periodo) + self._ide_empregador(evt) + + _sub(evt, "evtRemun", "S") + _sub(evt, "evtPgtos", "N") + _sub(evt, "evtAqProd", "N") + _sub(evt, "evtComProd", "N") + _sub(evt, "evtContratAvNP", "N") + _sub(evt, "evtInfoComplPer", "N") + + ET.indent(root, space=" ") + return ET.tostring(root, encoding="unicode", xml_declaration=True) + + def salvar_folha( + self, + folha: FolhaPagamento, + diretorio: str | Path, + ) -> list[Path]: + d = Path(diretorio) + d.mkdir(parents=True, exist_ok=True) + arquivos = [] + + p = d / f"S1200_{self.cnpj}_{folha.periodo}_{re.sub(r'\\D', '', folha.trabalhador.cpf)}.xml" + p.write_text(self.gerar_s1200(folha), encoding="utf-8") + arquivos.append(p) + + return arquivos + + +# Rubricas padrão CLT +RUBRICAS_PADRAO: list[RubricaFolha] = [ + RubricaFolha("0001", "SALÁRIO BASE", "1", "11", "11", "11"), + RubricaFolha("0002", "HORA EXTRA 50%", "1", "11", "11", "11"), + RubricaFolha("0003", "HORA EXTRA 100%", "1", "11", "11", "11"), + RubricaFolha("0010", "ADICIONAL NOTURNO", "1", "11", "11", "11"), + RubricaFolha("0020", "INSALUBRIDADE", "1", "11", "11", "11"), + RubricaFolha("0030", "PERICULOSIDADE", "1", "11", "11", "11"), + RubricaFolha("0040", "VALE ALIMENTAÇÃO", "3", "00", "00", "00"), + RubricaFolha("0050", "VALE TRANSPORTE", "3", "00", "00", "00"), + RubricaFolha("0100", "DESCONTO INSS", "2", "00", "00", "00"), + RubricaFolha("0101", "DESCONTO IRRF", "2", "00", "00", "00"), + RubricaFolha("0102", "DESCONTO VT", "2", "00", "00", "00"), + RubricaFolha("0103", "DESCONTO VA/VR", "2", "00", "00", "00"), + RubricaFolha("0200", "13º SALÁRIO", "1", "11", "11", "11"), + RubricaFolha("0201", "FÉRIAS", "1", "11", "11", "00"), + RubricaFolha("0202", "1/3 FÉRIAS", "1", "11", "11", "00"), + RubricaFolha("0300", "AVISO PRÉVIO TRABALHADO", "1", "11", "11", "11"), +] diff --git a/src/generators/gia.py b/src/generators/gia.py new file mode 100644 index 0000000000000000000000000000000000000000..efdcc5e52cb7b8b33cdf51ec633845ff0c23844d --- /dev/null +++ b/src/generators/gia.py @@ -0,0 +1,203 @@ + +from __future__ import annotations + +import hashlib +from dataclasses import dataclass, field +from datetime import date +from decimal import Decimal +from pathlib import Path + +from src.fiscal.entities import Empresa + + +def _fmt(v: Decimal) -> str: + return f"{v:.2f}" + + +@dataclass +class ApuracaoGIA: + """Dados de apuração do ICMS para a GIA-SP.""" + + debitos_operacoes_proprias: Decimal = Decimal("0") + debitos_st_retencao: Decimal = Decimal("0") # ST retido nas saídas + estorno_credito: Decimal = Decimal("0") # estorno de créditos indevidos + outros_debitos: Decimal = Decimal("0") + + creditos_entradas: Decimal = Decimal("0") + creditos_outros: Decimal = Decimal("0") + estorno_debito: Decimal = Decimal("0") # estorno de débitos indevidos + outros_creditos: Decimal = Decimal("0") + + compensacoes: Decimal = Decimal("0") + + saldo_credor_anterior: Decimal = Decimal("0") + + +@dataclass +class ApuracaoGIAST: + """Dados de ICMS-ST por UF de origem para GIA-ST.""" + uf_substituto: str # UF do substituto tributário (remetente) + base_calculo: Decimal + aliquota: Decimal # % ex: Decimal("18") + imposto_retido: Decimal + valor_entradas_st: Decimal = Decimal("0") # ICMS-ST nas entradas (crédito) + + +class GeradorGIA: + + + VERSAO_LAYOUT = "013" + + def __init__( + self, + empresa: Empresa, + periodo: str, # "YYYY-MM" + apuracao: ApuracaoGIA, + apuracoes_st: list[ApuracaoGIAST] | None = None, + retificadora: bool = False, + ): + self.empresa = empresa + self.periodo = periodo + self.apuracao = apuracao + self.apuracoes_st = apuracoes_st or [] + self.retificadora = retificadora + + # ------------------------------------------------------------------ + # Cálculos derivados + # ------------------------------------------------------------------ + + @property + def total_debitos(self) -> Decimal: + a = self.apuracao + return a.debitos_operacoes_proprias + a.debitos_st_retencao + a.estorno_credito + a.outros_debitos + + @property + def total_creditos(self) -> Decimal: + a = self.apuracao + return (a.creditos_entradas + a.creditos_outros + + a.estorno_debito + a.outros_creditos + + a.saldo_credor_anterior) + + @property + def saldo_periodo(self) -> Decimal: + return self.total_debitos - self.total_creditos - self.apuracao.compensacoes + + @property + def icms_a_recolher(self) -> Decimal: + return max(Decimal("0"), self.saldo_periodo) + + @property + def saldo_credor_proximo(self) -> Decimal: + return max(Decimal("0"), -self.saldo_periodo) + + def data_vencimento(self) -> date: + ano, mes = map(int, self.periodo.split("-")) + if mes == 12: + return date(ano + 1, 1, 20) + return date(ano, mes + 1, 20) + + # ------------------------------------------------------------------ + # Geração do arquivo AIE + # ------------------------------------------------------------------ + + def gerar_aie(self) -> str: + ano, mes = self.periodo.split("-") + periodo_aie = f"{ano}{mes}" + hoje = date.today().strftime("%d%m%Y") + a = self.apuracao + ie = self.empresa.ie or "" + cnpj = self.empresa.cnpj + razao = self.empresa.razao_social[:40] + municipio = self.empresa.endereco.municipio[:30] if self.empresa.endereco else "SAO PAULO" + cep = self.empresa.endereco.cep if self.empresa.endereco else "01310100" + + linhas: list[str] = [] + + + linhas.append(f"|GIA|{self.VERSAO_LAYOUT}|{cnpj}|{periodo_aie}|{hoje}|{'S' if self.retificadora else 'N'}|") + + + linhas.append(f"|IDENTIFICACAO|{ie}|{razao}|{municipio}|{cep}|{ie}|") + + + linhas.append( + f"|APURACAO_ICMS" + f"|{_fmt(a.debitos_operacoes_proprias)}" + f"|{_fmt(a.debitos_st_retencao)}" + f"|{_fmt(a.estorno_credito)}" + f"|{_fmt(a.outros_debitos)}" + f"|{_fmt(self.total_debitos)}" + f"|{_fmt(a.creditos_entradas)}" + f"|{_fmt(a.creditos_outros)}" + f"|{_fmt(a.estorno_debito)}" + f"|{_fmt(a.outros_creditos)}" + f"|{_fmt(self.total_creditos)}" + f"|{_fmt(a.saldo_credor_anterior)}" + f"|{_fmt(a.compensacoes)}" + f"|{_fmt(self.icms_a_recolher)}" + f"|{_fmt(self.saldo_credor_proximo)}" + f"|" + ) + + + total_retido_st = Decimal("0") + total_entradas_st = Decimal("0") + for st in self.apuracoes_st: + saldo_st = st.imposto_retido - st.valor_entradas_st + linhas.append( + f"|GIA_ST" + f"|{st.uf_substituto}" + f"|{_fmt(st.base_calculo)}" + f"|{_fmt(st.aliquota)}" + f"|{_fmt(st.imposto_retido)}" + f"|{_fmt(st.valor_entradas_st)}" + f"|{_fmt(saldo_st)}" + f"|" + ) + total_retido_st += st.imposto_retido + total_entradas_st += st.valor_entradas_st + + if self.apuracoes_st: + saldo_total_st = total_retido_st - total_entradas_st + linhas.append( + f"|TOTAIS_GIA_ST" + f"|{_fmt(total_retido_st)}" + f"|{_fmt(total_entradas_st)}" + f"|{_fmt(saldo_total_st)}" + f"|" + ) + + + conteudo_sem_encerr = "\n".join(linhas) + hash_md5 = hashlib.md5(conteudo_sem_encerr.encode("utf-8")).hexdigest().upper() + linhas.append(f"|ENCERRAMENTO|{len(linhas) + 1}|{hash_md5}|") + + return "\n".join(linhas) + "\n" + + def salvar(self, diretorio: str | Path = ".") -> Path: + caminho = Path(diretorio) / f"GIA_{self.empresa.cnpj}_{self.periodo}.aie" + caminho.parent.mkdir(parents=True, exist_ok=True) + caminho.write_text(self.gerar_aie(), encoding="utf-8") + return caminho + + def relatorio_resumo(self) -> str: + venc = self.data_vencimento() + linhas = [ + f"GIA-SP — {self.empresa.razao_social} — Período: {self.periodo}", + "=" * 60, + f"Total débitos: R$ {self.total_debitos:>12,.2f}", + f"Total créditos: R$ {self.total_creditos:>12,.2f}", + f"Compensações: R$ {self.apuracao.compensacoes:>12,.2f}", + "=" * 60, + ] + if self.icms_a_recolher > 0: + linhas.append(f"ICMS A RECOLHER: R$ {self.icms_a_recolher:>12,.2f}") + else: + linhas.append(f"SALDO CREDOR: R$ {self.saldo_credor_proximo:>12,.2f}") + + if self.apuracoes_st: + total_st = sum(s.imposto_retido for s in self.apuracoes_st) + linhas.append(f"ICMS-ST retido: R$ {total_st:>12,.2f}") + + linhas.append(f"Vencimento: {venc.strftime('%d/%m/%Y')}") + return "\n".join(linhas) diff --git a/src/generators/mdfe.py b/src/generators/mdfe.py new file mode 100644 index 0000000000000000000000000000000000000000..6371bc98339e1c289abf27d1a79286f331478279 --- /dev/null +++ b/src/generators/mdfe.py @@ -0,0 +1,271 @@ + +from __future__ import annotations + +import hashlib +import random +import re +from dataclasses import dataclass, field +from datetime import datetime, timezone +from pathlib import Path +from xml.etree import ElementTree as ET + +from src.fiscal.entities import Empresa + +NS_MDFE = "http://www.portalfiscal.inf.br/mdfe" + +_CUF_MAP = { + "AC": "12", "AL": "27", "AP": "16", "AM": "13", "BA": "29", + "CE": "23", "DF": "53", "ES": "32", "GO": "52", "MA": "21", + "MT": "51", "MS": "50", "MG": "31", "PA": "15", "PB": "25", + "PR": "41", "PE": "26", "PI": "22", "RJ": "33", "RN": "24", + "RS": "43", "RO": "11", "RR": "14", "SC": "42", "SP": "35", + "SE": "28", "TO": "17", +} + + +def _sub(pai: ET.Element, tag: str, texto: str = "") -> ET.Element: + el = ET.SubElement(pai, tag) + if texto: + el.text = str(texto) + return el + + +def _limpar(s: str) -> str: + return re.sub(r"[^\w\s\-.,/@]", "", str(s))[:60] + + +def _cuf(uf: str) -> str: + return _CUF_MAP.get(uf.upper(), "35") + + +@dataclass +class DocumentoMDFe: + """Referência a CT-e ou NF-e incluído no MDF-e.""" + chave: str # 44 dígitos + tipo: str # "CTe" ou "NFe" + + +@dataclass +class MunicipioDescarga: + cod_municipio: str + nome_municipio: str + uf: str + documentos: list[DocumentoMDFe] = field(default_factory=list) + + +@dataclass +class ConductorMDFe: + nome: str + cpf: str + + +@dataclass +class SeguroMDFe: + resp_seg: str # "1"=emitente, "2"=contratante + cnpj_seg: str # CNPJ seguradora + nome_seg: str + nApol: str # número apólice + nCT: list[str] = field(default_factory=list) # números averbação + + +class GeradorMDFe: + + + VERSAO = "3.00" + MOD = "58" + + def __init__( + self, + emitente: Empresa, + municipios_descarrega: list[MunicipioDescarga], + condutores: list[ConductorMDFe], + seguro: SeguroMDFe, + uf_ini: str, + uf_fim: str, + placa_veiculo: str, + uf_veiculo: str, + rntrc: str, + numero: str = "1", + serie: str = "1", + ambiente: str = "2", + ): + self.emitente = emitente + self.municipios_descarrega = municipios_descarrega + self.condutores = condutores + self.seguro = seguro + self.uf_ini = uf_ini.upper() + self.uf_fim = uf_fim.upper() + self.placa_veiculo = placa_veiculo.upper() + self.uf_veiculo = uf_veiculo.upper() + self.rntrc = rntrc + self.numero = numero + self.serie = serie + self.ambiente = ambiente # 1=Produção, 2=Homologação + self._dhEmi = datetime.now(tz=timezone.utc).astimezone().strftime("%Y-%m-%dT%H:%M:%S") + "-03:00" + + # ------------------------------------------------------------------ + # Chave de acesso + # ------------------------------------------------------------------ + + def _gerar_chave(self) -> str: + """Gera a chave de acesso de 44 dígitos do MDF-e (módulo 11).""" + uf = _cuf(self.emitente.endereco.uf.value) + aamm = datetime.now().strftime("%y%m") + cnpj = self.emitente.cnpj.zfill(14) + mod = self.MOD.zfill(2) + serie = self.serie.zfill(3) + numero = self.numero.zfill(9) + tp_emis = "1" # Emissão normal + + # cMDF — 8 dígitos aleatórios + c_mdf = str(random.randint(10000000, 99999999)) + + chave_sem_dv = f"{uf}{aamm}{cnpj}{mod}{serie}{numero}{tp_emis}{c_mdf}" + + # Módulo 11 para dígito verificador (mesmo algoritmo da NF-e) + pesos = [2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, + 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4] + soma = sum(int(chave_sem_dv[i]) * pesos[i] for i in range(43)) + resto = soma % 11 + dv = 0 if resto in (0, 1) else 11 - resto + + return chave_sem_dv + str(dv) + + # ------------------------------------------------------------------ + # Blocos XML + # ------------------------------------------------------------------ + + def _ide(self, inf_mdfe: ET.Element, chave: str) -> None: + ide = _sub(inf_mdfe, "ide") + _sub(ide, "cUF", _cuf(self.emitente.endereco.uf.value)) + _sub(ide, "tpAmb", self.ambiente) + _sub(ide, "tpEmit", "1") # 1=Transportador + _sub(ide, "tpTransp", "2") # 2=Carga própria + _sub(ide, "mod", self.MOD) + _sub(ide, "serie", self.serie.zfill(3)) + _sub(ide, "nMDF", self.numero.zfill(9)) + _sub(ide, "cMDF", chave[35:43]) + _sub(ide, "cDV", chave[-1]) + _sub(ide, "modal", "01") # 01=Rodoviário + _sub(ide, "dhEmi", self._dhEmi) + _sub(ide, "tpEmis", "1") # 1=Emissão normal + _sub(ide, "procEmi", "0") # 0=Aplicativo do contribuinte + _sub(ide, "verProc", "1.0.0") + _sub(ide, "UFIni", self.uf_ini) + _sub(ide, "UFFim", self.uf_fim) + _sub(ide, "dhIniViagem", self._dhEmi) + + def _emit(self, inf_mdfe: ET.Element) -> None: + emp = self.emitente + end = emp.endereco + emit = _sub(inf_mdfe, "emit") + _sub(emit, "CNPJ", emp.cnpj.zfill(14)) + if emp.ie: + _sub(emit, "IE", re.sub(r"\D", "", emp.ie)) + _sub(emit, "xNome", _limpar(emp.razao_social)[:60]) + ender = _sub(emit, "enderEmit") + _sub(ender, "xLgr", _limpar(end.logradouro)) + _sub(ender, "nro", end.numero) + _sub(ender, "xBairro", _limpar(end.bairro)) + _sub(ender, "cMun", end.cod_municipio or "3550308") + _sub(ender, "xMun", _limpar(end.municipio)) + _sub(ender, "CEP", re.sub(r"\D", "", end.cep)) + _sub(ender, "UF", end.uf.value) + _sub(ender, "cPais", "1058") + _sub(ender, "xPais", "Brasil") + if emp.contato.telefone: + _sub(ender, "fone", re.sub(r"\D", "", emp.contato.telefone)) + + def _inf_modal(self, inf_mdfe: ET.Element) -> None: + inf_modal = _sub(inf_mdfe, "infModal") + inf_modal.set("versaoModal", self.VERSAO) + rodo = _sub(inf_modal, "rodo") + _sub(rodo, "RNTRC", self.rntrc) + veic = _sub(rodo, "veicTracao") + _sub(veic, "cInt", "") + _sub(veic, "placa", self.placa_veiculo) + _sub(veic, "tara", "0") + _sub(veic, "tpRod", "06") # 06=Caminhão + _sub(veic, "tpCar", "00") # 00=Não aplicável + _sub(veic, "UF", self.uf_veiculo) + for condutor in self.condutores: + cond_el = _sub(rodo, "condutor") + _sub(cond_el, "xNome", _limpar(condutor.nome)[:60]) + _sub(cond_el, "CPF", re.sub(r"\D", "", condutor.cpf).zfill(11)) + + def _inf_doc(self, inf_mdfe: ET.Element) -> None: + inf_doc = _sub(inf_mdfe, "infDoc") + for mun in self.municipios_descarrega: + mun_el = _sub(inf_doc, "infMunDescarga") + _sub(mun_el, "cMunDescarga", mun.cod_municipio) + _sub(mun_el, "xMunDescarga", _limpar(mun.nome_municipio)[:60]) + for doc in mun.documentos: + if doc.tipo == "CTe": + doc_el = _sub(mun_el, "infCTe") + _sub(doc_el, "chCTe", doc.chave) + else: + doc_el = _sub(mun_el, "infNFe") + _sub(doc_el, "chNFe", doc.chave) + + def _seg(self, inf_mdfe: ET.Element) -> None: + seg = self.seguro + seg_el = _sub(inf_mdfe, "seg") + _sub(seg_el, "respSeg", seg.resp_seg) + _sub(seg_el, "xSeg", _limpar(seg.nome_seg)[:60]) + _sub(seg_el, "CNPJ", re.sub(r"\D", "", seg.cnpj_seg).zfill(14)) + _sub(seg_el, "nApol", seg.nApol[:20]) + for numero_ct in seg.nCT: + _sub(seg_el, "nCT", numero_ct[:20]) + inf_carga = _sub(seg_el, "infCarga") + _sub(inf_carga, "xProd", "CARGA GERAL") + + def _tot(self, inf_mdfe: ET.Element) -> None: + todos_docs = [d for m in self.municipios_descarrega for d in m.documentos] + q_cte = sum(1 for d in todos_docs if d.tipo == "CTe") + q_nfe = sum(1 for d in todos_docs if d.tipo == "NFe") + tot = _sub(inf_mdfe, "tot") + _sub(tot, "qCTe", str(q_cte)) + _sub(tot, "qNFe", str(q_nfe)) + _sub(tot, "qMDFe", "0") + _sub(tot, "vCarga", "0.00") + _sub(tot, "cUnid", "01") # 01=KG + _sub(tot, "qCarga", "0.0000") + + # ------------------------------------------------------------------ + # Geração e persistência + # ------------------------------------------------------------------ + + def gerar_xml(self) -> str: + + chave = self._gerar_chave() + + raiz = ET.Element("mdfeProc") + raiz.set("xmlns", NS_MDFE) + raiz.set("versao", self.VERSAO) + + mdfe = _sub(raiz, "MDFe") + mdfe.set("xmlns", NS_MDFE) + + inf_mdfe = _sub(mdfe, "infMDFe") + inf_mdfe.set("versao", self.VERSAO) + inf_mdfe.set("Id", f"MDFe{chave}") + + self._ide(inf_mdfe, chave) + self._emit(inf_mdfe) + self._inf_modal(inf_mdfe) + self._inf_doc(inf_mdfe) + self._seg(inf_mdfe) + self._tot(inf_mdfe) + + + _sub(inf_mdfe, "Signature") + + ET.indent(raiz, space=" ") + return '\n' + ET.tostring(raiz, encoding="unicode") + + def salvar(self, diretorio: str | Path = ".") -> Path: + + caminho = Path(diretorio) / f"MDFe_{self.emitente.cnpj}_{self.numero.zfill(9)}.xml" + caminho.parent.mkdir(parents=True, exist_ok=True) + caminho.write_text(self.gerar_xml(), encoding="utf-8") + return caminho diff --git a/src/generators/nfce_xml.py b/src/generators/nfce_xml.py new file mode 100644 index 0000000000000000000000000000000000000000..d1dee32d03584a3e0b91cd12220c282129d120cc --- /dev/null +++ b/src/generators/nfce_xml.py @@ -0,0 +1,401 @@ + +from __future__ import annotations + +import hashlib +import re +from dataclasses import dataclass +from datetime import date, datetime +from decimal import Decimal +from pathlib import Path +from xml.etree import ElementTree as ET + +from src.fiscal.entities import Empresa, ItemNotaFiscal + +NS_NFE = "http://www.portalfiscal.inf.br/nfe" +VERSAO_NFCE = "4.00" + + +def _sub(pai: ET.Element, tag: str, texto: str = "") -> ET.Element: + el = ET.SubElement(pai, tag) + if texto: + el.text = str(texto) + return el + + +def _fmt_dec(v: Decimal, casas: int = 2) -> str: + return f"{v:.{casas}f}" + + +def _limpar(s: str) -> str: + return re.sub(r"[^\w\s\-.,/@]", "", str(s))[:60] + + +# --------------------------------------------------------------------------- +# Dataclass auxiliar +# --------------------------------------------------------------------------- + +@dataclass +class ConsumidorNFCe: + """Destinatário opcional da NFC-e.""" + cpf: str = "" # pode ser vazio (sem identificação) + nome: str = "CONSUMIDOR" + uf: str = "SP" + + +# --------------------------------------------------------------------------- +# Gerador principal +# --------------------------------------------------------------------------- + +class GeradorNFCeXML: + """ + Gera o XML de NFC-e conforme o leiaute 4.00 da SEFAZ. + + O XML gerado deve ser assinado digitalmente com certificado ICP-Brasil + antes da transmissão. + """ + + VERSAO = "4.00" + MOD = "65" + + _CUF_MAP = { + "AC": "12", "AL": "27", "AP": "16", "AM": "13", "BA": "29", + "CE": "23", "DF": "53", "ES": "32", "GO": "52", "MA": "21", + "MT": "51", "MS": "50", "MG": "31", "PA": "15", "PB": "25", + "PR": "41", "PE": "26", "PI": "22", "RJ": "33", "RN": "24", + "RS": "43", "RO": "11", "RR": "14", "SC": "42", "SP": "35", + "SE": "28", "TO": "17", + } + + def __init__( + self, + emitente: Empresa, + itens: list[ItemNotaFiscal], + consumidor: ConsumidorNFCe | None = None, + numero: str = "1", + serie: str = "001", + forma_pagamento: str = "01", # 01=dinheiro, 03=cartão crédito, 04=cartão débito + valor_pagamento: Decimal | None = None, + ambiente: str = "2", + url_qrcode: str = "", + ): + self.emitente = emitente + self.itens = itens + self.consumidor = consumidor or ConsumidorNFCe() + self.numero = numero + self.serie = serie + self.forma_pagamento = forma_pagamento + self._valor_pagamento = valor_pagamento + self.ambiente = ambiente # 1=Produção, 2=Homologação + self.url_qrcode = url_qrcode + self._data_emissao: date = date.today() + + # ------------------------------------------------------------------ + # Utilitários internos + # ------------------------------------------------------------------ + + def _cuf(self, uf: str) -> str: + return self._CUF_MAP.get(uf.upper(), "35") + + @property + def _valor_total(self) -> Decimal: + return sum(i.valor_total for i in self.itens) + + @property + def _valor_produtos(self) -> Decimal: + return sum(i.valor_produtos for i in self.itens) + + @property + def _valor_icms(self) -> Decimal: + return sum(i.valor_icms for i in self.itens) + + @property + def _valor_ipi(self) -> Decimal: + return sum(i.valor_ipi for i in self.itens) + + @property + def valor_pagamento(self) -> Decimal: + return self._valor_pagamento if self._valor_pagamento is not None else self._valor_total + + def _gerar_chave(self) -> str: + """Gera a chave de acesso de 44 dígitos da NFC-e (modelo 65).""" + uf = self._cuf(self.emitente.endereco.uf.value) + aamm = self._data_emissao.strftime("%y%m") + cnpj = self.emitente.cnpj.zfill(14) + mod = self.MOD.zfill(2) + serie = self.serie.zfill(3) + numero = self.numero.zfill(9) + tp_emis = "1" # Emissão normal + c_nf = hashlib.md5(f"{cnpj}{numero}".encode()).hexdigest()[:8].upper() + c_nf_digits = "".join(str(int(c, 16) % 10) for c in c_nf)[:8] + + chave_sem_dv = f"{uf}{aamm}{cnpj}{mod}{serie}{numero}{tp_emis}{c_nf_digits}" + + # Módulo 11 para dígito verificador + pesos = [2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, + 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4] + soma = sum(int(chave_sem_dv[i]) * pesos[i] for i in range(43)) + resto = soma % 11 + dv = 0 if resto in (0, 1) else 11 - resto + + return chave_sem_dv + str(dv) + + def _fmt_dhemi(self) -> str: + return ( + datetime.combine(self._data_emissao, datetime.min.time()) + .strftime("%Y-%m-%dT%H:%M:%S") + "-03:00" + ) + + # ------------------------------------------------------------------ + # Blocos XML + # ------------------------------------------------------------------ + + def _ide(self, inf_nfe: ET.Element, chave: str) -> None: + emp = self.emitente + ide = _sub(inf_nfe, "ide") + _sub(ide, "cUF", self._cuf(emp.endereco.uf.value)) + _sub(ide, "cNF", chave[35:43]) + _sub(ide, "natOp", "VENDA A CONSUMIDOR") + _sub(ide, "mod", self.MOD) + _sub(ide, "serie", self.serie.zfill(3)) + _sub(ide, "nNF", self.numero.zfill(9)) + _sub(ide, "dhEmi", self._fmt_dhemi()) + _sub(ide, "tpNF", "1") # 1=Saída + _sub(ide, "idDest", "1") # 1=Operação interna + _sub(ide, "cMunFG", emp.endereco.cod_municipio or "3550308") + _sub(ide, "tpImp", "4") # 4=DANFE NFC-e + _sub(ide, "tpEmis", "1") # 1=Emissão normal + _sub(ide, "cDV", chave[-1]) + _sub(ide, "tpAmb", self.ambiente) + _sub(ide, "finNFe", "1") # 1=NF-e normal + _sub(ide, "indFinal", "1") # 1=Consumidor final + _sub(ide, "indPres", "1") # 1=Operação presencial + _sub(ide, "procEmi", "0") # 0=App do contribuinte + _sub(ide, "verProc", "1.0.0") + + def _emit(self, inf_nfe: ET.Element) -> None: + emp = self.emitente + end = emp.endereco + emit = _sub(inf_nfe, "emit") + _sub(emit, "CNPJ", emp.cnpj) + _sub(emit, "xNome", _limpar(emp.razao_social)[:60]) + if emp.nome_fantasia: + _sub(emit, "xFant", _limpar(emp.nome_fantasia)[:60]) + ender_emit = _sub(emit, "enderEmit") + _sub(ender_emit, "xLgr", _limpar(end.logradouro)) + _sub(ender_emit, "nro", end.numero) + if end.complemento: + _sub(ender_emit, "xCpl", _limpar(end.complemento)) + _sub(ender_emit, "xBairro", _limpar(end.bairro)) + _sub(ender_emit, "cMun", end.cod_municipio or "3550308") + _sub(ender_emit, "xMun", _limpar(end.municipio)) + _sub(ender_emit, "UF", end.uf.value) + _sub(ender_emit, "CEP", re.sub(r"\D", "", end.cep)) + _sub(ender_emit, "cPais", "1058") + _sub(ender_emit, "xPais", "BRASIL") + if emp.contato.telefone: + _sub(ender_emit, "fone", re.sub(r"\D", "", emp.contato.telefone)) + if emp.ie: + _sub(emit, "IE", re.sub(r"\D", "", emp.ie)) + _sub(emit, "CRT", emp.regime_tributario.value[:1]) + + def _dest(self, inf_nfe: ET.Element) -> None: + """Destinatário: omitido se sem CPF; incluído com CPF se identificado.""" + cpf = re.sub(r"\D", "", self.consumidor.cpf) + if not cpf: + return # NFC-e sem identificação do consumidor + dest = _sub(inf_nfe, "dest") + _sub(dest, "CPF", cpf.zfill(11)) + _sub(dest, "xNome", _limpar(self.consumidor.nome)[:60]) + _sub(dest, "indIEDest", "9") # 9=Não contribuinte + + def _det(self, inf_nfe: ET.Element) -> None: + for item in self.itens: + det = _sub(inf_nfe, "det") + det.set("nItem", str(item.numero_item)) + + prod = _sub(det, "prod") + _sub(prod, "cProd", item.produto.codigo[:60]) + _sub(prod, "cEAN", item.produto.cod_barras or "SEM GTIN") + _sub(prod, "xProd", _limpar(item.produto.descricao)[:120]) + _sub(prod, "NCM", re.sub(r"\D", "", item.produto.ncm)[:8]) + if item.produto.cest: + _sub(prod, "CEST", re.sub(r"\D", "", item.produto.cest)) + _sub(prod, "CFOP", item.cfop) + _sub(prod, "uCom", item.produto.unidade[:6]) + _sub(prod, "qCom", _fmt_dec(item.quantidade, 4)) + _sub(prod, "vUnCom", _fmt_dec(item.valor_unitario, 10)) + _sub(prod, "vProd", _fmt_dec(item.valor_produtos)) + _sub(prod, "cEANTrib", item.produto.cod_barras or "SEM GTIN") + _sub(prod, "uTrib", item.produto.unidade[:6]) + _sub(prod, "qTrib", _fmt_dec(item.quantidade, 4)) + _sub(prod, "vUnTrib", _fmt_dec(item.valor_unitario, 10)) + if item.desconto > 0: + _sub(prod, "vDesc", _fmt_dec(item.desconto)) + _sub(prod, "indTot", "1") + + # Impostos + imposto = _sub(det, "imposto") + self._icms_item(imposto, item) + self._pis_item(imposto, item) + self._cofins_item(imposto, item) + + def _icms_item(self, imposto: ET.Element, item: ItemNotaFiscal) -> None: + icms = _sub(imposto, "ICMS") + cst = item.produto.cst_icms + if cst in {"000"}: + grupo = _sub(icms, "ICMS00") + _sub(grupo, "orig", "0") + _sub(grupo, "CST", cst) + _sub(grupo, "modBC", "3") + _sub(grupo, "vBC", _fmt_dec(item.base_icms)) + _sub(grupo, "pICMS", _fmt_dec(item.produto.aliq_icms, 4)) + _sub(grupo, "vICMS", _fmt_dec(item.valor_icms)) + elif cst in {"020"}: + grupo = _sub(icms, "ICMS20") + _sub(grupo, "orig", "0") + _sub(grupo, "CST", cst) + _sub(grupo, "modBC", "3") + _sub(grupo, "pRedBC", _fmt_dec(Decimal("33.33"), 4)) + _sub(grupo, "vBC", _fmt_dec(item.base_icms)) + _sub(grupo, "pICMS", _fmt_dec(item.produto.aliq_icms, 4)) + _sub(grupo, "vICMS", _fmt_dec(item.valor_icms)) + elif cst in {"040", "041", "050"}: + grupo = _sub(icms, f"ICMS{cst}") + _sub(grupo, "orig", "0") + _sub(grupo, "CST", cst) + elif cst.startswith("1"): # Simples Nacional + grupo = _sub(icms, "ICMSSN102") + _sub(grupo, "orig", "0") + _sub(grupo, "CSOSN", cst) + else: + grupo = _sub(icms, "ICMS90") + _sub(grupo, "orig", "0") + _sub(grupo, "CST", "090") + _sub(grupo, "modBC", "3") + _sub(grupo, "vBC", _fmt_dec(item.base_icms)) + _sub(grupo, "pICMS", _fmt_dec(item.produto.aliq_icms, 4)) + _sub(grupo, "vICMS", _fmt_dec(item.valor_icms)) + + def _pis_item(self, imposto: ET.Element, item: ItemNotaFiscal) -> None: + pis = _sub(imposto, "PIS") + cst = item.produto.cst_pis + if cst in {"07", "08", "09"}: + pisnt = _sub(pis, "PISNT") + _sub(pisnt, "CST", cst) + else: + pisaliq = _sub(pis, "PISAliq") + _sub(pisaliq, "CST", cst) + _sub(pisaliq, "vBC", _fmt_dec(item.valor_produtos)) + _sub(pisaliq, "pPIS", _fmt_dec(Decimal("0.65"), 4)) + _sub(pisaliq, "vPIS", _fmt_dec(Decimal("0"))) + + def _cofins_item(self, imposto: ET.Element, item: ItemNotaFiscal) -> None: + cofins = _sub(imposto, "COFINS") + cst = item.produto.cst_cofins + if cst in {"07", "08", "09"}: + cofinsnt = _sub(cofins, "COFINSNT") + _sub(cofinsnt, "CST", cst) + else: + cofinsaliq = _sub(cofins, "COFINSAliq") + _sub(cofinsaliq, "CST", cst) + _sub(cofinsaliq, "vBC", _fmt_dec(item.valor_produtos)) + _sub(cofinsaliq, "pCOFINS", _fmt_dec(Decimal("3.00"), 4)) + _sub(cofinsaliq, "vCOFINS", _fmt_dec(Decimal("0"))) + + def _total(self, inf_nfe: ET.Element) -> None: + total = _sub(inf_nfe, "total") + ict = _sub(total, "ICMSTot") + _sub(ict, "vBC", _fmt_dec(sum(i.base_icms for i in self.itens))) + _sub(ict, "vICMS", _fmt_dec(self._valor_icms)) + _sub(ict, "vICMSDeson", "0.00") + _sub(ict, "vFCPUFDest", "0.00") + _sub(ict, "vICMSUFDest", "0.00") + _sub(ict, "vICMSUFRemet", "0.00") + _sub(ict, "vFCP", "0.00") + _sub(ict, "vBCST", "0.00") + _sub(ict, "vST", "0.00") + _sub(ict, "vFCPST", "0.00") + _sub(ict, "vFCPSTRet", "0.00") + _sub(ict, "vProd", _fmt_dec(self._valor_produtos)) + _sub(ict, "vFrete", "0.00") + _sub(ict, "vSeg", "0.00") + _sub(ict, "vDesc", _fmt_dec(sum(i.desconto for i in self.itens))) + _sub(ict, "vII", "0.00") + _sub(ict, "vIPI", _fmt_dec(self._valor_ipi)) + _sub(ict, "vIPIDevol", "0.00") + _sub(ict, "vPIS", "0.00") + _sub(ict, "vCOFINS", "0.00") + _sub(ict, "vOutro", "0.00") + _sub(ict, "vNF", _fmt_dec(self._valor_total)) + _sub(ict, "vTotTrib", "0.00") + + def _transp(self, inf_nfe: ET.Element) -> None: + transp = _sub(inf_nfe, "transp") + _sub(transp, "modFrete", "9") # 9=Sem frete (NFC-e é balcão) + + def _pag(self, inf_nfe: ET.Element) -> None: + pag = _sub(inf_nfe, "pag") + det_pag = _sub(pag, "detPag") + _sub(det_pag, "tPag", self.forma_pagamento) + _sub(det_pag, "vPag", _fmt_dec(self.valor_pagamento)) + + def _inf_nfe_supl(self, nfe: ET.Element, chave: str) -> None: + """ + Bloco suplementar obrigatório na NFC-e. + Contém QR Code e URL de consulta. + """ + # Digest e signature simplificados (placeholder sem assinatura real) + digest_hex = hashlib.sha256(chave.encode()).hexdigest()[:32] + signature_hex = hashlib.sha256((chave + "sig").encode()).hexdigest()[:32] + + base_url = self.url_qrcode or "https://www.sefaz.rs.gov.br/NFCE/NFCE-com.aspx" + qr_url = f"{base_url}?p={chave}|2|1|{digest_hex}|{signature_hex}" + url_consulta = "https://www.sefaz.rs.gov.br/NFCE/NFCE-com.aspx" + + supl = _sub(nfe, "infNFeSupl") + _sub(supl, "qrCode", qr_url) + _sub(supl, "urlChave", url_consulta) + + # ------------------------------------------------------------------ + # API pública + # ------------------------------------------------------------------ + + def gerar_xml(self) -> str: + """Retorna o XML da NFC-e não assinado como string.""" + chave = self._gerar_chave() + + nfe_proc = ET.Element("nfeProc") + nfe_proc.set("xmlns", NS_NFE) + nfe_proc.set("versao", self.VERSAO) + + nfe = _sub(nfe_proc, "NFe") + nfe.set("xmlns", NS_NFE) + + inf_nfe = _sub(nfe, "infNFe") + inf_nfe.set("versao", self.VERSAO) + inf_nfe.set("Id", f"NFe{chave}") + + self._ide(inf_nfe, chave) + self._emit(inf_nfe) + self._dest(inf_nfe) # omitido automaticamente se sem CPF + self._det(inf_nfe) + self._total(inf_nfe) + self._transp(inf_nfe) + self._pag(inf_nfe) + + # Placeholder de assinatura (deve vir antes de infNFeSupl no XML final) + _sub(nfe, "Signature") + + # infNFeSupl é obrigatório na NFC-e + self._inf_nfe_supl(nfe, chave) + + ET.indent(nfe_proc, space=" ") + return '\n' + ET.tostring(nfe_proc, encoding="unicode") + + def salvar(self, diretorio: str | Path = ".") -> Path: + """Salva o XML em diretório informado e retorna o caminho do arquivo.""" + chave = self._gerar_chave() + caminho = Path(diretorio) / f"NFCe{chave}.xml" + caminho.parent.mkdir(parents=True, exist_ok=True) + caminho.write_text(self.gerar_xml(), encoding="utf-8") + return caminho diff --git a/src/generators/nfe_xml.py b/src/generators/nfe_xml.py new file mode 100644 index 0000000000000000000000000000000000000000..e2e1d42660799064f2daa1488f45d752f1cf4484 --- /dev/null +++ b/src/generators/nfe_xml.py @@ -0,0 +1,452 @@ + +from __future__ import annotations + +import hashlib +import re +from datetime import datetime, timezone +from decimal import Decimal +from pathlib import Path +from typing import Optional +from xml.etree import ElementTree as ET + +from src.fiscal.entities import Empresa, ItemNotaFiscal, NotaFiscal + +NS_NFE = "http://www.portalfiscal.inf.br/nfe" +VERSAO_NFE = "4.00" + + +def _sub(pai: ET.Element, tag: str, texto: str = "") -> ET.Element: + el = ET.SubElement(pai, tag) + if texto: + el.text = str(texto) + return el + + +def _fmt_dec(v: Decimal, casas: int = 2) -> str: + return f"{v:.{casas}f}" + + +def _limpar(s: str) -> str: + return re.sub(r"[^\w\s\-.,/@]", "", str(s))[:60] + + +class GeradorNFeXML: + + def __init__(self, nf: NotaFiscal, ambiente: str = "2"): + self.nf = nf + self.ambiente = ambiente # 1=Produção, 2=Homologação + self._cuf_map = { + "AC": "12", "AL": "27", "AP": "16", "AM": "13", "BA": "29", + "CE": "23", "DF": "53", "ES": "32", "GO": "52", "MA": "21", + "MT": "51", "MS": "50", "MG": "31", "PA": "15", "PB": "25", + "PR": "41", "PE": "26", "PI": "22", "RJ": "33", "RN": "24", + "RS": "43", "RO": "11", "RR": "14", "SC": "42", "SP": "35", + "SE": "28", "TO": "17", + } + + def _cuf(self, uf: str) -> str: + return self._cuf_map.get(uf.upper(), "35") + + def _gerar_chave(self) -> str: + """Gera a chave de acesso de 44 dígitos da NF-e.""" + nf = self.nf + uf = self._cuf(nf.emitente.endereco.uf.value) + aamm = nf.data_emissao.strftime("%y%m") + cnpj = nf.emitente.cnpj.zfill(14) + mod = nf.modelo.zfill(2) + serie = nf.serie.zfill(3) + numero = nf.numero.zfill(9) + tp_emis = "1" # Emissão normal + c_nf = hashlib.md5(f"{cnpj}{numero}".encode()).hexdigest()[:8].upper() + c_nf_digits = "".join(str(int(c, 16) % 10) for c in c_nf)[:8] + + chave_sem_dv = f"{uf}{aamm}{cnpj}{mod}{serie}{numero}{tp_emis}{c_nf_digits}" + + + pesos = [2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, + 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4] + soma = sum(int(chave_sem_dv[i]) * pesos[i] for i in range(43)) + resto = soma % 11 + dv = 0 if resto in (0, 1) else 11 - resto + + return chave_sem_dv + str(dv) + + def _ide(self, root_nfe: ET.Element, chave: str) -> None: + nf = self.nf + ide = _sub(root_nfe, "ide") + _sub(ide, "cUF", self._cuf(nf.emitente.endereco.uf.value)) + _sub(ide, "cNF", chave[35:43]) + _sub(ide, "natOp", _limpar(nf.natureza_operacao)) + _sub(ide, "mod", nf.modelo) + _sub(ide, "serie", nf.serie.zfill(3)) + _sub(ide, "nNF", nf.numero.zfill(9)) + _sub(ide, "dhEmi", datetime.combine(nf.data_emissao, datetime.min.time()).strftime("%Y-%m-%dT%H:%M:%S") + "-03:00") + _sub(ide, "dhSaiEnt", datetime.combine(nf.data_saida_entrada, datetime.min.time()).strftime("%Y-%m-%dT%H:%M:%S") + "-03:00") + _sub(ide, "tpNF", nf.tipo_operacao) + _sub(ide, "idDest", "1") # 1=Operação interna + _sub(ide, "cMunFG", nf.emitente.endereco.cod_municipio or "3550308") + _sub(ide, "tpImp", "1") # 1=DANFE normal retrato + _sub(ide, "tpEmis", "1") # 1=Emissão normal + _sub(ide, "cDV", chave[-1]) + _sub(ide, "tpAmb", self.ambiente) + _sub(ide, "finNFe", "1") # 1=NF-e normal + _sub(ide, "indFinal", "0") # 0=Normal + _sub(ide, "indPres", nf.ind_pagamento) + _sub(ide, "procEmi", "0") # 0=Emissão de NF-e com aplicativo do contribuinte + _sub(ide, "verProc", "1.0.0") + + def _endereco(self, pai: ET.Element, emp: Empresa, prefixo: str = "") -> None: + end = emp.endereco + ender = _sub(pai, f"ender{prefixo}") + _sub(ender, "xLgr", _limpar(end.logradouro)) + _sub(ender, "nro", end.numero) + if end.complemento: + _sub(ender, "xCpl", _limpar(end.complemento)) + _sub(ender, "xBairro", _limpar(end.bairro)) + _sub(ender, "cMun", end.cod_municipio or "3550308") + _sub(ender, "xMun", _limpar(end.municipio)) + _sub(ender, "UF", end.uf.value) + _sub(ender, "CEP", re.sub(r"\D", "", end.cep)) + _sub(ender, "cPais", "1058") + _sub(ender, "xPais", "BRASIL") + if emp.contato.telefone: + _sub(ender, "fone", re.sub(r"\D", "", emp.contato.telefone)) + + def _emit(self, root_nfe: ET.Element) -> None: + nf = self.nf + emit = _sub(root_nfe, "emit") + _sub(emit, "CNPJ", nf.emitente.cnpj) + _sub(emit, "xNome", _limpar(nf.emitente.razao_social)[:60]) + if nf.emitente.nome_fantasia: + _sub(emit, "xFant", _limpar(nf.emitente.nome_fantasia)[:60]) + self._endereco(emit, nf.emitente, "Emit") + if nf.emitente.ie: + _sub(emit, "IE", re.sub(r"\D", "", nf.emitente.ie)) + _sub(emit, "CRT", nf.emitente.regime_tributario.value[:1]) + + def _dest(self, root_nfe: ET.Element) -> None: + nf = self.nf + dest = _sub(root_nfe, "dest") + _sub(dest, "CNPJ", nf.destinatario.cnpj) + _sub(dest, "xNome", _limpar(nf.destinatario.razao_social)[:60]) + self._endereco(dest, nf.destinatario, "Dest") + if nf.destinatario.ie: + _sub(dest, "IE", re.sub(r"\D", "", nf.destinatario.ie)) + _sub(dest, "indIEDest", "1") # 1=Contribuinte ICMS + if nf.destinatario.contato.email: + _sub(dest, "email", nf.destinatario.contato.email[:60]) + + def _det(self, root_nfe: ET.Element) -> None: + nf = self.nf + for item in nf.itens: + det = _sub(root_nfe, "det") + det.set("nItem", str(item.numero_item)) + + prod = _sub(det, "prod") + _sub(prod, "cProd", item.produto.codigo[:60]) + _sub(prod, "cEAN", item.produto.cod_barras or "SEM GTIN") + _sub(prod, "xProd", _limpar(item.produto.descricao)[:120]) + _sub(prod, "NCM", re.sub(r"\D", "", item.produto.ncm)[:8]) + if item.produto.cest: + _sub(prod, "CEST", re.sub(r"\D", "", item.produto.cest)) + _sub(prod, "CFOP", item.cfop) + _sub(prod, "uCom", item.produto.unidade[:6]) + _sub(prod, "qCom", _fmt_dec(item.quantidade, 4)) + _sub(prod, "vUnCom", _fmt_dec(item.valor_unitario, 10)) + _sub(prod, "vProd", _fmt_dec(item.valor_produtos)) + _sub(prod, "cEANTrib", item.produto.cod_barras or "SEM GTIN") + _sub(prod, "uTrib", item.produto.unidade[:6]) + _sub(prod, "qTrib", _fmt_dec(item.quantidade, 4)) + _sub(prod, "vUnTrib", _fmt_dec(item.valor_unitario, 10)) + if item.frete > 0: + _sub(prod, "vFrete", _fmt_dec(item.frete)) + if item.seguro > 0: + _sub(prod, "vSeg", _fmt_dec(item.seguro)) + if item.desconto > 0: + _sub(prod, "vDesc", _fmt_dec(item.desconto)) + if item.outras_despesas > 0: + _sub(prod, "vOutro", _fmt_dec(item.outras_despesas)) + _sub(prod, "indTot", "1") # 1=Valor do item compõe o total + + + imposto = _sub(det, "imposto") + self._icms_item(imposto, item) + self._ipi_item(imposto, item) + self._pis_item(imposto, item) + self._cofins_item(imposto, item) + + def _icms_item(self, imposto: ET.Element, item: ItemNotaFiscal) -> None: + icms = _sub(imposto, "ICMS") + cst = item.produto.cst_icms + + + if cst in {"000"}: + grupo = _sub(icms, "ICMS00") + _sub(grupo, "orig", "0") + _sub(grupo, "CST", cst) + _sub(grupo, "modBC", "3") + _sub(grupo, "vBC", _fmt_dec(item.base_icms)) + _sub(grupo, "pICMS", _fmt_dec(item.produto.aliq_icms, 4)) + _sub(grupo, "vICMS", _fmt_dec(item.valor_icms)) + elif cst in {"020"}: + grupo = _sub(icms, "ICMS20") + _sub(grupo, "orig", "0") + _sub(grupo, "CST", cst) + _sub(grupo, "modBC", "3") + _sub(grupo, "pRedBC", _fmt_dec(Decimal("33.33"), 4)) + _sub(grupo, "vBC", _fmt_dec(item.base_icms)) + _sub(grupo, "pICMS", _fmt_dec(item.produto.aliq_icms, 4)) + _sub(grupo, "vICMS", _fmt_dec(item.valor_icms)) + elif cst in {"040", "041", "050"}: + grupo = _sub(icms, f"ICMS{cst}") + _sub(grupo, "orig", "0") + _sub(grupo, "CST", cst) + elif cst.startswith("1"): # Simples Nacional + grupo = _sub(icms, "ICMSSN102") + _sub(grupo, "orig", "0") + _sub(grupo, "CSOSN", cst) + else: + grupo = _sub(icms, "ICMS90") + _sub(grupo, "orig", "0") + _sub(grupo, "CST", "090") + _sub(grupo, "modBC", "3") + _sub(grupo, "vBC", _fmt_dec(item.base_icms)) + _sub(grupo, "pICMS", _fmt_dec(item.produto.aliq_icms, 4)) + _sub(grupo, "vICMS", _fmt_dec(item.valor_icms)) + + def _ipi_item(self, imposto: ET.Element, item: ItemNotaFiscal) -> None: + if item.produto.aliq_ipi == 0 and item.produto.cst_ipi in {"52", "53", "54"}: + ipi = _sub(imposto, "IPI") + _sub(ipi, "cEnq", "999") + ipi_trib = _sub(ipi, "IPITrib") + _sub(ipi_trib, "CST", item.produto.cst_ipi) + _sub(ipi_trib, "vBC", _fmt_dec(Decimal("0"))) + _sub(ipi_trib, "pIPI", _fmt_dec(Decimal("0"), 4)) + _sub(ipi_trib, "vIPI", _fmt_dec(Decimal("0"))) + return + + ipi = _sub(imposto, "IPI") + _sub(ipi, "cEnq", "999") + ipi_trib = _sub(ipi, "IPITrib") + _sub(ipi_trib, "CST", item.produto.cst_ipi) + _sub(ipi_trib, "vBC", _fmt_dec(item.base_ipi)) + _sub(ipi_trib, "pIPI", _fmt_dec(item.produto.aliq_ipi, 4)) + _sub(ipi_trib, "vIPI", _fmt_dec(item.valor_ipi)) + + def _pis_item(self, imposto: ET.Element, item: ItemNotaFiscal) -> None: + pis = _sub(imposto, "PIS") + cst = item.produto.cst_pis + if cst in {"07", "08", "09"}: + pisnt = _sub(pis, "PISNT") + _sub(pisnt, "CST", cst) + else: + pisaliq = _sub(pis, "PISAliq") + _sub(pisaliq, "CST", cst) + _sub(pisaliq, "vBC", _fmt_dec(item.valor_produtos)) + _sub(pisaliq, "pPIS", _fmt_dec(Decimal("0.65"), 4)) + _sub(pisaliq, "vPIS", _fmt_dec(Decimal("0"))) + + def _cofins_item(self, imposto: ET.Element, item: ItemNotaFiscal) -> None: + cofins = _sub(imposto, "COFINS") + cst = item.produto.cst_cofins + if cst in {"07", "08", "09"}: + cofinsnt = _sub(cofins, "COFINSNT") + _sub(cofinsnt, "CST", cst) + else: + cofinsaliq = _sub(cofins, "COFINSAliq") + _sub(cofinsaliq, "CST", cst) + _sub(cofinsaliq, "vBC", _fmt_dec(item.valor_produtos)) + _sub(cofinsaliq, "pCOFINS", _fmt_dec(Decimal("3.00"), 4)) + _sub(cofinsaliq, "vCOFINS", _fmt_dec(Decimal("0"))) + + def _total(self, root_nfe: ET.Element) -> None: + nf = self.nf + total = _sub(root_nfe, "total") + ict = _sub(total, "ICMSTot") + _sub(ict, "vBC", _fmt_dec(sum(i.base_icms for i in nf.itens))) + _sub(ict, "vICMS", _fmt_dec(nf.valor_icms)) + _sub(ict, "vICMSDeson", "0.00") + _sub(ict, "vFCPUFDest", "0.00") + _sub(ict, "vICMSUFDest", "0.00") + _sub(ict, "vICMSUFRemet", "0.00") + _sub(ict, "vFCP", "0.00") + _sub(ict, "vBCST", "0.00") + _sub(ict, "vST", "0.00") + _sub(ict, "vFCPST", "0.00") + _sub(ict, "vFCPSTRet", "0.00") + _sub(ict, "vProd", _fmt_dec(nf.valor_produtos)) + _sub(ict, "vFrete", _fmt_dec(nf.valor_frete)) + _sub(ict, "vSeg", _fmt_dec(nf.valor_seguro)) + _sub(ict, "vDesc", _fmt_dec(nf.valor_desconto)) + _sub(ict, "vII", "0.00") + _sub(ict, "vIPI", _fmt_dec(nf.valor_ipi)) + _sub(ict, "vIPIDevol", "0.00") + _sub(ict, "vPIS", "0.00") + _sub(ict, "vCOFINS", "0.00") + _sub(ict, "vOutro", _fmt_dec(nf.valor_outras_despesas)) + _sub(ict, "vNF", _fmt_dec(nf.valor_total)) + _sub(ict, "vTotTrib", "0.00") + + def _transp(self, root_nfe: ET.Element) -> None: + transp = _sub(root_nfe, "transp") + _sub(transp, "modFrete", "9") # 9=Sem frete + + def _pag(self, root_nfe: ET.Element) -> None: + pag = _sub(root_nfe, "pag") + detPag = _sub(pag, "detPag") + _sub(detPag, "tPag", "01") # 01=Dinheiro + _sub(detPag, "vPag", _fmt_dec(self.nf.valor_total)) + + def _inf_adic(self, root_nfe: ET.Element) -> None: + nf = self.nf + if nf.info_complementar: + inf = _sub(root_nfe, "infAdic") + _sub(inf, "infCpl", _limpar(nf.info_complementar)[:500]) + + def gerar_xml(self) -> str: + + chave = self.nf.chave_acesso or self._gerar_chave() + + + nfeProc = ET.Element("nfeProc") + nfeProc.set("xmlns", NS_NFE) + nfeProc.set("versao", VERSAO_NFE) + + nfe = _sub(nfeProc, "NFe") + nfe.set("xmlns", NS_NFE) + + infNFe = _sub(nfe, "infNFe") + infNFe.set("versao", VERSAO_NFE) + infNFe.set("Id", f"NFe{chave}") + + self._ide(infNFe, chave) + self._emit(infNFe) + self._dest(infNFe) + self._det(infNFe) + self._total(infNFe) + self._transp(infNFe) + self._pag(infNFe) + self._inf_adic(infNFe) + + ET.indent(nfeProc, space=" ") + return '\n' + ET.tostring(nfeProc, encoding="unicode") + + def salvar(self, diretorio: str | Path = ".") -> Path: + chave = self.nf.chave_acesso or self._gerar_chave() + caminho = Path(diretorio) / f"NFe{chave}.xml" + caminho.parent.mkdir(parents=True, exist_ok=True) + caminho.write_text(self.gerar_xml(), encoding="utf-8") + return caminho + + +class AssinadorNFe: + """ + Assina digitalmente o XML da NF-e com certificado ICP-Brasil (A1 ou A3). + + Requer: cryptography + lxml (ou signxml) + O certificado deve ser um PKCS#12 (.pfx ou .p12) válido emitido por + uma Autoridade Certificadora credenciada pela ICP-Brasil. + """ + + def __init__(self, cert_path: str, cert_senha: str): + self.cert_path = cert_path + self.cert_senha = cert_senha + self._privkey = None + self._cert = None + self._carregar_certificado() + + def _carregar_certificado(self) -> None: + try: + from cryptography.hazmat.primitives.serialization import pkcs12 + dados = Path(self.cert_path).read_bytes() + priv, cert, _ = pkcs12.load_key_and_certificates( + dados, self.cert_senha.encode() + ) + self._privkey = priv + self._cert = cert + except ImportError: + raise RuntimeError("Instale 'cryptography': pip install cryptography") + except Exception as e: + raise RuntimeError(f"Erro ao carregar certificado: {e}") + + def assinar(self, xml_str: str) -> str: + """ + Assina o XML da NF-e conforme o padrão XMLDSig exigido pela SEFAZ. + Retorna o XML assinado como string. + """ + try: + from cryptography.hazmat.primitives import hashes, serialization + from cryptography.hazmat.primitives.asymmetric import padding + import base64 + from lxml import etree + + doc = etree.fromstring(xml_str.encode("utf-8")) + ns = {"nfe": NS_NFE} + + + inf_nfe = doc.find(".//nfe:infNFe", ns) + if inf_nfe is None: + raise ValueError("Elemento infNFe não encontrado no XML") + + ref_id = inf_nfe.get("Id", "") + + + inf_bytes = etree.tostring(inf_nfe, method="c14n", exclusive=True) + + + digest = hashes.Hash(hashes.SHA1()) + digest.update(inf_bytes) + digest_value = base64.b64encode(digest.finalize()).decode() + + + signed_info_xml = ( + '' + '' + '' + f'' + "" + '' + '' + "" + '' + f"{digest_value}" + "" + "" + ) + + si_doc = etree.fromstring(signed_info_xml.encode()) + si_bytes = etree.tostring(si_doc, method="c14n", exclusive=True) + + + sig_bytes = self._privkey.sign(si_bytes, padding.PKCS1v15(), hashes.SHA1()) + sig_value = base64.b64encode(sig_bytes).decode() + + + cert_der = self._cert.public_bytes(serialization.Encoding.DER) + cert_b64 = base64.b64encode(cert_der).decode() + + + signature_xml = ( + '' + + signed_info_xml + + f"{sig_value}" + "" + f"{cert_b64}" + "" + "" + ) + + sig_el = etree.fromstring(signature_xml.encode()) + inf_nfe.append(sig_el) + + return etree.tostring(doc, xml_declaration=True, encoding="unicode") + + except ImportError: + raise RuntimeError("Instale 'lxml' e 'cryptography': pip install lxml cryptography") + + def assinar_arquivo(self, xml_path: Path, destino: Optional[Path] = None) -> Path: + xml_str = xml_path.read_text(encoding="utf-8") + assinado = self.assinar(xml_str) + saida = destino or xml_path.with_suffix(".assinado.xml") + saida.write_text(assinado, encoding="utf-8") + return saida diff --git a/src/generators/sped_writer.py b/src/generators/sped_writer.py new file mode 100644 index 0000000000000000000000000000000000000000..c3070e19b25347f5cc0469fa7d5d5bfa11bd9656 --- /dev/null +++ b/src/generators/sped_writer.py @@ -0,0 +1,105 @@ + +from __future__ import annotations + +import hashlib +from datetime import date +from decimal import Decimal +from pathlib import Path +from typing import Any + + +def _fmt_data(d: date | None) -> str: + if d is None: + return "" + return d.strftime("%d%m%Y") + + +def _fmt_decimal(v: Decimal | None, casas: int = 2) -> str: + if v is None or v == 0: + return "" + return f"{v:.{casas}f}".replace(".", ",") + + +def _fmt_str(v: Any) -> str: + if v is None: + return "" + return str(v).strip() + + +class RegistroSPED: + + + def __init__(self, tipo: str): + self.tipo = tipo + self._campos: list[str] = [] + + def add(self, valor: Any, tipo_campo: str = "str", casas: int = 2) -> "RegistroSPED": + if valor is None or valor == "": + self._campos.append("") + elif tipo_campo == "data": + self._campos.append(_fmt_data(valor)) + elif tipo_campo == "decimal": + self._campos.append(_fmt_decimal(Decimal(str(valor)), casas)) + elif tipo_campo == "int": + self._campos.append(str(int(valor))) + else: + self._campos.append(str(valor).strip()) + return self + + def to_line(self) -> str: + campos = [self.tipo] + self._campos + return "|" + "|".join(campos) + "|\n" + + +class ArquivoSPED: + + + def __init__(self, nome_arquivo: str): + self.nome_arquivo = nome_arquivo + self._linhas: list[str] = [] + self._contadores: dict[str, int] = {} + + def _add_linha(self, linha: str) -> None: + self._linhas.append(linha) + tipo = linha.split("|")[1] if "|" in linha else "" + self._contadores[tipo] = self._contadores.get(tipo, 0) + 1 + + def adicionar_registro(self, reg: RegistroSPED) -> None: + self._add_linha(reg.to_line()) + + def adicionar_linha_raw(self, linha: str) -> None: + if not linha.endswith("\n"): + linha += "\n" + self._add_linha(linha) + + def total_linhas(self) -> int: + return len(self._linhas) + + def conteudo(self) -> str: + return "".join(self._linhas) + + def salvar(self, diretorio: str | Path = ".") -> Path: + caminho = Path(diretorio) / self.nome_arquivo + caminho.parent.mkdir(parents=True, exist_ok=True) + caminho.write_text(self.conteudo(), encoding="utf-8") + return caminho + + def hash_md5(self) -> str: + return hashlib.md5(self.conteudo().encode("utf-8")).hexdigest() + + +def criar_registro(tipo: str, *campos) -> str: + + partes = [tipo] + for campo in campos: + if campo is None: + partes.append("") + elif isinstance(campo, date): + partes.append(_fmt_data(campo)) + elif isinstance(campo, Decimal): + partes.append(_fmt_decimal(campo)) + elif isinstance(campo, float): + partes.append(_fmt_decimal(Decimal(str(campo)))) + else: + partes.append(str(campo).strip()) + return "|" + "|".join(partes) + "|\n" diff --git a/src/models/__init__.py b/src/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/models/__pycache__/__init__.cpython-312.pyc b/src/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c2ce14a55286fb589c470305f1cbd8f1b4846c11 Binary files /dev/null and b/src/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/models/__pycache__/fiscal_llm.cpython-312.pyc b/src/models/__pycache__/fiscal_llm.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..46d41925d70388a4d2e5629144e98c945c4110a2 Binary files /dev/null and b/src/models/__pycache__/fiscal_llm.cpython-312.pyc differ diff --git a/src/models/__pycache__/trainer.cpython-312.pyc b/src/models/__pycache__/trainer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..637b0f275832ee8e047b7f5de610f4db2c4417bd Binary files /dev/null and b/src/models/__pycache__/trainer.cpython-312.pyc differ diff --git a/src/models/fiscal_llm.py b/src/models/fiscal_llm.py new file mode 100644 index 0000000000000000000000000000000000000000..c35e672f69b90f9b222dbc99dda55891fd5d07e1 --- /dev/null +++ b/src/models/fiscal_llm.py @@ -0,0 +1,1171 @@ + +from __future__ import annotations + +import math +from dataclasses import dataclass +from datetime import date +from decimal import Decimal +from pathlib import Path +from typing import Any, Optional + +import torch +import torch.nn as nn +import torch.nn.functional as F + + +# --------------------------------------------------------------------------- +# Tokenizador simplificado (char-level) para texto fiscal em português +# --------------------------------------------------------------------------- + +class TokenizadorFiscal: + VOCAB_ESPECIAL = ["", "", "", "", "", ""] + + def __init__(self, vocab_size: int = 2048): + self.vocab_size = vocab_size + self._char2idx: dict[str, int] = {} + self._idx2char: dict[int, str] = {} + self._construir_vocab_base() + + def _construir_vocab_base(self) -> None: + idx = 0 + for tok in self.VOCAB_ESPECIAL: + self._char2idx[tok] = idx + self._idx2char[idx] = tok + idx += 1 + for c in range(32, 127): + ch = chr(c) + self._char2idx[ch] = idx + self._idx2char[idx] = ch + idx += 1 + for ch in "áéíóúâêîôûãõàèìòùäëïöüçñÁÉÍÓÚÂÊÎÔÛÃÕÀÈÌÒÙÄËÏÖÜÇÑ": + if ch not in self._char2idx: + self._char2idx[ch] = idx + self._idx2char[idx] = ch + idx += 1 + self.pad_id = self._char2idx[""] + self.unk_id = self._char2idx[""] + self.bos_id = self._char2idx[""] + self.eos_id = self._char2idx[""] + self.sep_id = self._char2idx[""] + self.mask_id = self._char2idx[""] + self._vocab_atual = idx + + def encode(self, texto: str, max_len: int = 512, add_special: bool = True) -> list[int]: + ids: list[int] = [] + if add_special: + ids.append(self.bos_id) + for ch in texto[: max_len - (2 if add_special else 0)]: + ids.append(self._char2idx.get(ch, self.unk_id)) + if add_special: + ids.append(self.eos_id) + return ids + + def decode(self, ids: list[int]) -> str: + especiais = {self.pad_id, self.bos_id, self.eos_id, self.sep_id} + return "".join(self._idx2char.get(i, "?") for i in ids if i not in especiais) + + def __len__(self) -> int: + return self._vocab_atual + + +# --------------------------------------------------------------------------- +# Positional Encoding +# --------------------------------------------------------------------------- + +class PositionalEncoding(nn.Module): + def __init__(self, d_model: int, max_len: int = 1024, dropout: float = 0.1): + super().__init__() + self.dropout = nn.Dropout(p=dropout) + pe = torch.zeros(max_len, d_model) + pos = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) + div = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) + pe[:, 0::2] = torch.sin(pos * div) + pe[:, 1::2] = torch.cos(pos * div) + self.register_buffer("pe", pe.unsqueeze(0)) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = x + self.pe[:, : x.size(1)] + return self.dropout(x) + + +# --------------------------------------------------------------------------- +# Transformer Encoder para texto fiscal +# --------------------------------------------------------------------------- + +class TransformerFiscal(nn.Module): + """ + Transformer encoder compacto para análise de texto fiscal. + Usado para: embeddings, classificação de obrigações, extração de entidades. + + Arquitetura: + - 4 camadas encoder, d_model=256, 8 cabeças de atenção + - Pre-LN (norm_first=True) para estabilidade de treinamento + - Cabeça de classificação linear (256 → 128 → num_classes) + - Cabeça de extração de valor escalar (256 → 64 → 1) + """ + + def __init__( + self, + vocab_size: int = 2048, + d_model: int = 256, + nhead: int = 8, + num_layers: int = 4, + dim_feedforward: int = 1024, + dropout: float = 0.1, + max_len: int = 1024, + num_classes: int = 16, + ): + super().__init__() + self.d_model = d_model + self.embedding = nn.Embedding(vocab_size, d_model, padding_idx=0) + self.pos_enc = PositionalEncoding(d_model, max_len, dropout) + encoder_layer = nn.TransformerEncoderLayer( + d_model=d_model, + nhead=nhead, + dim_feedforward=dim_feedforward, + dropout=dropout, + batch_first=True, + norm_first=True, + ) + self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) + self.norm = nn.LayerNorm(d_model) + self.classificador = nn.Sequential( + nn.Linear(d_model, d_model // 2), + nn.GELU(), + nn.Dropout(dropout), + nn.Linear(d_model // 2, num_classes), + ) + self.extrator_valor = nn.Sequential( + nn.Linear(d_model, d_model // 4), + nn.GELU(), + nn.Linear(d_model // 4, 1), + ) + self._init_weights() + + def _init_weights(self) -> None: + for p in self.parameters(): + if p.dim() > 1: + nn.init.xavier_uniform_(p) + + def encode(self, input_ids: torch.Tensor, attention_mask: Optional[torch.Tensor] = None) -> torch.Tensor: + """Retorna embeddings de sequência (shape: B × L × d_model).""" + x = self.embedding(input_ids) * math.sqrt(self.d_model) + x = self.pos_enc(x) + key_padding_mask = (attention_mask == 0) if attention_mask is not None else None + x = self.encoder(x, src_key_padding_mask=key_padding_mask) + return self.norm(x) + + def cls_embedding(self, input_ids: torch.Tensor, attention_mask: Optional[torch.Tensor] = None) -> torch.Tensor: + """Embedding CLS (primeiro token) como representação da sentença.""" + return self.encode(input_ids, attention_mask)[:, 0, :] + + def forward( + self, + input_ids: torch.Tensor, + attention_mask: Optional[torch.Tensor] = None, + task: str = "classificar", + ) -> torch.Tensor: + cls = self.cls_embedding(input_ids, attention_mask) + if task == "classificar": + return self.classificador(cls) + if task == "extrair_valor": + return self.extrator_valor(cls) + return cls + + def save(self, path: str | Path) -> None: + torch.save({ + "state_dict": self.state_dict(), + "config": { + "vocab_size": self.embedding.num_embeddings, + "d_model": self.d_model, + }, + }, path) + + @classmethod + def load(cls, path: str | Path, **kwargs) -> "TransformerFiscal": + ckpt = torch.load(path, map_location="cpu", weights_only=False) + cfg = {**ckpt.get("config", {}), **kwargs} + model = cls(**cfg) + model.load_state_dict(ckpt["state_dict"]) + return model + + +# --------------------------------------------------------------------------- +# Banco de Embeddings Fiscais (RAG em memória) +# --------------------------------------------------------------------------- + +class BancoEmbeddingsFiscais: + """ + Armazena embeddings de conhecimento fiscal para recuperação semântica (RAG). + Usa cosine similarity para busca eficiente. + """ + + def __init__(self, modelo: TransformerFiscal, tokenizador: TokenizadorFiscal, device: str = "cpu"): + self.modelo = modelo + self.tokenizador = tokenizador + self.device = device + self._textos: list[str] = [] + self._embeddings: Optional[torch.Tensor] = None + self.modelo.to(device) + self.modelo.eval() + + def _embed(self, textos: list[str]) -> torch.Tensor: + ids_list = [self.tokenizador.encode(t, max_len=512) for t in textos] + max_len = max(len(i) for i in ids_list) + padded = torch.zeros(len(ids_list), max_len, dtype=torch.long) + mask = torch.zeros(len(ids_list), max_len, dtype=torch.long) + for k, ids in enumerate(ids_list): + padded[k, : len(ids)] = torch.tensor(ids) + mask[k, : len(ids)] = 1 + padded = padded.to(self.device) + mask = mask.to(self.device) + with torch.no_grad(): + embs = self.modelo.cls_embedding(padded, mask) + return F.normalize(embs, dim=-1) + + def indexar(self, textos: list[str]) -> None: + self._textos = textos + self._embeddings = self._embed(textos) + + def buscar(self, query: str, top_k: int = 5) -> list[tuple[str, float]]: + if self._embeddings is None or not self._textos: + return [] + q_emb = self._embed([query]) + scores = (self._embeddings @ q_emb.T).squeeze(-1) + top_idx = scores.argsort(descending=True)[:top_k] + return [(self._textos[i], float(scores[i])) for i in top_idx] + + +# --------------------------------------------------------------------------- +# Classificador de Intenção Fiscal +# --------------------------------------------------------------------------- + +CLASSES_OBRIGACAO = [ + "EFD_ICMS_IPI", + "EFD_CONTRIBUICOES", + "ECD", + "ECF", + "NFe", + "NFSe", + "CTe", + "eSocial", + "EFD_REINF", + "DCTF", + "PGDAS", + "calculo_icms", + "calculo_ipi", + "calculo_pis_cofins", + "calculo_irpj_csll", + "calculo_iss", +] + + +class ClassificadorIntencaoFiscal: + """Classifica intenções em consultas fiscais usando o TransformerFiscal.""" + + def __init__(self, modelo: TransformerFiscal, tokenizador: TokenizadorFiscal, device: str = "cpu"): + self.modelo = modelo + self.tokenizador = tokenizador + self.device = device + self.classes = CLASSES_OBRIGACAO + self.modelo.to(device) + self.modelo.eval() + + def classificar(self, texto: str, top_k: int = 3) -> list[tuple[str, float]]: + ids = self.tokenizador.encode(texto, max_len=256) + t = torch.tensor([ids], dtype=torch.long).to(self.device) + mask = torch.ones_like(t) + with torch.no_grad(): + logits = self.modelo(t, mask, task="classificar") + probs = F.softmax(logits[0], dim=-1) + top = probs.argsort(descending=True)[:top_k] + return [(self.classes[i], float(probs[i])) for i in top] + + +# --------------------------------------------------------------------------- +# Resultado do pipeline +# --------------------------------------------------------------------------- + +@dataclass +class ResultadoFiscal: + """Resultado de uma operação processada pelo PipelineFiscal.""" + operacao: str + dados: dict[str, Any] + contexto_rag: list[tuple[str, float]] + intencoes: list[tuple[str, float]] + arquivos_gerados: list[str] + + def sucesso(self) -> bool: + return "erro" not in self.dados + + def resumo(self) -> str: + linhas = [f"[{self.operacao}]"] + for k, v in self.dados.items(): + if k in ("instrucoes", "partilha", "atividades"): + continue + if isinstance(v, float): + linhas.append(f" {k}: R$ {v:,.2f}") + elif isinstance(v, list): + linhas.append(f" {k}: {len(v)} item(ns)") + else: + linhas.append(f" {k}: {v}") + if self.arquivos_gerados: + linhas.append(f" arquivos: {', '.join(self.arquivos_gerados)}") + return "\n".join(linhas) + + +# --------------------------------------------------------------------------- +# PipelineFiscal — ponto de entrada principal do LLM +# --------------------------------------------------------------------------- + +class PipelineFiscal: + """ + Pipeline LLM completo para obrigações fiscais brasileiras. + + Combina: + - TransformerFiscal (PyTorch): classificação de intenção + embeddings RAG + - Calculadores tributários: ICMS, IPI, PIS/COFINS, IRPJ/CSLL, ISS, Simples Nacional + - Geradores SPED: EFD ICMS/IPI, EFD Contribuições, ECD, ECF, DCTF, EFD-Reinf, e-Social + - Transmissor NF-e: SEFAZ SOAP webservices + + Uso básico:: + + pipeline = PipelineFiscal(diretorio_saida="./output") + resultado = pipeline.processar( + "calcule o ICMS de uma venda", + {"valor_mercadoria": 10000, "aliquota": 18}, + ) + print(resultado.resumo()) + + # Geração de todas as obrigações de um período + arquivos = pipeline.gerar_obrigacoes(periodo) + + Uso com modelo treinado:: + + pipeline = PipelineFiscal(caminho_modelo="modelo_fiscal.pt") + """ + + _MAPA_PALAVRAS: dict[str, list[str]] = { + "calcular_icms": ["icms", "difal", "substituição tributária", "icms-st"], + "calcular_ipi": ["ipi", "industrializado", "tipi"], + "calcular_pis_cofins": ["pis", "cofins", "contribuição pis", "contribuição cofins"], + "calcular_irpj_csll": ["irpj", "csll", "lucro real", "lucro presumido", "imposto renda"], + "calcular_iss": ["iss", "imposto serviços", "serviço municipal"], + "calcular_simples": ["simples nacional", "das", "mei", "microempresa"], + "calcular_pgdas": ["pgdas", "pgdas-d", "declaração simples"], + "gerar_efd_icms_ipi": ["efd icms", "sped icms", "escrituração fiscal digital icms", "bloco k", "ciap"], + "gerar_efd_contribuicoes":["efd contribuições", "sped pis", "sped cofins"], + "gerar_ecd": ["ecd", "escrituração contábil digital", "livro diário"], + "gerar_ecf": ["ecf", "escrituração contábil fiscal", "lalur", "dipj"], + "gerar_dctf": ["dctf", "declaração débitos", "débitos tributários federais"], + "gerar_efd_reinf": ["reinf", "efd-reinf", "retenção previdenciária"], + "gerar_esocial": ["esocial", "e-social", "folha pagamento", "admissão", "rescisão"], + "gerar_cte": ["cte", "ct-e", "conhecimento transporte", "frete eletrônico"], + "gerar_nfce": ["nfce", "nfc-e", "modelo 65", "nota consumidor", "pdv"], + "gerar_mdfe": ["mdfe", "mdf-e", "manifesto documentos fiscais"], + "gerar_dirf": ["dirf", "imposto renda retido fonte", "declaração irrf anual"], + "gerar_defis": ["defis", "declaração informações simples nacional", "defis anual"], + "gerar_destda": ["destda", "de-stda", "icms-st simples", "diferencial alíquota simples"], + "gerar_gia": ["gia", "gia-st", "guia informação apuração icms sp", "scanc"], + "verificar_sefaz": ["sefaz", "status nfe", "autorizar nfe"], + } + + def __init__( + self, + diretorio_saida: str | Path = "./output", + caminho_modelo: Optional[str | Path] = None, + device: Optional[str] = None, + ): + self.diretorio_saida = Path(diretorio_saida) + self.diretorio_saida.mkdir(parents=True, exist_ok=True) + self.device = device or ("cuda" if torch.cuda.is_available() else "cpu") + + # Modelo PyTorch + self.tokenizador = TokenizadorFiscal() + if caminho_modelo and Path(caminho_modelo).exists(): + self.modelo = TransformerFiscal.load(caminho_modelo, num_classes=len(CLASSES_OBRIGACAO)) + else: + self.modelo = TransformerFiscal( + vocab_size=len(self.tokenizador), + d_model=256, + nhead=8, + num_layers=4, + dim_feedforward=1024, + num_classes=len(CLASSES_OBRIGACAO), + ) + + self.rag = BancoEmbeddingsFiscais(self.modelo, self.tokenizador, self.device) + self.classificador = ClassificadorIntencaoFiscal(self.modelo, self.tokenizador, self.device) + self.rag.indexar(BASE_CONHECIMENTO_FISCAL) + + # ------------------------------------------------------------------ + # API pública + # ------------------------------------------------------------------ + + def processar(self, query: str, params: Optional[dict] = None) -> ResultadoFiscal: + """ + Processa uma consulta fiscal em linguagem natural. + + Args: + query: Texto em português descrevendo a operação desejada. + params: Dicionário com os parâmetros numéricos/fiscais necessários. + + Returns: + ResultadoFiscal com dados calculados, contexto RAG e intenções. + """ + intencoes = self.classificador.classificar(query, top_k=3) + contexto = self.rag.buscar(query, top_k=2) + operacao = self._detectar_operacao(query, intencoes) + dados: dict[str, Any] = {} + arquivos: list[str] = [] + + if operacao and params is not None: + dados = self._executar(operacao, params) + if "arquivo" in dados: + arquivos = [dados["arquivo"]] + elif "arquivos" in dados: + arquivos = dados.get("arquivos", []) + + return ResultadoFiscal( + operacao=operacao or "consulta", + dados=dados, + contexto_rag=contexto, + intencoes=intencoes, + arquivos_gerados=arquivos, + ) + + def gerar_obrigacoes(self, periodo: Any, obrigacoes: Optional[list[str]] = None) -> dict[str, ResultadoFiscal]: + """ + Gera automaticamente todos os arquivos SPED do período conforme o regime tributário. + + Args: + periodo: PeriodoApuracao com dados da empresa e notas fiscais. + obrigacoes: Lista opcional de obrigações específicas. + Se None, determina automaticamente pelo regime. + + Returns: + Dicionário {nome_obrigacao: ResultadoFiscal}. + """ + from src.fiscal.entities import RegimeTributario + + regime = periodo.empresa.regime_tributario + if obrigacoes is None: + if regime == RegimeTributario.LUCRO_REAL: + obrigacoes = ["EFD_ICMS_IPI", "EFD_CONTRIBUICOES", "ECD", "ECF", "DCTF"] + elif regime == RegimeTributario.LUCRO_PRESUMIDO: + obrigacoes = ["EFD_ICMS_IPI", "EFD_CONTRIBUICOES", "DCTF"] + else: + obrigacoes = [] + + resultados: dict[str, ResultadoFiscal] = {} + for ob in obrigacoes: + dados = self._gerar_sped(ob, periodo) + arquivos = [dados.get("arquivo", "")] if "arquivo" in dados else dados.get("arquivos", []) + resultados[ob] = ResultadoFiscal( + operacao=ob, + dados=dados, + contexto_rag=[], + intencoes=[], + arquivos_gerados=[a for a in arquivos if a], + ) + return resultados + + def gerar_cte(self, params: dict) -> dict: + """Gera o CT-e (Conhecimento de Transporte Eletrônico) XML v4.00.""" + from src.generators.cte import GeradorCTe, CargaCTe, ParteCTe, DadosTransporte, DocumentoReferenciado + from datetime import date as _date + emitente = params.get("_emitente_obj") + if emitente is None: + return {"erro": "Emitente (objeto Empresa) não fornecido em params['_emitente_obj']"} + rem_d = params.get("remetente", {}) + dest_d = params.get("destinatario", {}) + remetente = ParteCTe( + cnpj=rem_d.get("cnpj", ""), + razao_social=rem_d.get("razao_social", "REMETENTE"), + endereco=rem_d.get("endereco", ""), + municipio=rem_d.get("municipio", ""), + uf=rem_d.get("uf", "SP"), + cep=rem_d.get("cep", ""), + cod_municipio=rem_d.get("cod_municipio", ""), + ) + destinatario = ParteCTe( + cnpj=dest_d.get("cnpj", ""), + razao_social=dest_d.get("razao_social", "DESTINATÁRIO"), + endereco=dest_d.get("endereco", ""), + municipio=dest_d.get("municipio", ""), + uf=dest_d.get("uf", "SP"), + cep=dest_d.get("cep", ""), + cod_municipio=dest_d.get("cod_municipio", ""), + ) + carga_d = params.get("carga", {}) + carga = CargaCTe( + descricao=carga_d.get("descricao", "CARGA GERAL"), + produto=carga_d.get("produto", "00"), + valor_carga=Decimal(str(carga_d.get("valor_carga", 0))), + peso_kg=Decimal(str(carga_d.get("peso_kg", 0))), + ) + transp_d = params.get("transporte", {}) + transporte = DadosTransporte( + rntrc=transp_d.get("rntrc", "00000000"), + placa_veiculo=transp_d.get("placa", "AAA0000"), + uf_veiculo=transp_d.get("uf_veiculo", "SP"), + data_prevista_entrega=_date.today(), + ) + documentos = [ + DocumentoReferenciado(chave_nfe=c) + for c in params.get("chaves_nfe", []) + ] + gerador = GeradorCTe( + emitente=emitente, + remetente=remetente, + destinatario=destinatario, + carga=carga, + documentos=documentos, + transporte=transporte, + numero=str(params.get("numero", "1")), + ambiente=str(params.get("ambiente", "2")), + ) + caminho = gerador.salvar(self.diretorio_saida) + return {"tipo": "CTe", "arquivo": str(caminho), "valido": True, "erros": []} + + def gerar_nfce(self, params: dict) -> dict: + """Gera a NFC-e (Nota Fiscal do Consumidor Eletrônica, modelo 65).""" + from src.generators.nfce_xml import GeradorNFCeXML, ConsumidorNFCe + emitente = params.get("_emitente_obj") + itens = params.get("_itens_obj", []) + if emitente is None or not itens: + return {"erro": "Forneça '_emitente_obj' e '_itens_obj' em params"} + cons_d = params.get("consumidor", {}) + consumidor = ConsumidorNFCe( + cpf=cons_d.get("cpf", ""), + nome=cons_d.get("nome", "CONSUMIDOR"), + uf=cons_d.get("uf", "SP"), + ) + gerador = GeradorNFCeXML( + emitente=emitente, + itens=itens, + consumidor=consumidor, + numero=str(params.get("numero", "1")), + forma_pagamento=str(params.get("forma_pagamento", "01")), + ambiente=str(params.get("ambiente", "2")), + ) + caminho = gerador.salvar(self.diretorio_saida) + return {"tipo": "NFCe", "arquivo": str(caminho), "valido": True, "erros": []} + + def gerar_mdfe(self, params: dict) -> dict: + """Gera o MDF-e (Manifesto de Documentos Fiscais Eletrônicos).""" + from src.generators.mdfe import GeradorMDFe, MunicipioDescarga, DocumentoMDFe, ConductorMDFe, SeguroMDFe + emitente = params.get("_emitente_obj") + if emitente is None: + return {"erro": "Forneça '_emitente_obj' em params"} + municipios = [ + MunicipioDescarga( + cod_municipio=m.get("cod_municipio", ""), + nome_municipio=m.get("nome", ""), + uf=m.get("uf", "SP"), + documentos=[DocumentoMDFe(chave=d["chave"], tipo=d.get("tipo", "NFe")) + for d in m.get("documentos", [])], + ) + for m in params.get("municipios", []) + ] + condutores = [ + ConductorMDFe(nome=c.get("nome", ""), cpf=c.get("cpf", "")) + for c in params.get("condutores", []) + ] + seg_d = params.get("seguro", {}) + seguro = SeguroMDFe( + resp_seg=seg_d.get("resp_seg", "1"), + cnpj_seg=seg_d.get("cnpj_seg", ""), + nome_seg=seg_d.get("nome_seg", ""), + nApol=seg_d.get("nApol", ""), + nCT=seg_d.get("nCT", []), + ) + gerador = GeradorMDFe( + emitente=emitente, + municipios_descarrega=municipios, + condutores=condutores, + seguro=seguro, + uf_ini=params.get("uf_ini", "SP"), + uf_fim=params.get("uf_fim", "SP"), + placa_veiculo=params.get("placa", "AAA0000"), + uf_veiculo=params.get("uf_veiculo", "SP"), + rntrc=params.get("rntrc", "00000000"), + numero=str(params.get("numero", "1")), + ambiente=str(params.get("ambiente", "2")), + ) + caminho = gerador.salvar(self.diretorio_saida) + return {"tipo": "MDFe", "arquivo": str(caminho), "valido": True, "erros": []} + + def gerar_dirf(self, params: dict) -> dict: + """Gera o arquivo DIRF (Declaração do IR Retido na Fonte) anual.""" + from src.generators.dirf import GeradorDIRF, BeneficiarioDIRF, ResponsavelDIRF + empresa = params.get("_empresa_obj") + if empresa is None: + return {"erro": "Forneça '_empresa_obj' em params"} + resp_d = params.get("responsavel", {}) + responsavel = ResponsavelDIRF( + cpf=resp_d.get("cpf", ""), + nome=resp_d.get("nome", "RESPONSÁVEL"), + cargo=resp_d.get("cargo", "CONTADOR"), + ddd=resp_d.get("ddd", "11"), + telefone=resp_d.get("telefone", ""), + ) + bens = [ + BeneficiarioDIRF( + cpf_cnpj=b.get("cpf_cnpj", ""), + nome=b.get("nome", ""), + tipo=b.get("tipo", "PF"), + cod_receita=b.get("cod_receita", "0561"), + rendimentos_por_mes={int(k): Decimal(str(v)) for k, v in b.get("rendimentos", {}).items()}, + ir_retido_por_mes={int(k): Decimal(str(v)) for k, v in b.get("ir_retido", {}).items()}, + ) + for b in params.get("beneficiarios", []) + ] + gerador = GeradorDIRF( + empresa=empresa, + ano_calendario=int(params.get("ano", date.today().year - 1)), + beneficiarios=bens, + responsavel=responsavel, + ) + caminho = gerador.salvar(self.diretorio_saida) + return {"tipo": "DIRF", "arquivo": str(caminho), "valido": True, "erros": []} + + def gerar_defis(self, params: dict) -> dict: + """Gera o XML DEFIS (Declaração Anual do Simples Nacional).""" + from src.generators.defis import GeradorDEFIS, ReceitaMensalDEFIS, SocioDEFIS + empresa = params.get("_empresa_obj") + if empresa is None: + return {"erro": "Forneça '_empresa_obj' em params"} + receitas = [ + ReceitaMensalDEFIS( + mes=r["mes"], + receita_bruta_total=Decimal(str(r.get("receita_bruta_total", 0))), + receita_bruta_exportacao=Decimal(str(r.get("receita_bruta_exportacao", 0))), + receita_bruta_isenta=Decimal(str(r.get("receita_bruta_isenta", 0))), + ) + for r in params.get("receitas_mensais", []) + ] + socios = [ + SocioDEFIS( + cpf_cnpj=s.get("cpf_cnpj", ""), + nome=s.get("nome", ""), + percentual_capital=Decimal(str(s.get("percentual_capital", 100))), + ) + for s in params.get("socios", []) + ] + gerador = GeradorDEFIS( + empresa=empresa, + ano_calendario=int(params.get("ano", date.today().year - 1)), + receitas_mensais=receitas, + socios=socios, + ) + caminho = gerador.salvar(self.diretorio_saida) + return {"tipo": "DEFIS", "arquivo": str(caminho), "valido": True, "erros": [], + "relatorio": gerador.relatorio_resumo()} + + def gerar_destda(self, params: dict) -> dict: + """Gera o XML DeSTDA (ICMS-ST e DIFAL do Simples Nacional).""" + from src.generators.destda import GeradorDeSTDA, OperacaoSTDeSTDA + empresa = params.get("_empresa_obj") + if empresa is None: + return {"erro": "Forneça '_empresa_obj' em params"} + operacoes = [ + OperacaoSTDeSTDA( + uf_origem=o.get("uf_origem", "SP"), + uf_destino=o.get("uf_destino", "SP"), + tipo=o.get("tipo", "ST"), + base_calculo=Decimal(str(o.get("base_calculo", 0))), + aliquota=Decimal(str(o.get("aliquota", 18))), + valor_imposto=Decimal(str(o.get("valor_imposto", 0))), + valor_pago=Decimal(str(o.get("valor_pago", 0))), + ) + for o in params.get("operacoes", []) + ] + gerador = GeradorDeSTDA( + empresa=empresa, + periodo=params.get("periodo", date.today().strftime("%Y-%m")), + operacoes=operacoes, + uf_declarante=params.get("uf_declarante", "SP"), + ) + caminho = gerador.salvar(self.diretorio_saida) + return {"tipo": "DeSTDA", "arquivo": str(caminho), "valido": True, "erros": [], + "relatorio": gerador.relatorio_resumo(), "vencimento": gerador.data_vencimento().isoformat()} + + def gerar_gia(self, params: dict) -> dict: + """Gera o arquivo AIE da GIA/GIA-ST (São Paulo).""" + from src.generators.gia import GeradorGIA, ApuracaoGIA, ApuracaoGIAST + empresa = params.get("_empresa_obj") + if empresa is None: + return {"erro": "Forneça '_empresa_obj' em params"} + apur_d = params.get("apuracao", {}) + apuracao = ApuracaoGIA( + debitos_operacoes_proprias=Decimal(str(apur_d.get("debitos_operacoes_proprias", 0))), + debitos_st_retencao=Decimal(str(apur_d.get("debitos_st_retencao", 0))), + estorno_credito=Decimal(str(apur_d.get("estorno_credito", 0))), + outros_debitos=Decimal(str(apur_d.get("outros_debitos", 0))), + creditos_entradas=Decimal(str(apur_d.get("creditos_entradas", 0))), + creditos_outros=Decimal(str(apur_d.get("creditos_outros", 0))), + estorno_debito=Decimal(str(apur_d.get("estorno_debito", 0))), + outros_creditos=Decimal(str(apur_d.get("outros_creditos", 0))), + compensacoes=Decimal(str(apur_d.get("compensacoes", 0))), + saldo_credor_anterior=Decimal(str(apur_d.get("saldo_credor_anterior", 0))), + ) + sts = [ + ApuracaoGIAST( + uf_substituto=s.get("uf", ""), + base_calculo=Decimal(str(s.get("base_calculo", 0))), + aliquota=Decimal(str(s.get("aliquota", 18))), + imposto_retido=Decimal(str(s.get("imposto_retido", 0))), + valor_entradas_st=Decimal(str(s.get("valor_entradas_st", 0))), + ) + for s in params.get("apuracoes_st", []) + ] + gerador = GeradorGIA( + empresa=empresa, + periodo=params.get("periodo", date.today().strftime("%Y-%m")), + apuracao=apuracao, + apuracoes_st=sts, + ) + caminho = gerador.salvar(self.diretorio_saida) + return {"tipo": "GIA", "arquivo": str(caminho), "valido": True, "erros": [], + "relatorio": gerador.relatorio_resumo(), + "icms_a_recolher": float(gerador.icms_a_recolher)} + + def verificar_sefaz(self, uf: str = "SP", ambiente: str = "2") -> dict: + """Verifica a disponibilidade dos serviços SEFAZ para a UF.""" + from src.transmitters.receita_federal import TransmissorNFe + t = TransmissorNFe(uf=uf, ambiente=ambiente) + r = t.verificar_status_servico() + return { + "uf": uf, + "ambiente": "homologação" if ambiente == "2" else "produção", + "sucesso": r.sucesso, + "codigo": r.codigo_retorno, + "mensagem": r.mensagem, + "timestamp": r.timestamp.isoformat(), + } + + def treinar( + self, + exemplos: Optional[list] = None, + epochs: int = 30, + caminho_saida: str = "modelo_fiscal.pt", + ) -> dict: + """ + Treina o TransformerFiscal com exemplos de classificação. + + Args: + exemplos: Lista de ExemploTreinamento. Se None, usa os 70+ exemplos padrão. + epochs: Número de épocas de treinamento. + caminho_saida: Caminho para salvar o modelo treinado. + + Returns: + Histórico de treinamento com loss e acurácia por época. + """ + from src.models.trainer import TrainerFiscal, EXEMPLOS_CLASSIFICACAO + exemplos = exemplos or EXEMPLOS_CLASSIFICACAO + trainer = TrainerFiscal(self.modelo, self.tokenizador, epochs=epochs, device=self.device) + historico = trainer.treinar(exemplos, caminho_saida=caminho_saida) + return historico + + # ------------------------------------------------------------------ + # Calculadores tributários + # ------------------------------------------------------------------ + + def calcular_icms(self, params: dict) -> dict: + from src.calculators.icms import ParametrosICMS, calcular_icms + p = ParametrosICMS( + valor_mercadoria=Decimal(str(params.get("valor_mercadoria", 0))), + aliquota=Decimal(str(params.get("aliquota", 18))), + cst=params.get("cst", "000"), + uf_origem=params.get("uf_origem", "SP"), + uf_destino=params.get("uf_destino", "SP"), + reducao_base=Decimal(str(params.get("reducao_base", 0))), + frete=Decimal(str(params.get("frete", 0))), + seguro=Decimal(str(params.get("seguro", 0))), + outras_despesas=Decimal(str(params.get("outras_despesas", 0))), + desconto=Decimal(str(params.get("desconto", 0))), + calcular_difal=params.get("calcular_difal", False), + consumidor_final=params.get("consumidor_final", False), + ) + r = calcular_icms(p) + return { + "base_calculo": float(r.base_calculo), + "aliquota": float(r.aliquota), + "valor_icms": float(r.valor_icms), + "valor_icms_st": float(r.valor_st), + "valor_fcp": float(r.valor_fcp), + "valor_difal": float(r.valor_difal_destino + r.valor_difal_origem), + "valor_total_icms": float(r.valor_total_icms), + } + + def calcular_ipi(self, params: dict) -> dict: + from src.calculators.ipi import ParametrosIPI, calcular_ipi + p = ParametrosIPI( + valor_produtos=Decimal(str(params.get("valor_produtos", 0))), + aliquota=Decimal(str(params.get("aliquota", 5))), + cst=params.get("cst", "50"), + frete=Decimal(str(params.get("frete", 0))), + ) + r = calcular_ipi(p) + return { + "base_calculo": float(r.base_calculo), + "aliquota": float(r.aliquota), + "valor_ipi": float(r.valor_tributo), + } + + def calcular_pis_cofins(self, params: dict) -> dict: + from src.calculators.pis_cofins import ReceitaBruta, calcular_pis_cofins + from src.fiscal.entities import RegimeTributario + receitas = [ + ReceitaBruta( + descricao=r.get("descricao", "Receita"), + valor=Decimal(str(r.get("valor", 0))), + cst_pis=r.get("cst_pis", "01"), + cst_cofins=r.get("cst_cofins", "01"), + ) + for r in params.get( + "receitas", + [{"descricao": "Receita", "valor": params.get("valor", 0)}], + ) + ] + regime_map = { + "lucro_real": RegimeTributario.LUCRO_REAL, + "lucro_presumido": RegimeTributario.LUCRO_PRESUMIDO, + "simples": RegimeTributario.SIMPLES_NACIONAL, + } + regime = regime_map.get(params.get("regime", "lucro_presumido"), RegimeTributario.LUCRO_PRESUMIDO) + r = calcular_pis_cofins(receitas, regime) + return { + "regime": r.regime, + "base_pis": float(r.base_pis), + "aliquota_pis": float(r.aliq_pis), + "valor_pis": float(r.valor_pis_debito), + "creditos_pis": float(r.creditos_pis), + "pis_a_recolher": float(r.pis_a_recolher), + "base_cofins": float(r.base_cofins), + "aliquota_cofins": float(r.aliq_cofins), + "valor_cofins": float(r.valor_cofins_debito), + "creditos_cofins": float(r.creditos_cofins), + "cofins_a_recolher": float(r.cofins_a_recolher), + } + + def calcular_irpj_csll(self, params: dict) -> dict: + from src.calculators.irpj_csll import ( + calcular_csll_lucro_presumido, calcular_csll_lucro_real, + calcular_irpj_lucro_presumido, calcular_irpj_lucro_real, + ) + regime = params.get("regime", "lucro_presumido") + valor = Decimal(str(params.get("valor", 0))) + atividade = params.get("atividade", "venda_mercadorias") + if regime == "lucro_real": + irpj = calcular_irpj_lucro_real(lucro_antes_ir=valor) + csll = calcular_csll_lucro_real(lucro_antes_csll=valor) + else: + irpj = calcular_irpj_lucro_presumido(receita_bruta=valor, atividade=atividade) + csll = calcular_csll_lucro_presumido( + receita_bruta=valor, + atividade="comercio_industria" if atividade != "servicos_em_geral" else "servicos_em_geral", + ) + return { + "regime": regime, + "base_irpj": float(irpj.base_calculo), + "valor_irpj_base": float(irpj.valor_base), + "valor_irpj_adicional": float(irpj.valor_adicional), + "irpj_total": float(irpj.valor_irpj_total), + "irpj_a_recolher": float(irpj.irpj_a_recolher), + "base_csll": float(csll.base_calculo), + "aliquota_csll": float(csll.aliquota), + "csll_total": float(csll.valor_csll), + "csll_a_recolher": float(csll.csll_a_recolher), + "total_impostos": float(irpj.irpj_a_recolher + csll.csll_a_recolher), + } + + def calcular_iss(self, params: dict) -> dict: + from src.calculators.iss import ParametrosISS, calcular_iss + p = ParametrosISS( + valor_servico=Decimal(str(params.get("valor_servico", 0))), + codigo_servico=params.get("codigo_servico", "17"), + aliquota_municipal=Decimal(str(params["aliquota"])) if "aliquota" in params else None, + retencao_fonte=params.get("retencao_fonte", False), + ) + r = calcular_iss(p) + return { + "base_calculo": float(r.base_calculo), + "aliquota": float(r.aliquota), + "valor_iss": float(r.valor_tributo), + "retencao_na_fonte": p.retencao_fonte, + } + + def calcular_simples(self, params: dict) -> dict: + from src.calculators.simples_nacional import calcular_das + receita_mes = Decimal(str(params.get("receita_mes", 0))) + rbt12 = Decimal(str(params.get("rbt12", receita_mes * 12))) + anexo = params.get("anexo", "I") + r = calcular_das(receita_mes=receita_mes, rbt12=rbt12, anexo=anexo) + return { + "receita_mes": float(receita_mes), + "rbt12": float(rbt12), + "anexo": anexo, + "faixa": r.faixa, + "aliquota_nominal": float(r.aliquota_nominal), + "aliquota_efetiva": float(r.aliquota_efetiva), + "valor_das": float(r.valor_das), + "partilha": {k: float(v) for k, v in r.partilha.items()}, + } + + def calcular_pgdas(self, params: dict) -> dict: + from src.calculators.simples_nacional import calcular_pgdas + periodo = params.get("periodo", date.today().strftime("%Y-%m")) + atividades = params.get("atividades", [{"tipo": "comercio", "receita": params.get("receita_mes", 0)}]) + rbt12 = Decimal(str(params.get("rbt12", 0))) + r = calcular_pgdas( + periodo=periodo, + atividades=[{"tipo": a["tipo"], "receita": Decimal(str(a["receita"]))} for a in atividades], + rbt12=rbt12, + ) + return { + "periodo": r.periodo, + "rbt12": float(r.rbt12), + "receita_total_mes": float(r.receita_total_mes), + "valor_total_das": float(r.valor_total_das), + "data_vencimento": r.data_vencimento.isoformat(), + "atividades": [ + { + "tipo": a.tipo, + "receita": float(a.receita), + "anexo": a.anexo, + "aliquota_efetiva": float(a.resultado.aliquota_efetiva), + "valor_das": float(a.resultado.valor_das), + } + for a in r.atividades + ], + } + + # ------------------------------------------------------------------ + # Geradores SPED/XML + # ------------------------------------------------------------------ + + def _gerar_sped(self, obrigacao: str, periodo: Any) -> dict: + from src.generators.efd_icms_ipi import GeradorEFDICMSIPI + from src.generators.efd_contribuicoes import GeradorEFDContribuicoes + from src.generators.ecd import GeradorECD + from src.generators.ecf import GeradorECF + from src.generators.dctf import montar_dctf_do_periodo + from src.transmitters.receita_federal import TransmissorSPEDLocal, ValidadorArquivoSPED + + transmissor = TransmissorSPEDLocal(self.diretorio_saida) + + if obrigacao == "EFD_ICMS_IPI": + caminho = GeradorEFDICMSIPI(periodo).gerar(self.diretorio_saida) + return transmissor.preparar_efd_icms_ipi(caminho) + + if obrigacao == "EFD_CONTRIBUICOES": + caminho = GeradorEFDContribuicoes(periodo).gerar(self.diretorio_saida) + return transmissor.preparar_efd_contribuicoes(caminho) + + if obrigacao == "ECD": + caminho = GeradorECD(periodo).gerar(self.diretorio_saida) + return transmissor.preparar_ecd(caminho) + + if obrigacao == "ECF": + caminho = GeradorECF(periodo).gerar(self.diretorio_saida) + _, erros = ValidadorArquivoSPED().validar(caminho) + import hashlib + return { + "tipo": "ECF", + "arquivo": str(caminho), + "valido": not erros, + "erros": erros, + "hash_md5": hashlib.md5(caminho.read_bytes()).hexdigest(), + "tamanho_bytes": caminho.stat().st_size, + } + + if obrigacao == "DCTF": + gerador = montar_dctf_do_periodo( + empresa=periodo.empresa, + periodo=periodo.data_inicio.strftime("%Y-%m"), + ) + caminho = gerador.salvar(self.diretorio_saida) + return { + "tipo": "DCTF", + "arquivo": str(caminho), + "valido": True, + "erros": [], + "relatorio": gerador.relatorio_resumo(), + "tamanho_bytes": caminho.stat().st_size, + } + + return {"erro": f"Obrigação desconhecida: {obrigacao}"} + + # ------------------------------------------------------------------ + # Despacho interno + # ------------------------------------------------------------------ + + def _detectar_operacao(self, query: str, intencoes: list[tuple[str, float]]) -> Optional[str]: + q = query.lower() + for operacao, palavras in self._MAPA_PALAVRAS.items(): + if any(p in q for p in palavras): + return operacao + if intencoes: + top = intencoes[0][0] + if "calculo" in top: + return f"calcular_{top.replace('calculo_', '')}" + mapa = { + "EFD_ICMS_IPI": "gerar_efd_icms_ipi", + "EFD_CONTRIBUICOES": "gerar_efd_contribuicoes", + "ECD": "gerar_ecd", + "ECF": "gerar_ecf", + "DCTF": "gerar_dctf", + "eSocial": "gerar_esocial", + "EFD_REINF": "gerar_efd_reinf", + "PGDAS": "calcular_pgdas", + } + return mapa.get(top) + return None + + def _executar(self, operacao: str, params: dict) -> dict: + metodos = { + "calcular_icms": self.calcular_icms, + "calcular_ipi": self.calcular_ipi, + "calcular_pis_cofins": self.calcular_pis_cofins, + "calcular_irpj_csll": self.calcular_irpj_csll, + "calcular_iss": self.calcular_iss, + "calcular_simples": self.calcular_simples, + "calcular_pgdas": self.calcular_pgdas, + "gerar_cte": self.gerar_cte, + "gerar_nfce": self.gerar_nfce, + "gerar_mdfe": self.gerar_mdfe, + "gerar_dirf": self.gerar_dirf, + "gerar_defis": self.gerar_defis, + "gerar_destda": self.gerar_destda, + "gerar_gia": self.gerar_gia, + "verificar_sefaz": self.verificar_sefaz, + } + if operacao in metodos: + try: + return metodos[operacao](params) + except Exception as e: + return {"erro": str(e)} + return {"erro": f"Operação '{operacao}' requer PeriodoApuracao — use gerar_obrigacoes()"} + + +# --------------------------------------------------------------------------- +# Função auxiliar para instanciar o pipeline (retrocompatibilidade) +# --------------------------------------------------------------------------- + +def criar_pipeline_fiscal( + caminho_modelo: Optional[str | Path] = None, + device: Optional[str] = None, +) -> tuple[TransformerFiscal, TokenizadorFiscal, BancoEmbeddingsFiscais, ClassificadorIntencaoFiscal]: + """Instancia os componentes individuais do pipeline (uso avançado).""" + pipeline = PipelineFiscal(caminho_modelo=caminho_modelo, device=device) + return pipeline.modelo, pipeline.tokenizador, pipeline.rag, pipeline.classificador + + +# --------------------------------------------------------------------------- +# Base de conhecimento fiscal (RAG) +# --------------------------------------------------------------------------- + +BASE_CONHECIMENTO_FISCAL = [ + "A EFD ICMS IPI é a Escrituração Fiscal Digital que substitui os livros fiscais de ICMS e IPI. " + "Deve ser entregue mensalmente até o 15º dia útil do mês subsequente. " + "Obrigatória para contribuintes do ICMS e IPI, exceto optantes do Simples Nacional.", + + "O arquivo EFD ICMS IPI é composto pelos blocos: 0 (identificação), C (NF-e mercadorias), " + "D (documentos de transporte), E (apuração ICMS e IPI), G (CIAP), H (inventário), " + "K (produção), 1 (outros), 9 (controle). Cada bloco começa com registro X001 e termina com X990.", + + "No Bloco E da EFD ICMS IPI, o registro E110 contém a apuração do ICMS: " + "VL_TOT_DEBITOS - VL_TOT_CREDITOS = saldo devedor (ICMS a recolher) ou credor (transportar).", + + "A EFD Contribuições escritura a apuração de PIS/PASEP e COFINS. " + "Entregue mensalmente até o 10º dia útil do 2º mês subsequente. " + "Obrigatória para pessoas jurídicas sujeitas ao IRPJ (Lucro Real e Presumido).", + + "PIS não-cumulativo (Lucro Real): alíquota 1,65%. " + "COFINS não-cumulativa (Lucro Real): alíquota 7,60%. " + "PIS cumulativo (Lucro Presumido): alíquota 0,65%. " + "COFINS cumulativa (Lucro Presumido): alíquota 3,00%.", + + "Créditos de PIS/COFINS (regime não-cumulativo, Lucro Real) podem ser tomados sobre: " + "aquisições de mercadorias para revenda, insumos, energia elétrica, aluguéis, " + "depreciação de máquinas e equipamentos, entre outros (Lei 10.637/2002 e 10.833/2003).", + + "A ECD (Escrituração Contábil Digital) é obrigatória para todas as pessoas jurídicas " + "sujeitas ao IRPJ pelo Lucro Real. Entregue até o último dia útil de junho do ano seguinte. " + "Contém livro diário, razão e balancetes.", + + "A ECF (Escrituração Contábil Fiscal) substitui a DIPJ. " + "Obrigatória para pessoas jurídicas tributadas pelo IRPJ (Lucro Real, Presumido ou Arbitrado). " + "Entregue até o último dia útil de julho do ano seguinte.", + + "O ICMS (Imposto sobre Circulação de Mercadorias e Serviços) é estadual. " + "Base de cálculo: valor da mercadoria + frete + seguro + outras despesas. " + "Alíquotas internas variam por estado (17% a 22%). " + "Alíquotas interestaduais: 4%, 7% ou 12%.", + + "Substituição Tributária (ST) de ICMS: o responsável tributário recolhe o imposto " + "de toda a cadeia. A base ST é calculada pela MVA (Margem de Valor Agregado): " + "Base ST = Base ICMS próprio × (1 + MVA%). ICMS ST = Base ST × alíquota interna - ICMS próprio.", + + "DIFAL (Diferencial de Alíquota - EC 87/2015): aplicável em operações interestaduais " + "para consumidor final não contribuinte. DIFAL = Base × (alíquota interna - alíquota interestadual). " + "100% para o estado de destino a partir de 2019.", + + "O IPI (Imposto sobre Produtos Industrializados) é federal, incide na saída de produtos " + "do estabelecimento industrial ou a ele equiparado. " + "Alíquotas variam por produto conforme a TIPI (Tabela de Incidência do IPI). " + "Base de cálculo: valor total da operação.", + + "IRPJ (Lucro Real): 15% sobre lucro real + adicional de 10% sobre lucro que exceder " + "R$20.000/mês ou R$60.000/trimestre. " + "Lucro real = lucro contábil + adições - exclusões - compensações de prejuízos.", + + "IRPJ (Lucro Presumido): alíquota 15% + adicional 10% sobre lucro presumido. " + "Lucro presumido = % sobre receita bruta: 8% para comércio/indústria, 32% para serviços. " + "Apuração trimestral.", + + "CSLL: alíquota 9% para empresas em geral, 15% para instituições financeiras. " + "No Lucro Presumido: base de presunção 12% (comércio/indústria) ou 32% (serviços).", + + "ISS (Imposto sobre Serviços): municipal, regido pela LC 116/2003. " + "Alíquota mínima 2%, máxima 5%. " + "Incide sobre prestação de serviços da lista anexa à LC 116/2003. " + "Retenção obrigatória pelo tomador para serviços específicos.", + + "NF-e (Nota Fiscal Eletrônica - Modelo 55): documento fiscal eletrônico para operações " + "com mercadorias. Autorização via SEFAZ (webservice). " + "Chave de acesso: 44 dígitos. Protocolada com nProt. " + "Validade: 24 horas após emissão (cancelamento).", + + "NFC-e (Nota Fiscal de Consumidor Eletrônica - Modelo 65): para vendas a consumidor final " + "presencial (PDV/frente de caixa). Substituiu o ECF (cupom fiscal).", + + "e-Social: escrituração digital das obrigações fiscais, previdenciárias e trabalhistas. " + "Substitui GFIP, RAIS, CAGED, DIRF, MANAD, PPP, SEFIP entre outros. " + "Grupos de eventos: S-1000 (empregador), S-2200 (trabalhadores), S-2299/2399 (rescisão).", + + "EFD-Reinf: escrituração de retenções e informações da previdência social. " + "Substitui parte da GFIP. Obrigatória para empresas que retêm IR, CSLL, PIS, COFINS " + "de serviços prestados por PJ, e que pagam rendimentos a PF/PJ sujeitos a retenção.", + + "DCTF (Declaração de Débitos e Créditos Tributários Federais): informa débitos apurados " + "e pagamentos/compensações dos tributos federais. " + "Entregue mensalmente até o 15º dia útil do 2º mês subsequente.", + + "Simples Nacional: regime unificado de arrecadação. " + "Abrange IRPJ, CSLL, PIS, COFINS, IPI, CPP, ICMS e ISS em documento único (DAS). " + "Obrigações acessórias: PGDAS-D (mensal, até dia 20), DEFIS (anual, até 31/03), " + "e-Social simplificado para folha.", + + "DARF (Documento de Arrecadação de Receitas Federais): guia de recolhimento para tributos federais. " + "Código de receita identifica o tributo: 6912=IRPJ estimativa, 2089=IRPJ LP, " + "8109=PIS não-cumulativo, 2172=COFINS não-cumulativa.", + + "Calendário fiscal mensal: DAS Simples até dia 20; DARF IRPJ estimativa até último dia útil; " + "DARF PIS/COFINS até dia 25; EFD ICMS/IPI até 15º dia útil; DCTF até 15º dia útil do 2º mês.", + + "Calendário fiscal anual: ECF até último dia útil de julho; ECD até último dia útil de junho; " + "DIRF até último dia útil de fevereiro; RAIS até data definida pelo MT.", +] diff --git a/src/models/trainer.py b/src/models/trainer.py new file mode 100644 index 0000000000000000000000000000000000000000..deb8d183b109138399ecb1f587f3b624a7ae3b0a --- /dev/null +++ b/src/models/trainer.py @@ -0,0 +1,597 @@ + +from __future__ import annotations + +import json +from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional + +import torch +import torch.nn as nn +import torch.optim as optim +from torch.utils.data import DataLoader, Dataset + +from src.models.fiscal_llm import ( + CLASSES_OBRIGACAO, + BASE_CONHECIMENTO_FISCAL, + TokenizadorFiscal, + TransformerFiscal, +) + + +# --------------------------------------------------------------------------- +# Dataset de classificação fiscal +# --------------------------------------------------------------------------- + +@dataclass +class ExemploTreinamento: + texto: str + classe: str + peso: float = 1.0 + + +# Dataset sintético para bootstrap do modelo — 160+ exemplos balanceados +EXEMPLOS_CLASSIFICACAO: list[ExemploTreinamento] = [ + # ----------------------------------------------------------------------- + # EFD_ICMS_IPI — 12 exemplos + # ----------------------------------------------------------------------- + ExemploTreinamento("gerar EFD ICMS IPI do mês de janeiro", "EFD_ICMS_IPI"), + ExemploTreinamento("preciso da escrituração fiscal digital ICMS", "EFD_ICMS_IPI"), + ExemploTreinamento("bloco C nota fiscal SPED", "EFD_ICMS_IPI"), + ExemploTreinamento("apuração ICMS mensal livro fiscal", "EFD_ICMS_IPI"), + ExemploTreinamento("EFD fiscal digital ICMS IPI vencimento", "EFD_ICMS_IPI"), + ExemploTreinamento("substituição tributária ICMS escrituração", "EFD_ICMS_IPI"), + ExemploTreinamento("CIAP controle crédito ICMS ativo imobilizado", "EFD_ICMS_IPI"), + ExemploTreinamento("inventário físico bloco H SPED", "EFD_ICMS_IPI"), + ExemploTreinamento("quando devo entregar o sped fiscal icms", "EFD_ICMS_IPI"), + ExemploTreinamento("bloco E110 apuração saldo devedor credor ICMS", "EFD_ICMS_IPI"), + ExemploTreinamento("transmissão EFD ICMS IPI prazo 15 dia útil", "EFD_ICMS_IPI"), + ExemploTreinamento("controle producao estoque bloco K industria", "EFD_ICMS_IPI"), + # ----------------------------------------------------------------------- + # EFD_CONTRIBUICOES — 11 exemplos + # ----------------------------------------------------------------------- + ExemploTreinamento("EFD contribuições PIS COFINS mensal", "EFD_CONTRIBUICOES"), + ExemploTreinamento("bloco M apuração PIS COFINS", "EFD_CONTRIBUICOES"), + ExemploTreinamento("créditos PIS COFINS não cumulativo", "EFD_CONTRIBUICOES"), + ExemploTreinamento("regime não cumulativo contribuições", "EFD_CONTRIBUICOES"), + ExemploTreinamento("EFD Contribuições prazo entrega transmissão", "EFD_CONTRIBUICOES"), + ExemploTreinamento("sped pis cofins lucro real", "EFD_CONTRIBUICOES"), + ExemploTreinamento("receita bruta PIS COFINS bloco F", "EFD_CONTRIBUICOES"), + ExemploTreinamento("M200 M600 apuração contribuições bloco M", "EFD_CONTRIBUICOES"), + ExemploTreinamento("gerar escrituração PIS COFINS mês", "EFD_CONTRIBUICOES"), + ExemploTreinamento("10 dia util segundo mes EFD contribuicoes prazo", "EFD_CONTRIBUICOES"), + ExemploTreinamento("CST PIS COFINS 01 02 07 49 50 tributação", "EFD_CONTRIBUICOES"), + # ----------------------------------------------------------------------- + # ECD — 10 exemplos + # ----------------------------------------------------------------------- + ExemploTreinamento("escrituração contábil digital ECD livro diário", "ECD"), + ExemploTreinamento("balancete mensal ECD razão contábil", "ECD"), + ExemploTreinamento("plano de contas ECD lançamentos contábeis", "ECD"), + ExemploTreinamento("PVA SPED contábil assinatura digital", "ECD"), + ExemploTreinamento("prazo ECD ultimo dia util junho", "ECD"), + ExemploTreinamento("livro diário razão balanço ECD transmissão", "ECD"), + ExemploTreinamento("SPED contábil lucro real obrigatoriedade", "ECD"), + ExemploTreinamento("I050 I100 I200 registros ECD lançamentos", "ECD"), + ExemploTreinamento("bloco I balanço patrimonial demonstração resultado", "ECD"), + ExemploTreinamento("contador CRC assinar ECD digital", "ECD"), + # ----------------------------------------------------------------------- + # ECF — 10 exemplos + # ----------------------------------------------------------------------- + ExemploTreinamento("ECF escrituração contábil fiscal DIPJ", "ECF"), + ExemploTreinamento("LALUR LACS lucro real ajustado", "ECF"), + ExemploTreinamento("lucro presumido ECF preenchimento", "ECF"), + ExemploTreinamento("IRPJ CSLL ECF apuração anual", "ECF"), + ExemploTreinamento("prazo ECF julho ano seguinte", "ECF"), + ExemploTreinamento("bloco N620 N630 ECF IRPJ CSLL", "ECF"), + ExemploTreinamento("bloco P lucro presumido receita trimestral ECF", "ECF"), + ExemploTreinamento("substituição DIPJ declaração ECF", "ECF"), + ExemploTreinamento("adições exclusões compensações LALUR ECF lucro real", "ECF"), + ExemploTreinamento("quadro de sócios Y600 ECF participação capital", "ECF"), + # ----------------------------------------------------------------------- + # NFe — 12 exemplos + # ----------------------------------------------------------------------- + ExemploTreinamento("emitir nota fiscal eletrônica NF-e", "NFe"), + ExemploTreinamento("XML nota fiscal SEFAZ autorização", "NFe"), + ExemploTreinamento("cancelar NF-e protocolo SEFAZ", "NFe"), + ExemploTreinamento("chave acesso NF-e danfe", "NFe"), + ExemploTreinamento("NF-e modelo 55 emissão produto", "NFe"), + ExemploTreinamento("autorizar nota fiscal SEFAZ SP online", "NFe"), + ExemploTreinamento("status servico sefaz disponivel", "NFe"), + ExemploTreinamento("gerar XML NF-e versao 4.00", "NFe"), + ExemploTreinamento("inutilizar numeração NF-e série", "NFe"), + ExemploTreinamento("carta correcao NF-e CC-e evento", "NFe"), + ExemploTreinamento("NFC-e modelo 65 consumidor balcão PDV", "NFe"), + ExemploTreinamento("emitir nota consumidor NFC-e cupom fiscal", "NFe"), + # ----------------------------------------------------------------------- + # NFSe — 10 exemplos + # ----------------------------------------------------------------------- + ExemploTreinamento("nota fiscal serviços NFS-e ISS prefeitura", "NFSe"), + ExemploTreinamento("RPS nota fiscal serviço eletrônica", "NFSe"), + ExemploTreinamento("emitir NFS-e prestação serviço municipal", "NFSe"), + ExemploTreinamento("nota fiscal servico prefeitura sao paulo", "NFSe"), + ExemploTreinamento("ISS nota servico eletronico municipal", "NFSe"), + ExemploTreinamento("converção RPS lote NFS-e", "NFSe"), + ExemploTreinamento("cancelar nota fiscal servico eletronica", "NFSe"), + ExemploTreinamento("webservice prefeitura NFS-e ABRASF padrão", "NFSe"), + ExemploTreinamento("tomador servico retenção ISS nota servico", "NFSe"), + ExemploTreinamento("codigo servico LC 116 NFS-e item lista", "NFSe"), + # ----------------------------------------------------------------------- + # CTe — 10 exemplos + # ----------------------------------------------------------------------- + ExemploTreinamento("CT-e conhecimento transporte eletrônico", "CTe"), + ExemploTreinamento("documento fiscal transporte carga CTe", "CTe"), + ExemploTreinamento("gerar CTe frete mercadoria transportadora", "CTe"), + ExemploTreinamento("modal rodoviário RNTRC transporte CT-e", "CTe"), + ExemploTreinamento("ICMS transporte interestadual alíquota CTe", "CTe"), + ExemploTreinamento("MDF-e manifesto documentos fiscais transporte", "CTe"), + ExemploTreinamento("cancelar CTe evento conhecimento transporte", "CTe"), + ExemploTreinamento("chave CTe 44 digitos modelo 57", "CTe"), + ExemploTreinamento("seguro carga averbação CTe transportadora", "CTe"), + ExemploTreinamento("valor frete tomador remetente destinatario CTe", "CTe"), + # ----------------------------------------------------------------------- + # eSocial — 12 exemplos + # ----------------------------------------------------------------------- + ExemploTreinamento("e-Social folha pagamento admissão", "eSocial"), + ExemploTreinamento("S-2200 admissão empregado e-Social", "eSocial"), + ExemploTreinamento("rescisão contrato trabalho e-Social S-2299", "eSocial"), + ExemploTreinamento("remuneração S-1200 e-Social", "eSocial"), + ExemploTreinamento("GFIP SEFIP substituído e-Social", "eSocial"), + ExemploTreinamento("evento S-1000 empregador cadastro e-Social", "eSocial"), + ExemploTreinamento("rubrica folha pagamento S-1010 tabela", "eSocial"), + ExemploTreinamento("S-1299 fechamento folha mensal esocial", "eSocial"), + ExemploTreinamento("enviar eventos trabalhistas esocial", "eSocial"), + ExemploTreinamento("aviso previo ferias CLT esocial", "eSocial"), + ExemploTreinamento("CAGED admissão demissão substituído esocial", "eSocial"), + ExemploTreinamento("RAIS DIRF obrigação trabalhista esocial", "eSocial"), + # ----------------------------------------------------------------------- + # EFD_REINF — 10 exemplos + # ----------------------------------------------------------------------- + ExemploTreinamento("EFD Reinf retenções serviços PJ", "EFD_REINF"), + ExemploTreinamento("R-2010 serviços tomados retenção CSLL IRRF", "EFD_REINF"), + ExemploTreinamento("R-2099 fechamento EFD-Reinf", "EFD_REINF"), + ExemploTreinamento("CPRB contribuição previdenciária receita bruta", "EFD_REINF"), + ExemploTreinamento("R-1000 cadastro empregador EFD-Reinf", "EFD_REINF"), + ExemploTreinamento("R-2020 serviços prestados retenção previdenciária", "EFD_REINF"), + ExemploTreinamento("retenção INSS PJ serviços prestados 11%", "EFD_REINF"), + ExemploTreinamento("R-2060 CPRB desoneração folha pagamento", "EFD_REINF"), + ExemploTreinamento("declaração digital reinf prazo dia 15", "EFD_REINF"), + ExemploTreinamento("GFIP substituição EFD reinf retenções", "EFD_REINF"), + # ----------------------------------------------------------------------- + # DCTF — 10 exemplos + # ----------------------------------------------------------------------- + ExemploTreinamento("DCTF declaração débitos tributários federais", "DCTF"), + ExemploTreinamento("DCTF mensal PGD débitos créditos", "DCTF"), + ExemploTreinamento("DARF IRPJ CSLL PIS COFINS declarar DCTF", "DCTF"), + ExemploTreinamento("prazo DCTF 15 dia util segundo mês", "DCTF"), + ExemploTreinamento("código receita DARF 6912 IRPJ estimativa DCTF", "DCTF"), + ExemploTreinamento("compensar crédito PER COMP DCTF", "DCTF"), + ExemploTreinamento("suspensão débito decisão judicial DCTF", "DCTF"), + ExemploTreinamento("retificadora DCTF corrigir débito declarado", "DCTF"), + ExemploTreinamento("DCTF inativa empresa sem movimento", "DCTF"), + ExemploTreinamento("importar XML DCTF web PGD transmitir", "DCTF"), + # ----------------------------------------------------------------------- + # PGDAS — 12 exemplos + # ----------------------------------------------------------------------- + ExemploTreinamento("PGDAS Simples Nacional DAS mensal", "PGDAS"), + ExemploTreinamento("MEI microempreendedor DAS simples", "PGDAS"), + ExemploTreinamento("alíquota simples nacional receita bruta", "PGDAS"), + ExemploTreinamento("DEFIS declaração simples anual", "PGDAS"), + ExemploTreinamento("anexo I II III IV V simples nacional", "PGDAS"), + ExemploTreinamento("fator R simples nacional anexo III V serviço", "PGDAS"), + ExemploTreinamento("RBT12 receita bruta acumulada 12 meses simples", "PGDAS"), + ExemploTreinamento("calcular DAS simples nacional faixa aliquota efetiva", "PGDAS"), + ExemploTreinamento("partilha DAS ICMS ISS CPP IRPJ simples", "PGDAS"), + ExemploTreinamento("vencimento DAS dia 20 simples nacional", "PGDAS"), + ExemploTreinamento("DeSTDA ICMS ST diferencial alíquota simples", "PGDAS"), + ExemploTreinamento("sublimite simples nacional estado ICMS ISS", "PGDAS"), + # ----------------------------------------------------------------------- + # calculo_icms — 12 exemplos + # ----------------------------------------------------------------------- + ExemploTreinamento("calcular ICMS nota fiscal saída", "calculo_icms"), + ExemploTreinamento("base cálculo ICMS mercadoria frete", "calculo_icms"), + ExemploTreinamento("alíquota ICMS interestadual SP MG", "calculo_icms"), + ExemploTreinamento("DIFAL diferencial alíquota operação interestadual", "calculo_icms"), + ExemploTreinamento("substituição tributária MVA base ST", "calculo_icms"), + ExemploTreinamento("FCP fundo combate pobreza ICMS", "calculo_icms"), + ExemploTreinamento("quanto é o ICMS de uma venda de R$ 10000", "calculo_icms"), + ExemploTreinamento("redução base de cálculo ICMS benefício fiscal", "calculo_icms"), + ExemploTreinamento("CST 000 020 040 041 ICMS tributado isento", "calculo_icms"), + ExemploTreinamento("calculo icms st com mva 40 porcento", "calculo_icms"), + ExemploTreinamento("alíquota interna ICMS 18% estado SP", "calculo_icms"), + ExemploTreinamento("ICMS de entrada crédito compra mercadoria", "calculo_icms"), + # ----------------------------------------------------------------------- + # calculo_ipi — 10 exemplos + # ----------------------------------------------------------------------- + ExemploTreinamento("calcular IPI produto industrializado", "calculo_ipi"), + ExemploTreinamento("NCM alíquota IPI TIPI tabela", "calculo_ipi"), + ExemploTreinamento("base cálculo IPI saída estabelecimento", "calculo_ipi"), + ExemploTreinamento("CST IPI 50 tributado 52 isento", "calculo_ipi"), + ExemploTreinamento("IPI frete seguro compõe base calculo", "calculo_ipi"), + ExemploTreinamento("alíquota IPI automóvel NCM capítulo 87", "calculo_ipi"), + ExemploTreinamento("isenção IPI produto farmacêutico NCM 30", "calculo_ipi"), + ExemploTreinamento("saída indústria IPI débito apuração", "calculo_ipi"), + ExemploTreinamento("quanto IPI pago produto industrializado saida", "calculo_ipi"), + ExemploTreinamento("IPI credito entrada materia prima insumo", "calculo_ipi"), + # ----------------------------------------------------------------------- + # calculo_pis_cofins — 11 exemplos + # ----------------------------------------------------------------------- + ExemploTreinamento("calcular PIS COFINS receita bruta", "calculo_pis_cofins"), + ExemploTreinamento("regime cumulativo PIS 0,65% COFINS 3%", "calculo_pis_cofins"), + ExemploTreinamento("créditos PIS COFINS lucro real insumos", "calculo_pis_cofins"), + ExemploTreinamento("retenção PIS COFINS serviços prestados PJ", "calculo_pis_cofins"), + ExemploTreinamento("lucro real PIS 1,65 COFINS 7,6 não cumulativo", "calculo_pis_cofins"), + ExemploTreinamento("crédito energia eletrica aluguel PIS COFINS", "calculo_pis_cofins"), + ExemploTreinamento("quanto PIS COFINS pago receita 100000 lucro presumido", "calculo_pis_cofins"), + ExemploTreinamento("exportação PIS COFINS alíquota zero imunidade", "calculo_pis_cofins"), + ExemploTreinamento("CST PIS 01 70 73 receita tributada", "calculo_pis_cofins"), + ExemploTreinamento("PIS COFINS monofásico combustivel farmácia", "calculo_pis_cofins"), + ExemploTreinamento("retenção minima PIS COFINS CSLL 10 reais serviço", "calculo_pis_cofins"), + # ----------------------------------------------------------------------- + # calculo_irpj_csll — 11 exemplos + # ----------------------------------------------------------------------- + ExemploTreinamento("calcular IRPJ lucro presumido trimestral", "calculo_irpj_csll"), + ExemploTreinamento("LALUR adicionar exclusão lucro real IRPJ", "calculo_irpj_csll"), + ExemploTreinamento("CSLL base cálculo percentual presunção", "calculo_irpj_csll"), + ExemploTreinamento("estimativa mensal IRPJ lucro real", "calculo_irpj_csll"), + ExemploTreinamento("adicional 10% IRPJ lucro excedente", "calculo_irpj_csll"), + ExemploTreinamento("IRPJ 15% alíquota base cálculo lucro", "calculo_irpj_csll"), + ExemploTreinamento("quanto imposto renda empresa lucro 500000", "calculo_irpj_csll"), + ExemploTreinamento("CSLL 9% lucro real contribuição social", "calculo_irpj_csll"), + ExemploTreinamento("presunção lucro 8% comércio 32% serviço IRPJ", "calculo_irpj_csll"), + ExemploTreinamento("prejuízo fiscal compensação 30% lucro real", "calculo_irpj_csll"), + ExemploTreinamento("IRPJ CSLL apuração trimestral lucro presumido", "calculo_irpj_csll"), + # ----------------------------------------------------------------------- + # calculo_iss — 10 exemplos + # ----------------------------------------------------------------------- + ExemploTreinamento("calcular ISS imposto sobre serviços", "calculo_iss"), + ExemploTreinamento("alíquota ISS prestação serviço município", "calculo_iss"), + ExemploTreinamento("retenção ISS tomador serviço nota", "calculo_iss"), + ExemploTreinamento("ISS simples nacional anexo III IV V", "calculo_iss"), + ExemploTreinamento("alíquota minima ISS 2% maxima 5%", "calculo_iss"), + ExemploTreinamento("LC 116 2003 lista serviços ISS municipio", "calculo_iss"), + ExemploTreinamento("ISS retido fonte tomador serviço obrigatorio", "calculo_iss"), + ExemploTreinamento("quanto ISS servico consultoria 10000 reais", "calculo_iss"), + ExemploTreinamento("ISS desenvolvimento software TI tecnologia", "calculo_iss"), + ExemploTreinamento("base calculo ISS dedução material construção", "calculo_iss"), +] + + +class DatasetClassificacaoFiscal(Dataset): + """Dataset PyTorch para treinamento do classificador de obrigações fiscais.""" + + def __init__( + self, + exemplos: list[ExemploTreinamento], + tokenizador: TokenizadorFiscal, + max_len: int = 256, + ): + self.exemplos = exemplos + self.tokenizador = tokenizador + self.max_len = max_len + self.classe2idx = {c: i for i, c in enumerate(CLASSES_OBRIGACAO)} + + def __len__(self) -> int: + return len(self.exemplos) + + def __getitem__(self, idx: int) -> dict[str, torch.Tensor]: + ex = self.exemplos[idx] + ids = self.tokenizador.encode(ex.texto, max_len=self.max_len) + ids_t = torch.zeros(self.max_len, dtype=torch.long) + mask = torch.zeros(self.max_len, dtype=torch.long) + n = min(len(ids), self.max_len) + ids_t[:n] = torch.tensor(ids[:n]) + mask[:n] = 1 + label = self.classe2idx.get(ex.classe, 0) + return { + "input_ids": ids_t, + "attention_mask": mask, + "labels": torch.tensor(label, dtype=torch.long), + "weight": torch.tensor(ex.peso, dtype=torch.float), + } + + +# --------------------------------------------------------------------------- +# Configuração de treinamento +# --------------------------------------------------------------------------- + +@dataclass +class ConfigTreinamento: + epochs: int = 50 + batch_size: int = 16 + lr: float = 3e-4 + weight_decay: float = 1e-4 + grad_clip: float = 1.0 + device: str = "auto" + checkpoint_dir: str = "./checkpoints" + log_interval: int = 10 + val_split: float = 0.15 + seed: int = 42 + early_stopping_patience: int = 8 # para se val_loss não melhora por N épocas + use_class_weights: bool = True # penaliza classes sub-representadas + + +# --------------------------------------------------------------------------- +# Trainer +# --------------------------------------------------------------------------- + +class TrainerFiscal: + """Treinador do TransformerFiscal com classificação de intenção fiscal.""" + + def __init__( + self, + modelo: TransformerFiscal, + tokenizador: TokenizadorFiscal, + cfg: Optional[ConfigTreinamento] = None, + ): + self.modelo = modelo + self.tokenizador = tokenizador + self.cfg = cfg or ConfigTreinamento() + self.device = self._resolver_device() + self.modelo.to(self.device) + self._historico: list[dict] = [] + + def _resolver_device(self) -> str: + if self.cfg.device == "auto": + return "cuda" if torch.cuda.is_available() else "cpu" + return self.cfg.device + + def _split_dataset( + self, exemplos: list[ExemploTreinamento] + ) -> tuple[list[ExemploTreinamento], list[ExemploTreinamento]]: + torch.manual_seed(self.cfg.seed) + n = len(exemplos) + n_val = max(1, int(n * self.cfg.val_split)) + indices = torch.randperm(n).tolist() + val_idx = set(indices[:n_val]) + treino = [e for i, e in enumerate(exemplos) if i not in val_idx] + val = [e for i, e in enumerate(exemplos) if i in val_idx] + return treino, val + + def _calcular_class_weights(self, dataset: DatasetClassificacaoFiscal) -> torch.Tensor: + """Pesos inversamente proporcionais à frequência de cada classe.""" + contagens = [0] * len(CLASSES_OBRIGACAO) + for idx in range(len(dataset)): + item = dataset[idx] + contagens[item["labels"].item()] += 1 + total = sum(contagens) + pesos = [total / (len(contagens) * max(c, 1)) for c in contagens] + return torch.tensor(pesos, dtype=torch.float).to(self.device) + + def treinar( + self, + exemplos: Optional[list[ExemploTreinamento]] = None, + salvar_em: Optional[str] = None, + ) -> list[dict]: + if exemplos is None: + exemplos = EXEMPLOS_CLASSIFICACAO + + treino_ex, val_ex = self._split_dataset(exemplos) + ds_treino = DatasetClassificacaoFiscal(treino_ex, self.tokenizador) + ds_val = DatasetClassificacaoFiscal(val_ex, self.tokenizador) + + loader_treino = DataLoader(ds_treino, batch_size=self.cfg.batch_size, shuffle=True) + loader_val = DataLoader(ds_val, batch_size=self.cfg.batch_size) + + # Class weights para balancear classes sub-representadas + class_w = ( + self._calcular_class_weights(ds_treino) + if self.cfg.use_class_weights + else None + ) + criterion = nn.CrossEntropyLoss(weight=class_w, reduction="none") + + optimizer = optim.AdamW( + self.modelo.parameters(), + lr=self.cfg.lr, + weight_decay=self.cfg.weight_decay, + ) + total_steps = len(loader_treino) * self.cfg.epochs + scheduler = optim.lr_scheduler.OneCycleLR( + optimizer, max_lr=self.cfg.lr, total_steps=total_steps, pct_start=0.1, + ) + + print(f"Treinamento: {len(treino_ex)} treino | {len(val_ex)} validação | " + f"device={self.device} | épocas={self.cfg.epochs}") + + best_val_loss = float("inf") + best_state: dict = {} + paciencia = 0 + + for epoch in range(1, self.cfg.epochs + 1): + self.modelo.train() + loss_total = corretos = total = 0 + + for batch in loader_treino: + ids = batch["input_ids"].to(self.device) + mask = batch["attention_mask"].to(self.device) + labels = batch["labels"].to(self.device) + pesos = batch["weight"].to(self.device) + + optimizer.zero_grad() + logits = self.modelo(ids, mask, task="classificar") + losses = criterion(logits, labels) + loss = (losses * pesos).mean() + loss.backward() + nn.utils.clip_grad_norm_(self.modelo.parameters(), self.cfg.grad_clip) + optimizer.step() + scheduler.step() + + loss_total += loss.item() + corretos += (logits.argmax(-1) == labels).sum().item() + total += labels.size(0) + + val_loss, val_acc = self._avaliar(loader_val, criterion) + + entrada = { + "epoch": epoch, + "train_loss": loss_total / len(loader_treino), + "train_acc": corretos / total, + "val_loss": val_loss, + "val_acc": val_acc, + "lr": scheduler.get_last_lr()[0], + } + self._historico.append(entrada) + + if epoch % self.cfg.log_interval == 0 or epoch == self.cfg.epochs: + print( + f"Época {epoch:3d}/{self.cfg.epochs} | " + f"Loss: {entrada['train_loss']:.4f} | Acc: {entrada['train_acc']:.3f} | " + f"Val Loss: {val_loss:.4f} | Val Acc: {val_acc:.3f}" + ) + + # Early stopping + if val_loss < best_val_loss - 1e-4: + best_val_loss = val_loss + best_state = {k: v.clone() for k, v in self.modelo.state_dict().items()} + paciencia = 0 + else: + paciencia += 1 + if paciencia >= self.cfg.early_stopping_patience: + print(f"Early stopping na época {epoch} (val_loss não melhorou há {paciencia} épocas)") + break + + # Restaura os melhores pesos + if best_state: + self.modelo.load_state_dict(best_state) + + if salvar_em: + caminho = Path(salvar_em) + caminho.parent.mkdir(parents=True, exist_ok=True) + self.modelo.save(caminho) + # Salva histórico e configuração + historico_path = caminho.with_suffix(".historico.json") + historico_path.write_text(json.dumps({ + "historico": self._historico, + "config": { + "epochs": self.cfg.epochs, + "lr": self.cfg.lr, + "batch_size": self.cfg.batch_size, + "val_split": self.cfg.val_split, + "use_class_weights": self.cfg.use_class_weights, + "early_stopping_patience": self.cfg.early_stopping_patience, + "n_exemplos": len(exemplos), + "n_classes": len(CLASSES_OBRIGACAO), + }, + }, indent=2)) + print(f"Modelo salvo: {caminho} | Melhor val_loss: {best_val_loss:.4f}") + + return self._historico + + @torch.no_grad() + def _avaliar( + self, + loader: DataLoader, + criterion: nn.Module, + ) -> tuple[float, float]: + self.modelo.eval() + loss_total = 0.0 + corretos = 0 + total = 0 + + for batch in loader: + ids = batch["input_ids"].to(self.device) + mask = batch["attention_mask"].to(self.device) + labels = batch["labels"].to(self.device) + + logits = self.modelo(ids, mask, task="classificar") + loss = criterion(logits, labels).mean() + + loss_total += loss.item() + corretos += (logits.argmax(-1) == labels).sum().item() + total += labels.size(0) + + n = max(len(loader), 1) + return loss_total / n, corretos / max(total, 1) + + def avaliar_exemplos(self, textos: list[str]) -> list[dict]: + """Avalia o modelo em textos e retorna top-3 predições com probabilidades.""" + import torch.nn.functional as F + self.modelo.eval() + resultados = [] + for texto in textos: + ids = self.tokenizador.encode(texto, max_len=256) + t = torch.tensor([ids], dtype=torch.long).to(self.device) + mask = torch.ones_like(t) + with torch.no_grad(): + logits = self.modelo(t, mask, task="classificar") + probs = F.softmax(logits[0], dim=-1) + top3 = probs.argsort(descending=True)[:3] + resultados.append({ + "texto": texto, + "predicoes": [ + {"classe": CLASSES_OBRIGACAO[i], "prob": float(probs[i])} + for i in top3 + ], + }) + return resultados + + def avaliar_por_classe(self, exemplos: list[ExemploTreinamento]) -> dict: + """ + Avalia o modelo retornando acurácia por classe (matriz de confusão simplificada). + Útil para identificar quais classes o modelo confunde. + """ + import torch.nn.functional as F + self.modelo.eval() + classe2idx = {c: i for i, c in enumerate(CLASSES_OBRIGACAO)} + corretos_por_classe = {c: 0 for c in CLASSES_OBRIGACAO} + total_por_classe = {c: 0 for c in CLASSES_OBRIGACAO} + confusoes: list[dict] = [] + + for ex in exemplos: + ids = self.tokenizador.encode(ex.texto, max_len=256) + t = torch.tensor([ids], dtype=torch.long).to(self.device) + mask = torch.ones_like(t) + with torch.no_grad(): + logits = self.modelo(t, mask, task="classificar") + pred_idx = logits[0].argmax().item() + pred = CLASSES_OBRIGACAO[pred_idx] + verdadeiro = ex.classe + + total_por_classe[verdadeiro] += 1 + if pred == verdadeiro: + corretos_por_classe[verdadeiro] += 1 + else: + confusoes.append({"texto": ex.texto[:60], "esperado": verdadeiro, "previsto": pred}) + + acuracia_por_classe = { + c: corretos_por_classe[c] / max(total_por_classe[c], 1) + for c in CLASSES_OBRIGACAO + } + acuracia_geral = sum(corretos_por_classe.values()) / max(sum(total_por_classe.values()), 1) + + return { + "acuracia_geral": acuracia_geral, + "acuracia_por_classe": acuracia_por_classe, + "confusoes": confusoes[:20], # top 20 erros + } + + +def treinar_modelo( + epochs: int = 30, + lr: float = 3e-4, + salvar_em: str = "./checkpoints/fiscal_llm.pt", + exemplos_extras: Optional[list[ExemploTreinamento]] = None, +) -> TransformerFiscal: + """ + Treina o TransformerFiscal com os dados sintéticos built-in + + quaisquer exemplos extras fornecidos. + """ + tokenizador = TokenizadorFiscal() + modelo = TransformerFiscal( + vocab_size=len(tokenizador), + d_model=256, + nhead=8, + num_layers=4, + dim_feedforward=1024, + num_classes=len(CLASSES_OBRIGACAO), + ) + + exemplos = list(EXEMPLOS_CLASSIFICACAO) + if exemplos_extras: + exemplos.extend(exemplos_extras) + + cfg = ConfigTreinamento(epochs=epochs, lr=lr) + trainer = TrainerFiscal(modelo, tokenizador, cfg) + trainer.treinar(exemplos, salvar_em=salvar_em) + + return modelo diff --git a/src/transmitters/__init__.py b/src/transmitters/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/transmitters/__pycache__/__init__.cpython-312.pyc b/src/transmitters/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2389e062f6f48739d55f6d18631e3660e341bb70 Binary files /dev/null and b/src/transmitters/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/transmitters/__pycache__/receita_federal.cpython-312.pyc b/src/transmitters/__pycache__/receita_federal.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..adfd9bbbe4a96cc1a5679e62cfc53585c0506bac Binary files /dev/null and b/src/transmitters/__pycache__/receita_federal.cpython-312.pyc differ diff --git a/src/transmitters/receita_federal.py b/src/transmitters/receita_federal.py new file mode 100644 index 0000000000000000000000000000000000000000..3be8bcb6ef58e360b860a3d63a1a15d3ef9ee87e --- /dev/null +++ b/src/transmitters/receita_federal.py @@ -0,0 +1,388 @@ + +from __future__ import annotations + +import hashlib +import xml.etree.ElementTree as ET +from datetime import datetime +from pathlib import Path +from typing import Optional + +import httpx + + +class ResultadoTransmissao: + def __init__( + self, + sucesso: bool, + protocolo: str = "", + mensagem: str = "", + codigo_retorno: str = "", + xml_resposta: str = "", + ): + self.sucesso = sucesso + self.protocolo = protocolo + self.mensagem = mensagem + self.codigo_retorno = codigo_retorno + self.xml_resposta = xml_resposta + self.timestamp = datetime.now() + + def __repr__(self) -> str: + status = "OK" if self.sucesso else "ERRO" + return f"ResultadoTransmissao({status}, protocolo={self.protocolo}, msg={self.mensagem})" + + +class ValidadorArquivoSPED: + """Validação básica de arquivo SPED antes da transmissão.""" + + REGISTROS_OBRIGATORIOS_EFD = {"0000", "0001", "0990", "9001", "9990", "9999"} + REGISTROS_OBRIGATORIOS_ECD = {"0000", "0001", "I001", "I990", "9001", "9999"} + + @staticmethod + def _extrair_registros(conteudo: str) -> set[str]: + registros = set() + for linha in conteudo.splitlines(): + partes = linha.strip("|").split("|") + if partes: + registros.add(partes[0]) + return registros + + def validar_efd(self, caminho: Path) -> list[str]: + erros = [] + if not caminho.exists(): + return [f"Arquivo não encontrado: {caminho}"] + conteudo = caminho.read_text(encoding="utf-8") + encontrados = self._extrair_registros(conteudo) + for reg in self.REGISTROS_OBRIGATORIOS_EFD: + if reg not in encontrados: + erros.append(f"Registro obrigatório ausente: {reg}") + linhas = conteudo.splitlines() + if not linhas: + erros.append("Arquivo vazio") + return erros + # Verifica total de linhas no 9999 + for linha in reversed(linhas): + if linha.startswith("|9999|"): + partes = linha.strip("|").split("|") + if len(partes) >= 2: + try: + qt_declarada = int(partes[1]) + qt_real = len(linhas) + if abs(qt_declarada - qt_real) > 5: + erros.append( + f"Total de linhas divergente: declarado={qt_declarada}, real={qt_real}" + ) + except ValueError: + erros.append("Registro 9999 com total de linhas inválido") + break + return erros + + def validar_ecd(self, caminho: Path) -> list[str]: + erros = [] + if not caminho.exists(): + return [f"Arquivo não encontrado: {caminho}"] + conteudo = caminho.read_text(encoding="utf-8") + encontrados = self._extrair_registros(conteudo) + for reg in self.REGISTROS_OBRIGATORIOS_ECD: + if reg not in encontrados: + erros.append(f"Registro obrigatório ausente: {reg}") + return erros + + def calcular_hash(self, caminho: Path) -> str: + conteudo = caminho.read_bytes() + return hashlib.md5(conteudo).hexdigest() + + +class TransmissorSPEDLocal: + """ + Prepara arquivos SPED para transmissão. + + A transmissão efetiva dos arquivos EFD ICMS/IPI, EFD Contribuições e ECD + é feita através do PGE (Programa Gerador de Escriturações) da Receita Federal, + um aplicativo Java que deve ser instalado localmente. Esta classe prepara e + valida os arquivos para uso com o PGE/SPED Validador. + + Para envio via web, utilize o portal e-CAC ou o SPED Transmissão. + """ + + def __init__(self, diretorio_saida: str | Path = "./output"): + self.diretorio_saida = Path(diretorio_saida) + self.validador = ValidadorArquivoSPED() + + def preparar_efd_icms_ipi(self, caminho_arquivo: Path) -> dict: + erros = self.validador.validar_efd(caminho_arquivo) + hash_md5 = self.validador.calcular_hash(caminho_arquivo) + return { + "arquivo": str(caminho_arquivo), + "tipo": "EFD_ICMS_IPI", + "valido": len(erros) == 0, + "erros": erros, + "hash_md5": hash_md5, + "tamanho_bytes": caminho_arquivo.stat().st_size if caminho_arquivo.exists() else 0, + "instrucoes": [ + "1. Abra o PGE (Programa Gerador de Escriturações) da Receita Federal", + "2. Selecione 'EFD ICMS IPI'", + "3. Clique em 'Importar Arquivo'", + "4. Selecione este arquivo", + "5. Valide e gere o arquivo assinado", + "6. Transmita pelo portal SPED (https://www.sped.fazenda.gov.br)", + ], + } + + def preparar_efd_contribuicoes(self, caminho_arquivo: Path) -> dict: + erros = self.validador.validar_efd(caminho_arquivo) + hash_md5 = self.validador.calcular_hash(caminho_arquivo) + return { + "arquivo": str(caminho_arquivo), + "tipo": "EFD_CONTRIBUICOES", + "valido": len(erros) == 0, + "erros": erros, + "hash_md5": hash_md5, + "instrucoes": [ + "1. Abra o PGE - EFD Contribuições", + "2. Selecione 'Importar Arquivo'", + "3. Selecione este arquivo TXT", + "4. Valide internamente", + "5. Transmita via portal SPED ou e-CAC", + ], + } + + def preparar_ecd(self, caminho_arquivo: Path) -> dict: + erros = self.validador.validar_ecd(caminho_arquivo) + hash_md5 = self.validador.calcular_hash(caminho_arquivo) + return { + "arquivo": str(caminho_arquivo), + "tipo": "ECD", + "valido": len(erros) == 0, + "erros": erros, + "hash_md5": hash_md5, + "instrucoes": [ + "1. Abra o SPED Contábil (Programa Validador e Assinador - PVA)", + "2. Selecione 'Importar'", + "3. Selecione este arquivo", + "4. Assine digitalmente com certificado A1 ou A3", + "5. Transmita pelo portal SPED", + ], + } + + +class TransmissorNFe: + """ + Transmissão de NF-e para a SEFAZ via webservice SOAP. + + Ambientes: + - Homologação: testes (CNPJ série 00 para NF-e) + - Produção: ambiente real + """ + + # URLs dos webservices SEFAZ por UF (ambiente produção) + URLS_SEFAZ: dict[str, dict[str, str]] = { + "SP": { + "NFeAutorizacao": "https://nfe.fazenda.sp.gov.br/ws/nfeautorizacao4.asmx", + "NFeRetAutorizacao": "https://nfe.fazenda.sp.gov.br/ws/nferetautorizacao4.asmx", + "NFeConsultaProtocolo": "https://nfe.fazenda.sp.gov.br/ws/nfeconsultaprotocolo4.asmx", + "NFeInutilizacao": "https://nfe.fazenda.sp.gov.br/ws/nfeinutilizacao4.asmx", + "NFeRecepcaoEvento": "https://nfe.fazenda.sp.gov.br/ws/nferecepcaoevento4.asmx", + "NFeStatusServico": "https://nfe.fazenda.sp.gov.br/ws/nfestatusservico4.asmx", + }, + "SVRS": { # SEFAZ Virtual RS (demais estados) + "NFeAutorizacao": "https://nfe.svrs.rs.gov.br/ws/NfeAutorizacao/NFeAutorizacao4.asmx", + "NFeRetAutorizacao": "https://nfe.svrs.rs.gov.br/ws/NfeRetAutorizacao/NFeRetAutorizacao4.asmx", + "NFeConsultaProtocolo": "https://nfe.svrs.rs.gov.br/ws/NfeConsulta2/NfeConsulta2.asmx", + "NFeStatusServico": "https://nfe.svrs.rs.gov.br/ws/NfeStatusServico/NfeStatusServico4.asmx", + "NFeRecepcaoEvento": "https://nfe.svrs.rs.gov.br/ws/recepcaoEvento/recepcaoEvento4.asmx", + }, + "SVAN": { # SEFAZ Virtual AN (Ambiente Nacional) + "NFeAutorizacao": "https://www.sefazvirtual.fazenda.gov.br/NFeAutorizacao4/NFeAutorizacao4.asmx", + "NFeRetAutorizacao": "https://www.sefazvirtual.fazenda.gov.br/NFeRetAutorizacao4/NFeRetAutorizacao4.asmx", + "NFeStatusServico": "https://www.sefazvirtual.fazenda.gov.br/NFeStatusServico4/NFeStatusServico4.asmx", + }, + } + + URLS_HOMOLOGACAO: dict[str, str] = { + "NFeAutorizacao": "https://homologacao.nfe.fazenda.sp.gov.br/ws/nfeautorizacao4.asmx", + "NFeStatusServico": "https://homologacao.nfe.fazenda.sp.gov.br/ws/nfestatusservico4.asmx", + } + + def __init__( + self, + uf: str = "SP", + ambiente: str = "2", # 1=Produção, 2=Homologação + certificado_path: Optional[str] = None, + certificado_senha: Optional[str] = None, + ): + self.uf = uf + self.ambiente = ambiente + self.certificado_path = certificado_path + self.certificado_senha = certificado_senha + + def _soap_envelope(self, body_xml: str, acao: str) -> str: + return f""" + + + + {self._cod_uf()} + 4.00 + + + + + {body_xml} + + +""" + + def _cod_uf(self) -> str: + ufs = { + "AC": "12", "AL": "27", "AP": "16", "AM": "13", "BA": "29", + "CE": "23", "DF": "53", "ES": "32", "GO": "52", "MA": "21", + "MT": "51", "MS": "50", "MG": "31", "PA": "15", "PB": "25", + "PR": "41", "PE": "26", "PI": "22", "RJ": "33", "RN": "24", + "RS": "43", "RO": "11", "RR": "14", "SC": "42", "SP": "35", + "SE": "28", "TO": "17", + } + return ufs.get(self.uf, "35") + + def verificar_status_servico(self) -> ResultadoTransmissao: + urls = self.URLS_SEFAZ.get(self.uf, self.URLS_SEFAZ["SVRS"]) + url = urls.get("NFeStatusServico", "") + + xml_status = f""" + {self.ambiente} + {self._cod_uf()} + STATUS +""" + + envelope = self._soap_envelope(xml_status, "NFeStatusServico4") + + try: + with httpx.Client(timeout=30.0) as client: + resp = client.post( + url, + content=envelope.encode("utf-8"), + headers={ + "Content-Type": 'application/soap+xml; charset=utf-8; action="http://www.portalfiscal.inf.br/nfe/wsdl/NFeStatusServico4/nfeStatusServicoNF"', + }, + ) + xml_resp = resp.text + # Parsear resposta + root = ET.fromstring(xml_resp) + ns = {"nfe": "http://www.portalfiscal.inf.br/nfe"} + cStat = root.find(".//nfe:cStat", ns) + xMotivo = root.find(".//nfe:xMotivo", ns) + dhRecbto = root.find(".//nfe:dhRecbto", ns) + + cod = cStat.text if cStat is not None else "" + msg = xMotivo.text if xMotivo is not None else "" + sucesso = cod == "107" + + return ResultadoTransmissao( + sucesso=sucesso, + codigo_retorno=cod, + mensagem=msg, + xml_resposta=xml_resp, + ) + except Exception as e: + return ResultadoTransmissao( + sucesso=False, + mensagem=f"Erro na conexão com SEFAZ: {e}", + ) + + def autorizar_nfe(self, xml_nfe_assinado: str) -> ResultadoTransmissao: + """ + Envia NF-e assinada digitalmente para autorização na SEFAZ. + O xml_nfe_assinado deve estar assinado com certificado digital válido. + """ + lote_xml = f""" + 1 + 1 + {xml_nfe_assinado} +""" + + urls = self.URLS_SEFAZ.get(self.uf, self.URLS_SEFAZ["SVRS"]) + url = urls.get("NFeAutorizacao", "") + + try: + with httpx.Client(timeout=60.0) as client: + resp = client.post( + url, + content=self._soap_envelope(lote_xml, "NFeAutorizacao4").encode("utf-8"), + headers={"Content-Type": "application/soap+xml; charset=utf-8"}, + ) + xml_resp = resp.text + root = ET.fromstring(xml_resp) + ns = {"nfe": "http://www.portalfiscal.inf.br/nfe"} + cStat = root.find(".//nfe:cStat", ns) + xMotivo = root.find(".//nfe:xMotivo", ns) + nProt = root.find(".//nfe:nProt", ns) + + cod = cStat.text if cStat is not None else "" + msg = xMotivo.text if xMotivo is not None else "" + prot = nProt.text if nProt is not None else "" + sucesso = cod in {"100", "150"} + + return ResultadoTransmissao( + sucesso=sucesso, + protocolo=prot, + codigo_retorno=cod, + mensagem=msg, + xml_resposta=xml_resp, + ) + except Exception as e: + return ResultadoTransmissao( + sucesso=False, + mensagem=f"Erro ao autorizar NF-e: {e}", + ) + + def cancelar_nfe( + self, + chave_acesso: str, + protocolo_autorizacao: str, + justificativa: str, + ) -> ResultadoTransmissao: + if len(justificativa) < 15: + return ResultadoTransmissao( + sucesso=False, + mensagem="Justificativa deve ter no mínimo 15 caracteres", + ) + + agora = datetime.now().strftime("%Y-%m-%dT%H:%M:%S-03:00") + xml_evento = f""" + 1 + + + {self._cod_uf()} + {self.ambiente} + {chave_acesso[6:20]} + {chave_acesso} + {agora} + 110111 + 1 + 1.00 + + Cancelamento + {protocolo_autorizacao} + {justificativa} + + + +""" + + urls = self.URLS_SEFAZ.get(self.uf, self.URLS_SEFAZ["SVRS"]) + url = urls.get("NFeRecepcaoEvento", "") + + try: + with httpx.Client(timeout=30.0) as client: + resp = client.post( + url, + content=self._soap_envelope(xml_evento, "NFeRecepcaoEvento4").encode("utf-8"), + headers={"Content-Type": "application/soap+xml; charset=utf-8"}, + ) + return ResultadoTransmissao( + sucesso=resp.status_code == 200, + mensagem=f"HTTP {resp.status_code}", + xml_resposta=resp.text, + ) + except Exception as e: + return ResultadoTransmissao(sucesso=False, mensagem=str(e)) diff --git a/src/validators/__init__.py b/src/validators/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/__pycache__/__init__.cpython-312.pyc b/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9633559ca467328c89fb0f2401bf0c8a45c81420 Binary files /dev/null and b/tests/__pycache__/__init__.cpython-312.pyc differ diff --git a/tests/__pycache__/test_calculators.cpython-312-pytest-8.3.2.pyc b/tests/__pycache__/test_calculators.cpython-312-pytest-8.3.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c8b4131262465ff4e5c7ca3fa15bcec5c0845900 Binary files /dev/null and b/tests/__pycache__/test_calculators.cpython-312-pytest-8.3.2.pyc differ diff --git a/tests/__pycache__/test_generators.cpython-312-pytest-8.3.2.pyc b/tests/__pycache__/test_generators.cpython-312-pytest-8.3.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..69021c3bf2bf9c1a9d1b2609ddf12cba62b2213b Binary files /dev/null and b/tests/__pycache__/test_generators.cpython-312-pytest-8.3.2.pyc differ diff --git a/tests/test_calculators.py b/tests/test_calculators.py new file mode 100644 index 0000000000000000000000000000000000000000..1573c677bbb39bf4a860d888b55152befea81b9e --- /dev/null +++ b/tests/test_calculators.py @@ -0,0 +1,418 @@ + +import pytest +from decimal import Decimal + +from src.calculators import arred +from src.calculators.icms import ( + ParametrosICMS, + calcular_icms, + calcular_icms_apuracao, + calcular_icms_credito_entrada, +) +from src.calculators.ipi import ParametrosIPI, calcular_ipi, obter_aliquota_ipi_ncm +from src.calculators.pis_cofins import ( + CreditoPisCofins, + ReceitaBruta, + calcular_pis_cofins, + calcular_pis_cofins_retencao, +) +from src.calculators.irpj_csll import ( + calcular_csll_lucro_presumido, + calcular_csll_lucro_real, + calcular_irpj_lucro_presumido, + calcular_irpj_lucro_real, + calcular_irpj_estimativa_mensal, +) +from src.calculators.iss import ParametrosISS, calcular_iss, requer_retencao_iss +from src.calculators.simples_nacional import ( + calcular_das, + calcular_fator_r, + calcular_pgdas, + determinar_anexo_servicos, +) +from src.fiscal.entities import RegimeTributario + + +# --------------------------------------------------------------------------- +# ICMS +# --------------------------------------------------------------------------- + +class TestICMS: + + def test_icms_basico(self): + r = calcular_icms(ParametrosICMS( + valor_mercadoria=Decimal("10000"), + aliquota=Decimal("18"), + )) + assert r.base_calculo == Decimal("10000.00") + assert r.valor_icms == Decimal("1800.00") + assert r.valor_total_icms == Decimal("1800.00") + + def test_icms_com_frete(self): + r = calcular_icms(ParametrosICMS( + valor_mercadoria=Decimal("10000"), + aliquota=Decimal("18"), + frete=Decimal("500"), + )) + assert r.base_calculo == Decimal("10500.00") + assert r.valor_icms == Decimal("1890.00") + + def test_icms_cst_40_isento(self): + r = calcular_icms(ParametrosICMS( + valor_mercadoria=Decimal("10000"), + aliquota=Decimal("18"), + cst="040", + )) + assert r.valor_icms == Decimal("0") + assert r.valor_total_icms == Decimal("0") + + def test_icms_reducao_base(self): + r = calcular_icms(ParametrosICMS( + valor_mercadoria=Decimal("10000"), + aliquota=Decimal("18"), + cst="020", + reducao_base=Decimal("30"), + )) + assert r.base_calculo == Decimal("7000.00") + assert r.valor_icms == Decimal("1260.00") + + def test_icms_st_com_mva(self): + r = calcular_icms(ParametrosICMS( + valor_mercadoria=Decimal("1000"), + aliquota=Decimal("12"), + cst="010", + uf_origem="SP", + uf_destino="MG", + mva=Decimal("40"), + aliq_st=Decimal("18"), + )) + assert r.valor_icms > 0 + assert r.valor_st > 0 + assert r.base_st == Decimal("1400.00") + + def test_icms_apuracao(self): + debitos = [Decimal("1000"), Decimal("2000")] + creditos = [Decimal("500"), Decimal("800")] + r = calcular_icms_apuracao(debitos, creditos) + assert r.valor_a_recolher == Decimal("1700.00") + assert r.detalhes["saldo_credor"] == Decimal("0") + + def test_icms_apuracao_saldo_credor(self): + debitos = [Decimal("500")] + creditos = [Decimal("1000")] + r = calcular_icms_apuracao(debitos, creditos) + assert r.valor_a_recolher == Decimal("0") + assert r.detalhes["saldo_credor"] == Decimal("500.00") + + def test_credito_icms_entrada(self): + credito = calcular_icms_credito_entrada( + valor_nf=Decimal("5000"), + aliquota=Decimal("12"), + ) + assert credito == Decimal("600.00") + + def test_icms_difal(self): + r = calcular_icms(ParametrosICMS( + valor_mercadoria=Decimal("10000"), + aliquota=Decimal("12"), + uf_origem="SP", + uf_destino="BA", + calcular_difal=True, + consumidor_final=True, + )) + assert r.valor_difal_destino > 0 or r.valor_difal_origem > 0 + + +# --------------------------------------------------------------------------- +# IPI +# --------------------------------------------------------------------------- + +class TestIPI: + + def test_ipi_basico(self): + r = calcular_ipi(ParametrosIPI( + valor_produtos=Decimal("10000"), + aliquota=Decimal("5"), + )) + assert r.base_calculo == Decimal("10000.00") + assert r.valor_tributo == Decimal("500.00") + + def test_ipi_cst_52_isento(self): + r = calcular_ipi(ParametrosIPI( + valor_produtos=Decimal("5000"), + aliquota=Decimal("10"), + cst="52", + )) + assert r.valor_tributo == Decimal("0") + + def test_ipi_cst_50_tributado(self): + r = calcular_ipi(ParametrosIPI( + valor_produtos=Decimal("2000"), + aliquota=Decimal("15"), + cst="50", + )) + assert r.valor_tributo == Decimal("300.00") + + def test_aliquota_ipi_ncm_farmaceutico(self): + aliq = obter_aliquota_ipi_ncm("30050000") # Capítulo 30 = farmacêutico + assert aliq == Decimal("0") + + def test_aliquota_ipi_ncm_automovel(self): + aliq = obter_aliquota_ipi_ncm("87032310") # Capítulo 87 = automóveis + assert aliq == Decimal("25") + + def test_ipi_com_frete(self): + r = calcular_ipi(ParametrosIPI( + valor_produtos=Decimal("1000"), + aliquota=Decimal("10"), + frete=Decimal("100"), + )) + assert r.base_calculo == Decimal("1100.00") + assert r.valor_tributo == Decimal("110.00") + + +# --------------------------------------------------------------------------- +# PIS / COFINS +# --------------------------------------------------------------------------- + +class TestPisCofins: + + def test_lucro_presumido_cumulativo(self): + receitas = [ReceitaBruta("Vendas", Decimal("100000"))] + r = calcular_pis_cofins(receitas, RegimeTributario.LUCRO_PRESUMIDO) + assert r.regime == "Cumulativo" + assert r.aliq_pis == Decimal("0.65") + assert r.aliq_cofins == Decimal("3.00") + assert r.pis_a_recolher == Decimal("650.00") + assert r.cofins_a_recolher == Decimal("3000.00") + + def test_lucro_real_nao_cumulativo(self): + receitas = [ReceitaBruta("Serviços", Decimal("50000"))] + r = calcular_pis_cofins(receitas, RegimeTributario.LUCRO_REAL) + assert r.regime == "Não-Cumulativo" + assert r.aliq_pis == Decimal("1.65") + assert r.aliq_cofins == Decimal("7.60") + assert r.pis_a_recolher == Decimal("825.00") + assert r.cofins_a_recolher == Decimal("3800.00") + + def test_creditos_reduzem_valor_a_recolher(self): + receitas = [ReceitaBruta("Vendas", Decimal("100000"))] + creditos = [CreditoPisCofins( + descricao="Compras para revenda", + base_calculo=Decimal("60000"), + aliquota_pis=Decimal("1.65"), + aliquota_cofins=Decimal("7.60"), + )] + r = calcular_pis_cofins(receitas, RegimeTributario.LUCRO_REAL, creditos) + assert r.creditos_pis == Decimal("990.00") + assert r.creditos_cofins == Decimal("4560.00") + assert r.pis_a_recolher == Decimal("660.00") + # debito=7600 - credito=4560 = 3040 (créditos reduzem mas não zeram) + assert r.cofins_a_recolher == Decimal("3040.00") + + def test_cst_07_nao_tributado(self): + receitas = [ + ReceitaBruta("Exportação", Decimal("50000"), cst_pis="07", cst_cofins="07") + ] + r = calcular_pis_cofins(receitas, RegimeTributario.LUCRO_REAL) + assert r.pis_a_recolher == Decimal("0.00") + assert r.cofins_a_recolher == Decimal("0.00") + + def test_retencao_servicos(self): + r = calcular_pis_cofins_retencao(valor_servico=Decimal("10000")) + assert r["valor_pis"] == Decimal("65.00") + assert r["valor_cofins"] == Decimal("300.00") + assert r["total_retido"] > Decimal("10") + + def test_retencao_dispensada_valor_baixo(self): + r = calcular_pis_cofins_retencao(valor_servico=Decimal("100")) + assert r["total_retido"] == Decimal("0") + assert "dispensada" in r["observacao"] + + +# --------------------------------------------------------------------------- +# IRPJ / CSLL +# --------------------------------------------------------------------------- + +class TestIRPJCSLL: + + def test_irpj_lucro_presumido_comercio(self): + r = calcular_irpj_lucro_presumido( + receita_bruta=Decimal("100000"), + atividade="venda_mercadorias", + ) + # Base = 100.000 × 8% = 8.000 → IRPJ = 8.000 × 15% = 1.200 + assert r.base_calculo == Decimal("8000.00") + assert r.valor_base == Decimal("1200.00") + assert r.irpj_a_recolher == Decimal("1200.00") + assert r.valor_adicional == Decimal("0") + + def test_irpj_lucro_presumido_servicos(self): + r = calcular_irpj_lucro_presumido( + receita_bruta=Decimal("100000"), + atividade="servicos_em_geral", + ) + # Base = 100.000 × 32% = 32.000 → IRPJ = 32.000 × 15% = 4.800 + assert r.base_calculo == Decimal("32000.00") + assert r.valor_base == Decimal("4800.00") + + def test_irpj_adicional_10pct(self): + r = calcular_irpj_lucro_presumido( + receita_bruta=Decimal("1000000"), + atividade="venda_mercadorias", + ) + # Base = 80.000 → excede 60.000 em 20.000 → adicional = 2.000 + assert r.base_calculo == Decimal("80000.00") + assert r.base_adicional == Decimal("20000.00") + assert r.valor_adicional == Decimal("2000.00") + + def test_irpj_lucro_real_compensacao(self): + r = calcular_irpj_lucro_real( + lucro_antes_ir=Decimal("100000"), + compensacoes_prejuizo=Decimal("50000"), + ) + # Limite compensação = 30% de 100.000 = 30.000 + assert r.base_calculo == Decimal("70000.00") + + def test_csll_lucro_presumido_comercio(self): + r = calcular_csll_lucro_presumido( + receita_bruta=Decimal("100000"), + atividade="comercio_industria", + ) + # Base = 100.000 × 12% = 12.000 → CSLL = 12.000 × 9% = 1.080 + assert r.base_calculo == Decimal("12000.00") + assert r.valor_csll == Decimal("1080.00") + + def test_csll_lucro_real(self): + r = calcular_csll_lucro_real(lucro_antes_csll=Decimal("50000")) + assert r.base_calculo == Decimal("50000.00") + assert r.valor_csll == Decimal("4500.00") + + def test_estimativa_mensal(self): + r = calcular_irpj_estimativa_mensal( + receita_bruta_mes=Decimal("500000"), + atividade="venda_mercadorias", + ) + assert r["base_estimada"] == Decimal("40000.00") + assert r["irpj_aliquota_base"] == Decimal("6000.00") + assert r["irpj_adicional"] == Decimal("2000.00") + + +# --------------------------------------------------------------------------- +# ISS +# --------------------------------------------------------------------------- + +class TestISS: + + def test_iss_basico(self): + r = calcular_iss(ParametrosISS( + valor_servico=Decimal("10000"), + codigo_servico="17", + aliquota_municipal=Decimal("2"), + )) + assert r.base_calculo == Decimal("10000") + assert r.aliquota == Decimal("2") + assert r.valor_tributo == Decimal("200.00") + + def test_iss_aliquota_minima(self): + r = calcular_iss(ParametrosISS( + valor_servico=Decimal("5000"), + codigo_servico="17", + aliquota_municipal=Decimal("1"), # Menor que o mínimo de 2% + )) + assert r.aliquota == Decimal("2") + + def test_iss_aliquota_maxima(self): + r = calcular_iss(ParametrosISS( + valor_servico=Decimal("5000"), + codigo_servico="22", + aliquota_municipal=Decimal("6"), # Maior que o máximo de 5% + )) + assert r.aliquota == Decimal("5") + + def test_requer_retencao_ti(self): + assert requer_retencao_iss("01") is True # TI sempre retém + + def test_requer_retencao_alimentacao(self): + assert requer_retencao_iss("99") is False + + +# --------------------------------------------------------------------------- +# Simples Nacional +# --------------------------------------------------------------------------- + +class TestSimplesNacional: + + def test_das_anexo_i_faixa_1(self): + r = calcular_das( + receita_bruta_mes=Decimal("15000"), + receita_bruta_acumulada_12m=Decimal("100000"), + anexo="I", + ) + assert r.faixa == 1 + assert r.aliq_nominal == Decimal("4.00") + assert r.aliq_efetiva == Decimal("4.00") + assert r.valor_das == Decimal("600.00") + + def test_das_anexo_i_faixa_2(self): + r = calcular_das( + receita_bruta_mes=Decimal("30000"), + receita_bruta_acumulada_12m=Decimal("250000"), + anexo="I", + ) + assert r.faixa == 2 + assert r.aliq_efetiva < r.aliq_nominal # Alíq efetiva < nominal por dedução + + def test_das_anexo_iii_servicos(self): + r = calcular_das( + receita_bruta_mes=Decimal("10000"), + receita_bruta_acumulada_12m=Decimal("150000"), + anexo="III", + ) + assert r.faixa == 1 + assert r.aliq_nominal == Decimal("6.00") + + def test_fator_r(self): + fator = calcular_fator_r( + folha_12m=Decimal("84000"), + rec_12m=Decimal("300000"), + ) + assert fator == Decimal("28.0000") + + def test_determinar_anexo_advocacia_fator_r_baixo(self): + anexo = determinar_anexo_servicos(Decimal("20"), "advocacia") + assert anexo == "V" + + def test_determinar_anexo_advocacia_fator_r_alto(self): + anexo = determinar_anexo_servicos(Decimal("30"), "advocacia") + assert anexo == "III" + + def test_pgdas_multiplas_atividades(self): + r = calcular_pgdas( + cnpj="11222333000181", + periodo="2024-01", + receitas_por_atividade=[ + {"valor": "20000", "anexo": "I"}, + {"valor": "10000", "anexo": "III"}, + ], + receita_acumulada_12m=Decimal("200000"), + ) + assert r.das_total > 0 + assert len(r.das_por_estabelecimento) == 2 + assert r.data_vencimento is not None + + def test_das_dentro_limite(self): + r = calcular_das( + receita_bruta_mes=Decimal("400000"), + receita_bruta_acumulada_12m=Decimal("4800000"), + anexo="I", + ) + assert r.dentro_limite is True + + def test_das_acima_limite(self): + r = calcular_das( + receita_bruta_mes=Decimal("500000"), + receita_bruta_acumulada_12m=Decimal("5000000"), + anexo="I", + ) + assert r.dentro_limite is False diff --git a/tests/test_generators.py b/tests/test_generators.py new file mode 100644 index 0000000000000000000000000000000000000000..f62ca3ae6b683e9c78022ea5dcaa394389bbd3bd --- /dev/null +++ b/tests/test_generators.py @@ -0,0 +1,787 @@ +"""Testes dos geradores de arquivos SPED e XML fiscal.""" +import pytest +import re +from datetime import date +from decimal import Decimal +from pathlib import Path + +from src.fiscal.entities import ( + Contato, + Empresa, + Endereco, + ItemNotaFiscal, + LancamentoContabil, + NotaFiscal, + PeriodoApuracao, + Produto, + RegimeTributario, + TipoEmpresa, + UF, +) +from src.generators.sped_writer import ArquivoSPED, RegistroSPED, criar_registro +from src.generators.efd_icms_ipi import GeradorEFDICMSIPI +from src.generators.efd_contribuicoes import GeradorEFDContribuicoes +from src.generators.ecd import GeradorECD, PlanoContas +from src.generators.ecf import DadosLucroPresumido, DadosLucroReal, GeradorECF +from src.generators.nfe_xml import GeradorNFeXML +from src.generators.efd_reinf import ( + EventoR2010, + GeradorEFDReinf, + PrestadorServico, +) +from src.generators.esocial import ( + GeradorESocial, + Trabalhador, + VinculoEmpregaticio, + FolhaPagamento, + ItemFolha, + RUBRICAS_PADRAO, +) +from src.generators.dctf import DebitoDCTF, GeradorDCTF, ItemDCTF, montar_dctf_do_periodo +from src.generators.cte import GeradorCTe, CargaCTe, ParteCTe, DadosTransporte, DocumentoReferenciado +from src.generators.nfce_xml import GeradorNFCeXML, ConsumidorNFCe +from src.generators.mdfe import GeradorMDFe, MunicipioDescarga, DocumentoMDFe, ConductorMDFe, SeguroMDFe +from src.generators.dirf import GeradorDIRF, BeneficiarioDIRF, ResponsavelDIRF +from src.generators.defis import GeradorDEFIS, ReceitaMensalDEFIS, SocioDEFIS +from src.generators.destda import GeradorDeSTDA, OperacaoSTDeSTDA +from src.generators.gia import GeradorGIA, ApuracaoGIA, ApuracaoGIAST + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + +@pytest.fixture +def empresa_sp(): + return Empresa( + cnpj="11222333000181", + razao_social="EMPRESA TESTE LTDA", + nome_fantasia="TESTE", + ie="111111111111", + regime_tributario=RegimeTributario.LUCRO_PRESUMIDO, + tipo_empresa=TipoEmpresa.COMERCIO, + endereco=Endereco( + logradouro="Av. Paulista", + numero="1000", + bairro="Bela Vista", + municipio="São Paulo", + uf=UF.SP, + cep="01310100", + cod_municipio="3550308", + ), + contato=Contato(telefone="1133334444", email="teste@teste.com"), + ) + + +@pytest.fixture +def cliente_sp(): + return Empresa( + cnpj="22333444000181", + razao_social="CLIENTE TESTE SA", + regime_tributario=RegimeTributario.LUCRO_PRESUMIDO, + tipo_empresa=TipoEmpresa.COMERCIO, + endereco=Endereco( + logradouro="Rua das Flores", + numero="100", + bairro="Centro", + municipio="Campinas", + uf=UF.SP, + cep="13010050", + cod_municipio="3509502", + ), + ) + + +@pytest.fixture +def produto_padrao(): + return Produto( + codigo="PROD001", + descricao="PRODUTO TESTE", + ncm="84713012", + unidade="UN", + aliq_icms=Decimal("18"), + aliq_ipi=Decimal("5"), + cst_icms="000", + cst_ipi="50", + cst_pis="01", + cst_cofins="01", + ) + + +@pytest.fixture +def nota_fiscal(empresa_sp, cliente_sp, produto_padrao): + item = ItemNotaFiscal( + numero_item=1, + produto=produto_padrao, + cfop="5102", + quantidade=Decimal("10"), + valor_unitario=Decimal("100.00"), + ) + return NotaFiscal( + numero="000001", + serie="001", + data_emissao=date(2024, 1, 15), + data_saida_entrada=date(2024, 1, 15), + emitente=empresa_sp, + destinatario=cliente_sp, + natureza_operacao="VENDA DE MERCADORIA", + itens=[item], + chave_acesso="35240111222333000181550010000000011000000015", + ) + + +@pytest.fixture +def periodo(empresa_sp, nota_fiscal): + return PeriodoApuracao( + data_inicio=date(2024, 1, 1), + data_fim=date(2024, 1, 31), + empresa=empresa_sp, + notas_saida=[nota_fiscal], + ) + + +# --------------------------------------------------------------------------- +# Testes do escritor base SPED +# --------------------------------------------------------------------------- + +class TestSpedWriter: + + def test_criar_registro_formato_pipe(self): + linha = criar_registro("C100", "1", "0", "12345678") + assert linha.startswith("|C100|") + assert linha.endswith("|\n") + + def test_criar_registro_data(self): + linha = criar_registro("0000", date(2024, 1, 1)) + assert "01012024" in linha + + def test_criar_registro_decimal(self): + linha = criar_registro("E110", Decimal("1234.56")) + assert "1234,56" in linha + + def test_criar_registro_none_vazio(self): + linha = criar_registro("X001", None, "") + parts = linha.strip("|").split("|") + assert parts[1] == "" + assert parts[2] == "" + + def test_arquivo_sped_contagem_linhas(self): + arq = ArquivoSPED("teste.txt") + arq.adicionar_linha_raw(criar_registro("0000", "017")) + arq.adicionar_linha_raw(criar_registro("0001", "0")) + assert arq.total_linhas() == 2 + + def test_registro_sped_builder(self): + reg = RegistroSPED("C100") + reg.add("1").add("0").add("12345678000195") + linha = reg.to_line() + assert "|C100|1|0|12345678000195|" in linha + + +# --------------------------------------------------------------------------- +# Testes EFD ICMS/IPI +# --------------------------------------------------------------------------- + +class TestEFDICMSIPI: + + def test_gera_arquivo_existente(self, periodo, tmp_path): + gerador = GeradorEFDICMSIPI(periodo) + caminho = gerador.gerar(tmp_path) + assert caminho.exists() + assert caminho.stat().st_size > 0 + + def test_registro_0000_presente(self, periodo, tmp_path): + gerador = GeradorEFDICMSIPI(periodo) + caminho = gerador.gerar(tmp_path) + conteudo = caminho.read_text() + assert "|0000|" in conteudo + + def test_cnpj_no_registro_0000(self, periodo, tmp_path): + gerador = GeradorEFDICMSIPI(periodo) + caminho = gerador.gerar(tmp_path) + conteudo = caminho.read_text() + assert "11222333000181" in conteudo + + def test_registro_c100_presente(self, periodo, tmp_path): + gerador = GeradorEFDICMSIPI(periodo) + caminho = gerador.gerar(tmp_path) + conteudo = caminho.read_text() + assert "|C100|" in conteudo + + def test_registro_e110_presente(self, periodo, tmp_path): + gerador = GeradorEFDICMSIPI(periodo) + caminho = gerador.gerar(tmp_path) + conteudo = caminho.read_text() + assert "|E110|" in conteudo + + def test_registro_9999_presente(self, periodo, tmp_path): + gerador = GeradorEFDICMSIPI(periodo) + caminho = gerador.gerar(tmp_path) + conteudo = caminho.read_text() + assert "|9999|" in conteudo + + def test_todas_linhas_sao_pipe_delimited(self, periodo, tmp_path): + gerador = GeradorEFDICMSIPI(periodo) + caminho = gerador.gerar(tmp_path) + for linha in caminho.read_text().splitlines(): + assert linha.startswith("|") and linha.endswith("|"), f"Linha inválida: {linha}" + + def test_nome_arquivo_correto(self, periodo): + gerador = GeradorEFDICMSIPI(periodo) + assert "11222333000181" in gerador._arquivo.nome_arquivo + assert "202401" in gerador._arquivo.nome_arquivo + + +# --------------------------------------------------------------------------- +# Testes EFD Contribuições +# --------------------------------------------------------------------------- + +class TestEFDContribuicoes: + + def test_gera_arquivo(self, periodo, tmp_path): + gerador = GeradorEFDContribuicoes(periodo) + caminho = gerador.gerar(tmp_path) + assert caminho.exists() + + def test_registro_m200_presente(self, periodo, tmp_path): + gerador = GeradorEFDContribuicoes(periodo) + caminho = gerador.gerar(tmp_path) + assert "|M200|" in caminho.read_text() + + def test_registro_m600_presente(self, periodo, tmp_path): + gerador = GeradorEFDContribuicoes(periodo) + caminho = gerador.gerar(tmp_path) + assert "|M600|" in caminho.read_text() + + +# --------------------------------------------------------------------------- +# Testes ECD +# --------------------------------------------------------------------------- + +class TestECD: + + def test_gera_arquivo(self, periodo, tmp_path): + gerador = GeradorECD(periodo) + caminho = gerador.gerar(tmp_path) + assert caminho.exists() + assert caminho.stat().st_size > 100 + + def test_plano_contas_no_arquivo(self, periodo, tmp_path): + gerador = GeradorECD(periodo) + caminho = gerador.gerar(tmp_path) + assert "|I050|" in caminho.read_text() + + def test_registro_i001_presente(self, periodo, tmp_path): + gerador = GeradorECD(periodo) + caminho = gerador.gerar(tmp_path) + assert "|I001|" in caminho.read_text() + + def test_lancamentos_contabeis(self, periodo, empresa_sp, tmp_path): + periodo.lancamentos = [ + LancamentoContabil( + data=date(2024, 1, 5), + numero_lancamento="000001", + historico="Venda de mercadorias", + debito_conta="1.1.2.01", + credito_conta="4.1.01", + valor=Decimal("1000.00"), + ) + ] + gerador = GeradorECD(periodo) + caminho = gerador.gerar(tmp_path) + assert "|I200|" in caminho.read_text() + assert "|I250|" in caminho.read_text() + + def test_plano_contas_customizado(self, periodo, tmp_path): + plano = [ + PlanoContas("1", "ATIVO", 1, "S", "D"), + PlanoContas("1.01", "CAIXA", 2, "A", "D"), + ] + gerador = GeradorECD(periodo, plano_contas=plano) + caminho = gerador.gerar(tmp_path) + assert "CAIXA" in caminho.read_text() + + +# --------------------------------------------------------------------------- +# Testes ECF +# --------------------------------------------------------------------------- + +class TestECF: + + def test_gera_ecf_lucro_presumido(self, empresa_sp, tmp_path): + dados = DadosLucroPresumido( + receita_venda_mercadorias=Decimal("500000"), + receita_prestacao_servicos=Decimal("100000"), + ) + gerador = GeradorECF(empresa_sp, 2023, dados_lp=dados) + caminho = gerador.gerar(tmp_path) + assert caminho.exists() + + def test_registro_0000_ecf(self, empresa_sp, tmp_path): + dados = DadosLucroPresumido(receita_venda_mercadorias=Decimal("100000")) + gerador = GeradorECF(empresa_sp, 2023, dados_lp=dados) + caminho = gerador.gerar(tmp_path) + assert "|0000|" in caminho.read_text() + + def test_gera_ecf_lucro_real(self, tmp_path): + empresa = Empresa( + cnpj="11222333000181", + razao_social="EMPRESA LR LTDA", + regime_tributario=RegimeTributario.LUCRO_REAL, + tipo_empresa=TipoEmpresa.COMERCIO, + endereco=Endereco( + logradouro="Rua X", numero="1", bairro="B", + municipio="SP", uf=UF.SP, cep="01310100", + ), + ) + dados = DadosLucroReal( + lucro_contabil=Decimal("200000"), + adicoes=Decimal("10000"), + exclusoes=Decimal("5000"), + ) + gerador = GeradorECF(empresa, 2023, dados_lr=dados) + caminho = gerador.gerar(tmp_path) + assert "|L020|" in caminho.read_text() + + +# --------------------------------------------------------------------------- +# Testes NF-e XML +# --------------------------------------------------------------------------- + +class TestNFeXML: + + def test_gera_xml_valido(self, nota_fiscal, tmp_path): + gerador = GeradorNFeXML(nota_fiscal) + xml = gerador.gerar_xml() + assert xml.startswith("" in xml + assert "" in xml or "" in xml + + def test_chave_acesso_44_digitos(self, nota_fiscal): + gerador = GeradorNFeXML(nota_fiscal) + chave = gerador._gerar_chave() + assert len(chave) == 44 + assert chave.isdigit() + + def test_salva_arquivo_xml(self, nota_fiscal, tmp_path): + gerador = GeradorNFeXML(nota_fiscal) + caminho = gerador.salvar(tmp_path) + assert caminho.exists() + assert caminho.suffix == ".xml" + + +# --------------------------------------------------------------------------- +# Testes EFD-Reinf +# --------------------------------------------------------------------------- + +class TestEFDReinf: + + def test_gera_r2010(self): + gerador = GeradorEFDReinf("11222333000181") + evt = EventoR2010( + cnpj_tomador="11222333000181", + periodo="2024-01", + prestadores=[ + PrestadorServico( + cnpj_cpf="22333444000181", + nome="PRESTADOR TESTE", + valor_bruto=Decimal("10000"), + valor_bc_csll=Decimal("10000"), + valor_bc_irrf=Decimal("10000"), + valor_bc_pis=Decimal("10000"), + valor_bc_cofins=Decimal("10000"), + ) + ], + ) + xml = gerador.gerar_r2010(evt) + assert "evtServTom" in xml + assert "22333444000181" in xml + + def test_gera_r2099(self): + gerador = GeradorEFDReinf("11222333000181") + xml = gerador.gerar_r2099("2024-01") + assert "evtFechamento" in xml + + def test_salva_eventos(self, tmp_path): + gerador = GeradorEFDReinf("11222333000181") + arquivos = gerador.salvar_eventos(tmp_path, periodo="2024-01") + assert len(arquivos) >= 2 + assert all(a.exists() for a in arquivos) + + +# --------------------------------------------------------------------------- +# Testes e-Social +# --------------------------------------------------------------------------- + +class TestESocial: + + def test_gera_s2200_admissao(self): + gerador = GeradorESocial("11222333000181") + trab = Trabalhador( + cpf="12345678909", + nome="JOAO DA SILVA", + data_nascimento=date(1985, 3, 15), + ) + vinculo = VinculoEmpregaticio( + trabalhador=trab, + matricula="001", + data_admissao=date(2024, 1, 2), + salario_base=Decimal("3000"), + ) + xml = gerador.gerar_s2200(vinculo) + assert "evtAdmissao" in xml + assert "JOAO DA SILVA" in xml + + def test_gera_s1200_folha(self): + gerador = GeradorESocial("11222333000181") + trab = Trabalhador(cpf="12345678909", nome="MARIA SOUZA", data_nascimento=date(1990, 6, 20)) + rubrica = RUBRICAS_PADRAO[0] + folha = FolhaPagamento( + trabalhador=trab, + matricula="002", + periodo="2024-01", + itens=[ItemFolha(rubrica=rubrica, valor=Decimal("3000"))], + base_cp=Decimal("3000"), + base_fgts=Decimal("3000"), + valor_fgts=Decimal("240"), + ) + xml = gerador.gerar_s1200(folha) + assert "evtRemun" in xml + assert "3000.00" in xml + + def test_gera_s1299_fechamento(self): + gerador = GeradorESocial("11222333000181") + xml = gerador.gerar_s1299("2024-01") + assert "evtFechamento" in xml + + +# --------------------------------------------------------------------------- +# Testes DCTF +# --------------------------------------------------------------------------- + +class TestDCTF: + + def test_gera_xml_dctf(self, empresa_sp, tmp_path): + item = ItemDCTF(debito=DebitoDCTF( + codigo_receita="2089", + periodo_apuracao="2024-01", + valor_debito=Decimal("5000"), + )) + gerador = GeradorDCTF(empresa_sp, "2024-01", [item]) + xml = gerador.gerar_xml() + assert "