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)