Speed / src /generators /dirf.py
LesterCerioli's picture
Building Speed LLM
d9d7b41
Raw
History Blame Contribute Delete
7.17 kB
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)