|
|
| 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 |
|
|
|
|
| COD_UF = { |
| "AC": "12", "AL": "27", "AM": "13", "AP": "16", "BA": "29", |
| "CE": "23", "DF": "53", "ES": "32", "GO": "52", "MA": "21", |
| "MG": "31", "MS": "50", "MT": "51", "PA": "15", "PB": "25", |
| "PE": "26", "PI": "22", "PR": "41", "RJ": "33", "RN": "24", |
| "RO": "11", "RR": "14", "RS": "43", "SC": "42", "SE": "28", |
| "SP": "35", "TO": "17", |
| } |
|
|
|
|
| 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(v: Decimal) -> str: |
| return f"{v:.2f}" |
|
|
|
|
| @dataclass |
| class OperacaoSTDeSTDA: |
| """Operação de ICMS-ST, DIFAL ou antecipação do Simples Nacional.""" |
| uf_origem: str |
| uf_destino: str |
| tipo: str |
| base_calculo: Decimal |
| aliquota: Decimal |
| valor_imposto: Decimal |
| valor_pago: Decimal = Decimal("0") |
|
|
| def __post_init__(self): |
| if self.valor_pago == Decimal("0"): |
| self.valor_pago = self.valor_imposto |
|
|
|
|
| class GeradorDeSTDA: |
| |
|
|
| VERSAO = "1.1" |
|
|
| def __init__( |
| self, |
| empresa: Empresa, |
| periodo: str, |
| operacoes: list[OperacaoSTDeSTDA], |
| uf_declarante: str = "SP", |
| retificadora: bool = False, |
| ): |
| self.empresa = empresa |
| self.periodo = periodo |
| self.operacoes = operacoes |
| self.uf_declarante = uf_declarante.upper() |
| self.retificadora = retificadora |
|
|
| |
| def data_vencimento(self) -> date: |
| ano, mes = map(int, self.periodo.split("-")) |
| if mes == 12: |
| return date(ano + 1, 1, 20) |
| return date(ano, mes + 1, 20) |
|
|
| @property |
| def _ops_st(self) -> list[OperacaoSTDeSTDA]: |
| return [o for o in self.operacoes if o.tipo == "ST"] |
|
|
| @property |
| def _ops_difal(self) -> list[OperacaoSTDeSTDA]: |
| return [o for o in self.operacoes if o.tipo == "DIFAL"] |
|
|
| @property |
| def _ops_antec(self) -> list[OperacaoSTDeSTDA]: |
| return [o for o in self.operacoes if o.tipo == "ANTEC"] |
|
|
| def _total(self, ops: list[OperacaoSTDeSTDA], campo: str) -> Decimal: |
| return sum(getattr(o, campo) for o in ops) |
|
|
| |
| def gerar_xml(self) -> str: |
| ano, mes = self.periodo.split("-") |
| ultimo_dia = self._ultimo_dia(int(ano), int(mes)) |
|
|
| root = ET.Element("DeSTDA") |
| root.set("versao", self.VERSAO) |
| root.set("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") |
|
|
| |
| inf = _sub(root, "infDeSTDA") |
| _sub(inf, "CNPJ", self.empresa.cnpj) |
| _sub(inf, "cUF", COD_UF.get(self.uf_declarante, "35")) |
| _sub(inf, "dtInicio", f"{ano}-{mes}-01") |
| _sub(inf, "dtFim", f"{ano}-{mes}-{ultimo_dia}") |
| _sub(inf, "indRetificadora", "S" if self.retificadora else "N") |
| _sub(inf, "dtEnvio", date.today().strftime("%Y-%m-%d")) |
|
|
| |
| self._bloco_st(root) |
| self._bloco_difal(root) |
| self._bloco_antec(root) |
|
|
| |
| tot = _sub(root, "totais") |
| vst = self._total(self._ops_st, "valor_imposto") |
| vdifal = self._total(self._ops_difal, "valor_imposto") |
| vantec = self._total(self._ops_antec, "valor_imposto") |
| vpago = self._total(self.operacoes, "valor_pago") |
| _sub(tot, "vST_Total", _fmt(vst)) |
| _sub(tot, "vDIFAL_Total", _fmt(vdifal)) |
| _sub(tot, "vANTEC_Total", _fmt(vantec)) |
| _sub(tot, "vTotal", _fmt(vst + vdifal + vantec)) |
| _sub(tot, "vPago_Total", _fmt(vpago)) |
|
|
| ET.indent(root, space=" ") |
| return '<?xml version="1.0" encoding="UTF-8"?>\n' + ET.tostring(root, encoding="unicode") |
|
|
| def _bloco_st(self, root: ET.Element) -> None: |
| for op in self._ops_st: |
| el = _sub(root, "ST") |
| _sub(el, "UF_ST", op.uf_destino) |
| _sub(el, "vBC_ST", _fmt(op.base_calculo)) |
| _sub(el, "pICMS_ST", _fmt(op.aliquota)) |
| _sub(el, "vICMS_ST", _fmt(op.valor_imposto)) |
| _sub(el, "vICMS_ST_Pago", _fmt(op.valor_pago)) |
|
|
| def _bloco_difal(self, root: ET.Element) -> None: |
| for op in self._ops_difal: |
| el = _sub(root, "DIFAL") |
| _sub(el, "UF_DIFAL", op.uf_destino) |
| _sub(el, "vBC_DIFAL", _fmt(op.base_calculo)) |
| _sub(el, "pICMS_DIFAL", _fmt(op.aliquota)) |
| _sub(el, "vICMS_DIFAL", _fmt(op.valor_imposto)) |
| _sub(el, "vICMS_DIFAL_Pago", _fmt(op.valor_pago)) |
|
|
| def _bloco_antec(self, root: ET.Element) -> None: |
| for op in self._ops_antec: |
| el = _sub(root, "ANTEC") |
| _sub(el, "UF_ANTEC", op.uf_origem) |
| _sub(el, "vBC_ANTEC", _fmt(op.base_calculo)) |
| _sub(el, "pICMS_ANTEC", _fmt(op.aliquota)) |
| _sub(el, "vICMS_ANTEC", _fmt(op.valor_imposto)) |
| _sub(el, "vICMS_ANTEC_Pago", _fmt(op.valor_pago)) |
|
|
| def salvar(self, diretorio: str | Path = ".") -> Path: |
| caminho = Path(diretorio) / f"DeSTDA_{self.empresa.cnpj}_{self.periodo}.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: |
| vst = self._total(self._ops_st, "valor_imposto") |
| vdifal = self._total(self._ops_difal, "valor_imposto") |
| vantec = self._total(self._ops_antec, "valor_imposto") |
| total = vst + vdifal + vantec |
| venc = self.data_vencimento() |
| linhas = [ |
| f"DeSTDA — {self.empresa.razao_social} — Período: {self.periodo}", |
| "=" * 60, |
| f"ICMS-ST: R$ {vst:>12,.2f} ({len(self._ops_st)} operação/ões)", |
| f"DIFAL: R$ {vdifal:>12,.2f} ({len(self._ops_difal)} operação/ões)", |
| f"Antecipação: R$ {vantec:>12,.2f} ({len(self._ops_antec)} operação/ões)", |
| "=" * 60, |
| f"TOTAL A PAGAR: R$ {total:>12,.2f}", |
| f"Vencimento: {venc.strftime('%d/%m/%Y')}", |
| ] |
| return "\n".join(linhas) |
|
|
| @staticmethod |
| def _ultimo_dia(ano: int, mes: int) -> str: |
| if mes == 12: |
| return "31" |
| from datetime import date as d |
| dias = (d(ano, mes + 1, 1) - d(ano, mes, 1)).days |
| return str(dias).zfill(2) |
|
|