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 "