File size: 7,173 Bytes
d9d7b41 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
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)
|