|
|
| 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 |
| 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 |
| tipo: str = "PF" |
| pais_domicilio: str = "105" |
|
|
|
|
| @dataclass |
| class EmpregadoDEFIS: |
| competencia: str |
| 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 |
|
|
| |
| |
| |
|
|
| @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) |
|
|
| |
| |
| |
|
|
| def gerar_xml(self) -> str: |
| """Gera o XML DEFIS para transmissão.""" |
| root = ET.Element("DEFIS") |
| root.set("versao", self.VERSAO) |
|
|
| |
| 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 '<?xml version="1.0" encoding="UTF-8"?>\n' + ET.tostring(root, encoding="unicode") |
|
|
| |
| |
| |
|
|
| 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 |
|
|
| |
| |
| |
|
|
| 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) |
|
|