|
|
| 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 |
|
|
|
|
| |
| |
| |
|
|
| @dataclass |
| class BeneficiarioDIRF: |
| """Beneficiário de rendimentos sujeitos à retenção na fonte.""" |
| cpf_cnpj: str |
| nome: str |
| tipo: str |
| cod_receita: str |
| rendimentos_por_mes: dict[int, Decimal] = field(default_factory=dict) |
| ir_retido_por_mes: dict[int, Decimal] = field(default_factory=dict) |
| |
| data_admissao: Optional[date] = None |
| data_demissao: Optional[date] = None |
| natureza: str = "A" |
|
|
|
|
| @dataclass |
| class ResponsavelDIRF: |
| cpf: str |
| nome: str |
| cargo: str |
| ddd: str |
| telefone: str |
|
|
|
|
| |
| |
| |
|
|
| 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, |
| 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 |
|
|
| |
| |
| |
|
|
| @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 |
|
|
| |
| |
| |
|
|
| 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 |
|
|
| 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) |
|
|