|
|
| from __future__ import annotations |
|
|
| import hashlib |
| import re |
| from dataclasses import dataclass |
| from datetime import date, datetime |
| from decimal import Decimal |
| from pathlib import Path |
| from xml.etree import ElementTree as ET |
|
|
| from src.fiscal.entities import Empresa, ItemNotaFiscal |
|
|
| NS_NFE = "http://www.portalfiscal.inf.br/nfe" |
| VERSAO_NFCE = "4.00" |
|
|
|
|
| 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, casas: int = 2) -> str: |
| return f"{v:.{casas}f}" |
|
|
|
|
| def _limpar(s: str) -> str: |
| return re.sub(r"[^\w\s\-.,/@]", "", str(s))[:60] |
|
|
|
|
| |
| |
| |
|
|
| @dataclass |
| class ConsumidorNFCe: |
| """Destinatário opcional da NFC-e.""" |
| cpf: str = "" |
| nome: str = "CONSUMIDOR" |
| uf: str = "SP" |
|
|
|
|
| |
| |
| |
|
|
| class GeradorNFCeXML: |
| """ |
| Gera o XML de NFC-e conforme o leiaute 4.00 da SEFAZ. |
| |
| O XML gerado deve ser assinado digitalmente com certificado ICP-Brasil |
| antes da transmissão. |
| """ |
|
|
| VERSAO = "4.00" |
| MOD = "65" |
|
|
| _CUF_MAP = { |
| "AC": "12", "AL": "27", "AP": "16", "AM": "13", "BA": "29", |
| "CE": "23", "DF": "53", "ES": "32", "GO": "52", "MA": "21", |
| "MT": "51", "MS": "50", "MG": "31", "PA": "15", "PB": "25", |
| "PR": "41", "PE": "26", "PI": "22", "RJ": "33", "RN": "24", |
| "RS": "43", "RO": "11", "RR": "14", "SC": "42", "SP": "35", |
| "SE": "28", "TO": "17", |
| } |
|
|
| def __init__( |
| self, |
| emitente: Empresa, |
| itens: list[ItemNotaFiscal], |
| consumidor: ConsumidorNFCe | None = None, |
| numero: str = "1", |
| serie: str = "001", |
| forma_pagamento: str = "01", |
| valor_pagamento: Decimal | None = None, |
| ambiente: str = "2", |
| url_qrcode: str = "", |
| ): |
| self.emitente = emitente |
| self.itens = itens |
| self.consumidor = consumidor or ConsumidorNFCe() |
| self.numero = numero |
| self.serie = serie |
| self.forma_pagamento = forma_pagamento |
| self._valor_pagamento = valor_pagamento |
| self.ambiente = ambiente |
| self.url_qrcode = url_qrcode |
| self._data_emissao: date = date.today() |
|
|
| |
| |
| |
|
|
| def _cuf(self, uf: str) -> str: |
| return self._CUF_MAP.get(uf.upper(), "35") |
|
|
| @property |
| def _valor_total(self) -> Decimal: |
| return sum(i.valor_total for i in self.itens) |
|
|
| @property |
| def _valor_produtos(self) -> Decimal: |
| return sum(i.valor_produtos for i in self.itens) |
|
|
| @property |
| def _valor_icms(self) -> Decimal: |
| return sum(i.valor_icms for i in self.itens) |
|
|
| @property |
| def _valor_ipi(self) -> Decimal: |
| return sum(i.valor_ipi for i in self.itens) |
|
|
| @property |
| def valor_pagamento(self) -> Decimal: |
| return self._valor_pagamento if self._valor_pagamento is not None else self._valor_total |
|
|
| def _gerar_chave(self) -> str: |
| """Gera a chave de acesso de 44 dígitos da NFC-e (modelo 65).""" |
| uf = self._cuf(self.emitente.endereco.uf.value) |
| aamm = self._data_emissao.strftime("%y%m") |
| cnpj = self.emitente.cnpj.zfill(14) |
| mod = self.MOD.zfill(2) |
| serie = self.serie.zfill(3) |
| numero = self.numero.zfill(9) |
| tp_emis = "1" |
| c_nf = hashlib.md5(f"{cnpj}{numero}".encode()).hexdigest()[:8].upper() |
| c_nf_digits = "".join(str(int(c, 16) % 10) for c in c_nf)[:8] |
|
|
| chave_sem_dv = f"{uf}{aamm}{cnpj}{mod}{serie}{numero}{tp_emis}{c_nf_digits}" |
|
|
| |
| pesos = [2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, |
| 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4] |
| soma = sum(int(chave_sem_dv[i]) * pesos[i] for i in range(43)) |
| resto = soma % 11 |
| dv = 0 if resto in (0, 1) else 11 - resto |
|
|
| return chave_sem_dv + str(dv) |
|
|
| def _fmt_dhemi(self) -> str: |
| return ( |
| datetime.combine(self._data_emissao, datetime.min.time()) |
| .strftime("%Y-%m-%dT%H:%M:%S") + "-03:00" |
| ) |
|
|
| |
| |
| |
|
|
| def _ide(self, inf_nfe: ET.Element, chave: str) -> None: |
| emp = self.emitente |
| ide = _sub(inf_nfe, "ide") |
| _sub(ide, "cUF", self._cuf(emp.endereco.uf.value)) |
| _sub(ide, "cNF", chave[35:43]) |
| _sub(ide, "natOp", "VENDA A CONSUMIDOR") |
| _sub(ide, "mod", self.MOD) |
| _sub(ide, "serie", self.serie.zfill(3)) |
| _sub(ide, "nNF", self.numero.zfill(9)) |
| _sub(ide, "dhEmi", self._fmt_dhemi()) |
| _sub(ide, "tpNF", "1") |
| _sub(ide, "idDest", "1") |
| _sub(ide, "cMunFG", emp.endereco.cod_municipio or "3550308") |
| _sub(ide, "tpImp", "4") |
| _sub(ide, "tpEmis", "1") |
| _sub(ide, "cDV", chave[-1]) |
| _sub(ide, "tpAmb", self.ambiente) |
| _sub(ide, "finNFe", "1") |
| _sub(ide, "indFinal", "1") |
| _sub(ide, "indPres", "1") |
| _sub(ide, "procEmi", "0") |
| _sub(ide, "verProc", "1.0.0") |
|
|
| def _emit(self, inf_nfe: ET.Element) -> None: |
| emp = self.emitente |
| end = emp.endereco |
| emit = _sub(inf_nfe, "emit") |
| _sub(emit, "CNPJ", emp.cnpj) |
| _sub(emit, "xNome", _limpar(emp.razao_social)[:60]) |
| if emp.nome_fantasia: |
| _sub(emit, "xFant", _limpar(emp.nome_fantasia)[:60]) |
| ender_emit = _sub(emit, "enderEmit") |
| _sub(ender_emit, "xLgr", _limpar(end.logradouro)) |
| _sub(ender_emit, "nro", end.numero) |
| if end.complemento: |
| _sub(ender_emit, "xCpl", _limpar(end.complemento)) |
| _sub(ender_emit, "xBairro", _limpar(end.bairro)) |
| _sub(ender_emit, "cMun", end.cod_municipio or "3550308") |
| _sub(ender_emit, "xMun", _limpar(end.municipio)) |
| _sub(ender_emit, "UF", end.uf.value) |
| _sub(ender_emit, "CEP", re.sub(r"\D", "", end.cep)) |
| _sub(ender_emit, "cPais", "1058") |
| _sub(ender_emit, "xPais", "BRASIL") |
| if emp.contato.telefone: |
| _sub(ender_emit, "fone", re.sub(r"\D", "", emp.contato.telefone)) |
| if emp.ie: |
| _sub(emit, "IE", re.sub(r"\D", "", emp.ie)) |
| _sub(emit, "CRT", emp.regime_tributario.value[:1]) |
|
|
| def _dest(self, inf_nfe: ET.Element) -> None: |
| """Destinatário: omitido se sem CPF; incluído com CPF se identificado.""" |
| cpf = re.sub(r"\D", "", self.consumidor.cpf) |
| if not cpf: |
| return |
| dest = _sub(inf_nfe, "dest") |
| _sub(dest, "CPF", cpf.zfill(11)) |
| _sub(dest, "xNome", _limpar(self.consumidor.nome)[:60]) |
| _sub(dest, "indIEDest", "9") |
|
|
| def _det(self, inf_nfe: ET.Element) -> None: |
| for item in self.itens: |
| det = _sub(inf_nfe, "det") |
| det.set("nItem", str(item.numero_item)) |
|
|
| prod = _sub(det, "prod") |
| _sub(prod, "cProd", item.produto.codigo[:60]) |
| _sub(prod, "cEAN", item.produto.cod_barras or "SEM GTIN") |
| _sub(prod, "xProd", _limpar(item.produto.descricao)[:120]) |
| _sub(prod, "NCM", re.sub(r"\D", "", item.produto.ncm)[:8]) |
| if item.produto.cest: |
| _sub(prod, "CEST", re.sub(r"\D", "", item.produto.cest)) |
| _sub(prod, "CFOP", item.cfop) |
| _sub(prod, "uCom", item.produto.unidade[:6]) |
| _sub(prod, "qCom", _fmt_dec(item.quantidade, 4)) |
| _sub(prod, "vUnCom", _fmt_dec(item.valor_unitario, 10)) |
| _sub(prod, "vProd", _fmt_dec(item.valor_produtos)) |
| _sub(prod, "cEANTrib", item.produto.cod_barras or "SEM GTIN") |
| _sub(prod, "uTrib", item.produto.unidade[:6]) |
| _sub(prod, "qTrib", _fmt_dec(item.quantidade, 4)) |
| _sub(prod, "vUnTrib", _fmt_dec(item.valor_unitario, 10)) |
| if item.desconto > 0: |
| _sub(prod, "vDesc", _fmt_dec(item.desconto)) |
| _sub(prod, "indTot", "1") |
|
|
| |
| imposto = _sub(det, "imposto") |
| self._icms_item(imposto, item) |
| self._pis_item(imposto, item) |
| self._cofins_item(imposto, item) |
|
|
| def _icms_item(self, imposto: ET.Element, item: ItemNotaFiscal) -> None: |
| icms = _sub(imposto, "ICMS") |
| cst = item.produto.cst_icms |
| if cst in {"000"}: |
| grupo = _sub(icms, "ICMS00") |
| _sub(grupo, "orig", "0") |
| _sub(grupo, "CST", cst) |
| _sub(grupo, "modBC", "3") |
| _sub(grupo, "vBC", _fmt_dec(item.base_icms)) |
| _sub(grupo, "pICMS", _fmt_dec(item.produto.aliq_icms, 4)) |
| _sub(grupo, "vICMS", _fmt_dec(item.valor_icms)) |
| elif cst in {"020"}: |
| grupo = _sub(icms, "ICMS20") |
| _sub(grupo, "orig", "0") |
| _sub(grupo, "CST", cst) |
| _sub(grupo, "modBC", "3") |
| _sub(grupo, "pRedBC", _fmt_dec(Decimal("33.33"), 4)) |
| _sub(grupo, "vBC", _fmt_dec(item.base_icms)) |
| _sub(grupo, "pICMS", _fmt_dec(item.produto.aliq_icms, 4)) |
| _sub(grupo, "vICMS", _fmt_dec(item.valor_icms)) |
| elif cst in {"040", "041", "050"}: |
| grupo = _sub(icms, f"ICMS{cst}") |
| _sub(grupo, "orig", "0") |
| _sub(grupo, "CST", cst) |
| elif cst.startswith("1"): |
| grupo = _sub(icms, "ICMSSN102") |
| _sub(grupo, "orig", "0") |
| _sub(grupo, "CSOSN", cst) |
| else: |
| grupo = _sub(icms, "ICMS90") |
| _sub(grupo, "orig", "0") |
| _sub(grupo, "CST", "090") |
| _sub(grupo, "modBC", "3") |
| _sub(grupo, "vBC", _fmt_dec(item.base_icms)) |
| _sub(grupo, "pICMS", _fmt_dec(item.produto.aliq_icms, 4)) |
| _sub(grupo, "vICMS", _fmt_dec(item.valor_icms)) |
|
|
| def _pis_item(self, imposto: ET.Element, item: ItemNotaFiscal) -> None: |
| pis = _sub(imposto, "PIS") |
| cst = item.produto.cst_pis |
| if cst in {"07", "08", "09"}: |
| pisnt = _sub(pis, "PISNT") |
| _sub(pisnt, "CST", cst) |
| else: |
| pisaliq = _sub(pis, "PISAliq") |
| _sub(pisaliq, "CST", cst) |
| _sub(pisaliq, "vBC", _fmt_dec(item.valor_produtos)) |
| _sub(pisaliq, "pPIS", _fmt_dec(Decimal("0.65"), 4)) |
| _sub(pisaliq, "vPIS", _fmt_dec(Decimal("0"))) |
|
|
| def _cofins_item(self, imposto: ET.Element, item: ItemNotaFiscal) -> None: |
| cofins = _sub(imposto, "COFINS") |
| cst = item.produto.cst_cofins |
| if cst in {"07", "08", "09"}: |
| cofinsnt = _sub(cofins, "COFINSNT") |
| _sub(cofinsnt, "CST", cst) |
| else: |
| cofinsaliq = _sub(cofins, "COFINSAliq") |
| _sub(cofinsaliq, "CST", cst) |
| _sub(cofinsaliq, "vBC", _fmt_dec(item.valor_produtos)) |
| _sub(cofinsaliq, "pCOFINS", _fmt_dec(Decimal("3.00"), 4)) |
| _sub(cofinsaliq, "vCOFINS", _fmt_dec(Decimal("0"))) |
|
|
| def _total(self, inf_nfe: ET.Element) -> None: |
| total = _sub(inf_nfe, "total") |
| ict = _sub(total, "ICMSTot") |
| _sub(ict, "vBC", _fmt_dec(sum(i.base_icms for i in self.itens))) |
| _sub(ict, "vICMS", _fmt_dec(self._valor_icms)) |
| _sub(ict, "vICMSDeson", "0.00") |
| _sub(ict, "vFCPUFDest", "0.00") |
| _sub(ict, "vICMSUFDest", "0.00") |
| _sub(ict, "vICMSUFRemet", "0.00") |
| _sub(ict, "vFCP", "0.00") |
| _sub(ict, "vBCST", "0.00") |
| _sub(ict, "vST", "0.00") |
| _sub(ict, "vFCPST", "0.00") |
| _sub(ict, "vFCPSTRet", "0.00") |
| _sub(ict, "vProd", _fmt_dec(self._valor_produtos)) |
| _sub(ict, "vFrete", "0.00") |
| _sub(ict, "vSeg", "0.00") |
| _sub(ict, "vDesc", _fmt_dec(sum(i.desconto for i in self.itens))) |
| _sub(ict, "vII", "0.00") |
| _sub(ict, "vIPI", _fmt_dec(self._valor_ipi)) |
| _sub(ict, "vIPIDevol", "0.00") |
| _sub(ict, "vPIS", "0.00") |
| _sub(ict, "vCOFINS", "0.00") |
| _sub(ict, "vOutro", "0.00") |
| _sub(ict, "vNF", _fmt_dec(self._valor_total)) |
| _sub(ict, "vTotTrib", "0.00") |
|
|
| def _transp(self, inf_nfe: ET.Element) -> None: |
| transp = _sub(inf_nfe, "transp") |
| _sub(transp, "modFrete", "9") |
|
|
| def _pag(self, inf_nfe: ET.Element) -> None: |
| pag = _sub(inf_nfe, "pag") |
| det_pag = _sub(pag, "detPag") |
| _sub(det_pag, "tPag", self.forma_pagamento) |
| _sub(det_pag, "vPag", _fmt_dec(self.valor_pagamento)) |
|
|
| def _inf_nfe_supl(self, nfe: ET.Element, chave: str) -> None: |
| """ |
| Bloco suplementar obrigatório na NFC-e. |
| Contém QR Code e URL de consulta. |
| """ |
| |
| digest_hex = hashlib.sha256(chave.encode()).hexdigest()[:32] |
| signature_hex = hashlib.sha256((chave + "sig").encode()).hexdigest()[:32] |
|
|
| base_url = self.url_qrcode or "https://www.sefaz.rs.gov.br/NFCE/NFCE-com.aspx" |
| qr_url = f"{base_url}?p={chave}|2|1|{digest_hex}|{signature_hex}" |
| url_consulta = "https://www.sefaz.rs.gov.br/NFCE/NFCE-com.aspx" |
|
|
| supl = _sub(nfe, "infNFeSupl") |
| _sub(supl, "qrCode", qr_url) |
| _sub(supl, "urlChave", url_consulta) |
|
|
| |
| |
| |
|
|
| def gerar_xml(self) -> str: |
| """Retorna o XML da NFC-e não assinado como string.""" |
| chave = self._gerar_chave() |
|
|
| nfe_proc = ET.Element("nfeProc") |
| nfe_proc.set("xmlns", NS_NFE) |
| nfe_proc.set("versao", self.VERSAO) |
|
|
| nfe = _sub(nfe_proc, "NFe") |
| nfe.set("xmlns", NS_NFE) |
|
|
| inf_nfe = _sub(nfe, "infNFe") |
| inf_nfe.set("versao", self.VERSAO) |
| inf_nfe.set("Id", f"NFe{chave}") |
|
|
| self._ide(inf_nfe, chave) |
| self._emit(inf_nfe) |
| self._dest(inf_nfe) |
| self._det(inf_nfe) |
| self._total(inf_nfe) |
| self._transp(inf_nfe) |
| self._pag(inf_nfe) |
|
|
| |
| _sub(nfe, "Signature") |
|
|
| |
| self._inf_nfe_supl(nfe, chave) |
|
|
| ET.indent(nfe_proc, space=" ") |
| return '<?xml version="1.0" encoding="UTF-8"?>\n' + ET.tostring(nfe_proc, encoding="unicode") |
|
|
| def salvar(self, diretorio: str | Path = ".") -> Path: |
| """Salva o XML em diretório informado e retorna o caminho do arquivo.""" |
| chave = self._gerar_chave() |
| caminho = Path(diretorio) / f"NFCe{chave}.xml" |
| caminho.parent.mkdir(parents=True, exist_ok=True) |
| caminho.write_text(self.gerar_xml(), encoding="utf-8") |
| return caminho |
|
|