From 9133d2481e3904f6c7138b5b1e8e8678ea1428ba Mon Sep 17 00:00:00 2001 From: Felipe Correa Date: Thu, 23 Apr 2026 13:54:15 -0300 Subject: [PATCH 1/4] feat(reforma): suporte a gIBSCBSMono para CST 620 monofasica [DEV-1929] (#2) * feat(reforma): suporte a gIBSCBSMono para CST 620 monofasica [DEV-1929] Adiciona suporte a tributacao monofasica IBS/CBS conforme NT 2025.002-RTC. Produtos com CST 620 (combustiveis e demais itens monofasicos) agora emitem com qBCMono, adRemIBS, vIBSMono, adRemCBS e vCBSMono no lugar do grupo padrao (pIBSUF/pIBSMun/pCBS). Contexto: SEFAZ rejeita (cStat 1026 - "Aliquota do IBS da UF invalida") qualquer NF-e com CST 620 emitida com gIBSCBS padrao, pois a spec exige o grupo gIBSCBSMono para regime monofasico. Mudancas: - NotaFiscalProduto ganha 5 novos atributos (ibscbs_q_bc_mono, ibscbs_ad_rem_ibs, ibscbs_v_ibs_mono, ibscbs_ad_rem_cbs, ibscbs_v_cbs_mono). - _serializar_ibscbs roteia para _serializar_gibscbs_mono quando CST in _IBSCBS_CST_MONOFASICO (por ora so "620"; 630/640 virao no futuro). - CST 620 removido de _IBSCBS_CST_TRIBUTADOS (agora pertence ao conjunto monofasico) para evitar emissao duplicada. - Docs atualizadas: gIBSCBSMono removido da lista "Nao inclui". - 3 novos testes (CST 620 com valores zero, CST 620 com ad rem, regressao CST 000). * style(serializacao): aplica ruff format em gIBSCBSMono [DEV-1929] --------- Co-authored-by: felps-dev --- docs/reforma_tributaria.md | 19 ++- pynfe/entidades/notafiscal.py | 11 ++ pynfe/processamento/serializacao.py | 103 ++++++++---- ...est_nfe_serializacao_reforma_tributaria.py | 156 ++++++++++++++++++ 4 files changed, 258 insertions(+), 31 deletions(-) diff --git a/docs/reforma_tributaria.md b/docs/reforma_tributaria.md index cae67eb2..cb11c879 100644 --- a/docs/reforma_tributaria.md +++ b/docs/reforma_tributaria.md @@ -37,8 +37,25 @@ A implementacao cobre: - `vNF` **NAO inclui** IBS/CBS (proibido em 2025-2026) - `finNFe=5` (Nota de Debito) e `finNFe=6` (Nota de Credito) - Campos de entidade para IS (Imposto Seletivo) — **armazenados mas nao serializados** ate o schema suportar (2027) +- Tributacao monofasica (`gIBSCBSMono`) para CST 620 — combustiveis e demais produtos sujeitos ao regime monofasico de IBS/CBS -**Nao inclui** (ainda): Split Payment, cashback, eventos de apuracao assistida, Grupo VB (total do item), Grupo VC (referenciamento de DF-e), Grupo BB (antecipacao de pagamento), tributacao monofasica (`gIBSCBSMono`), diferimento per-item (`gDif`), devolucao de tributos per-item (`gDevTrib`), reducao de aliquota per-item (`gRed`), estorno de credito (`gEstornoCred`), credito presumido per-item (`gCredPresOper`, `gCredPresIBSZFM`). +**Nao inclui** (ainda): Split Payment, cashback, eventos de apuracao assistida, Grupo VB (total do item), Grupo VC (referenciamento de DF-e), Grupo BB (antecipacao de pagamento), diferimento per-item (`gDif`), devolucao de tributos per-item (`gDevTrib`), reducao de aliquota per-item (`gRed`), estorno de credito (`gEstornoCred`), credito presumido per-item (`gCredPresOper`, `gCredPresIBSZFM`). + +### Tributacao monofasica — `gIBSCBSMono` + +Para produtos com CST 620 (combustiveis, etc.) o grupo emitido dentro de `` e `` ao inves de ``. Campos obrigatorios: + +| Campo | Tipo | Descricao | +|-------|------|-----------| +| `qBCMono` | TDec_1104v | Quantidade tributada na base monofasica | +| `adRemIBS` | TDec_0302a10 | Aliquota ad rem IBS (valor em BRL por unidade) | +| `vIBSMono` | TDec_1302 | Valor IBS monofasico | +| `adRemCBS` | TDec_0302a10 | Aliquota ad rem CBS (valor em BRL por unidade) | +| `vCBSMono` | TDec_1302 | Valor CBS monofasico | + +Atributos na entidade `NotaFiscalProduto`: `ibscbs_q_bc_mono`, `ibscbs_ad_rem_ibs`, `ibscbs_v_ibs_mono`, `ibscbs_ad_rem_cbs`, `ibscbs_v_cbs_mono`. + +Durante o Teste de Carga 2026 os ad rem ainda nao foram publicados pela SEFAZ, entao os valores podem ser zerados — o grupo `gIBSCBSMono` ainda sera emitido corretamente. ## CSTs disponiveis diff --git a/pynfe/entidades/notafiscal.py b/pynfe/entidades/notafiscal.py index 7047138e..f65114ef 100644 --- a/pynfe/entidades/notafiscal.py +++ b/pynfe/entidades/notafiscal.py @@ -1046,6 +1046,17 @@ class NotaFiscalProduto(Entidade): ibscbs_p_cbs = Decimal() # pCBS ibscbs_v_cbs = Decimal() # vCBS + # gIBSCBSMono - Tributacao monofasica (CST 620) + # Emitted as instead of for CST 620 items. + # qBCMono = quantity in monophasic base unit (TDec_1104v) + # adRemIBS / adRemCBS = ad rem rate in BRL per unit (TDec_0302a10) + # vIBSMono / vCBSMono = final value in BRL + ibscbs_q_bc_mono = Decimal() # qBCMono + ibscbs_ad_rem_ibs = Decimal() # adRemIBS + ibscbs_v_ibs_mono = Decimal() # vIBSMono + ibscbs_ad_rem_cbs = Decimal() # adRemCBS + ibscbs_v_cbs_mono = Decimal() # vCBSMono + # IS (Imposto Seletivo) - Group UB-IS is_cst_selec = str() # CSTSelec (2-digit) is_c_class_trib = str() # cClassTribIS 6-digit diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index cb463000..2ca28645 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -1316,8 +1316,13 @@ def _serializar_imposto_importacao( # Reforma Tributaria - IVA Dual (NT 2025.002-RTC) # ============================================= - # CSTs that have taxable values (vBC, rates, amounts) - _IBSCBS_CST_TRIBUTADOS = ("000", "010", "200", "400", "510", "600", "620", "800", "810", "900") + # CSTs that have taxable values (vBC, rates, amounts) and use + _IBSCBS_CST_TRIBUTADOS = ("000", "010", "200", "400", "510", "600", "800", "810", "900") + + # CSTs that use the monophasic tax regime and emit instead of + # (qBCMono, adRemIBS/adRemCBS, vIBSMono/vCBSMono). Start with 620 (combustiveis); + # 630/640 will be added when SEFAZ publishes the corresponding cClassTrib codes. + _IBSCBS_CST_MONOFASICO = ("620",) def _serializar_imposto_ibscbs( self, produto_servico, modelo, tag_raiz="imposto", retorna_string=True @@ -1340,45 +1345,83 @@ def _serializar_imposto_ibscbs( # self._serializar_is(produto_servico, tag_raiz) def _serializar_ibscbs(self, produto_servico, tag_raiz): - """Serializa com gIBSCBS contendo gIBSUF, gIBSMun e gCBS.""" + """Serializa . + + Para CSTs monofasicas (ex: 620) emite com qBCMono, adRemIBS, + vIBSMono, adRemCBS, vCBSMono. Para demais CSTs tributados emite + com vBC + gIBSUF + gIBSMun + gCBS. + """ ibscbs = etree.SubElement(tag_raiz, "IBSCBS") etree.SubElement(ibscbs, "CST").text = produto_servico.ibscbs_cst if produto_servico.ibscbs_c_class_trib: etree.SubElement(ibscbs, "cClassTrib").text = produto_servico.ibscbs_c_class_trib - if produto_servico.ibscbs_cst in self._IBSCBS_CST_TRIBUTADOS: - gibscbs = etree.SubElement(ibscbs, "gIBSCBS") + if produto_servico.ibscbs_cst in self._IBSCBS_CST_MONOFASICO: + self._serializar_gibscbs_mono(produto_servico, ibscbs) + elif produto_servico.ibscbs_cst in self._IBSCBS_CST_TRIBUTADOS: + self._serializar_gibscbs(produto_servico, ibscbs) - etree.SubElement(gibscbs, "vBC").text = "{:.2f}".format(produto_servico.ibscbs_vbc or 0) + def _serializar_gibscbs(self, produto_servico, ibscbs): + """Serializa padrao com vBC, gIBSUF, gIBSMun, vIBS e gCBS.""" + gibscbs = etree.SubElement(ibscbs, "gIBSCBS") - # gIBSUF - gibsuf = etree.SubElement(gibscbs, "gIBSUF") - etree.SubElement(gibsuf, "pIBSUF").text = "{:.4f}".format( - produto_servico.ibscbs_p_ibs_uf or 0 - ) - etree.SubElement(gibsuf, "vIBSUF").text = "{:.2f}".format( - produto_servico.ibscbs_v_ibs_uf or 0 - ) + etree.SubElement(gibscbs, "vBC").text = "{:.2f}".format(produto_servico.ibscbs_vbc or 0) - # gIBSMun - gibsmun = etree.SubElement(gibscbs, "gIBSMun") - etree.SubElement(gibsmun, "pIBSMun").text = "{:.4f}".format( - produto_servico.ibscbs_p_ibs_mun or 0 - ) - etree.SubElement(gibsmun, "vIBSMun").text = "{:.2f}".format( - produto_servico.ibscbs_v_ibs_mun or 0 - ) + # gIBSUF + gibsuf = etree.SubElement(gibscbs, "gIBSUF") + etree.SubElement(gibsuf, "pIBSUF").text = "{:.4f}".format( + produto_servico.ibscbs_p_ibs_uf or 0 + ) + etree.SubElement(gibsuf, "vIBSUF").text = "{:.2f}".format( + produto_servico.ibscbs_v_ibs_uf or 0 + ) - # vIBS total - etree.SubElement(gibscbs, "vIBS").text = "{:.2f}".format( - produto_servico.ibscbs_v_ibs or 0 - ) + # gIBSMun + gibsmun = etree.SubElement(gibscbs, "gIBSMun") + etree.SubElement(gibsmun, "pIBSMun").text = "{:.4f}".format( + produto_servico.ibscbs_p_ibs_mun or 0 + ) + etree.SubElement(gibsmun, "vIBSMun").text = "{:.2f}".format( + produto_servico.ibscbs_v_ibs_mun or 0 + ) - # gCBS - gcbs = etree.SubElement(gibscbs, "gCBS") - etree.SubElement(gcbs, "pCBS").text = "{:.4f}".format(produto_servico.ibscbs_p_cbs or 0) - etree.SubElement(gcbs, "vCBS").text = "{:.2f}".format(produto_servico.ibscbs_v_cbs or 0) + # vIBS total + etree.SubElement(gibscbs, "vIBS").text = "{:.2f}".format(produto_servico.ibscbs_v_ibs or 0) + + # gCBS + gcbs = etree.SubElement(gibscbs, "gCBS") + etree.SubElement(gcbs, "pCBS").text = "{:.4f}".format(produto_servico.ibscbs_p_cbs or 0) + etree.SubElement(gcbs, "vCBS").text = "{:.2f}".format(produto_servico.ibscbs_v_cbs or 0) + + def _serializar_gibscbs_mono(self, produto_servico, ibscbs): + """Serializa para CSTs monofasicas (620). + + Estrutura obrigatoria por NT 2025.002-RTC: + + TDec_1104v (4 casas) + TDec_0302a10 (4 casas) + TDec_1302 (2 casas) + TDec_0302a10 (4 casas) + TDec_1302 (2 casas) + + """ + gibscbs_mono = etree.SubElement(ibscbs, "gIBSCBSMono") + etree.SubElement(gibscbs_mono, "qBCMono").text = "{:.4f}".format( + produto_servico.ibscbs_q_bc_mono or 0 + ) + etree.SubElement(gibscbs_mono, "adRemIBS").text = "{:.4f}".format( + produto_servico.ibscbs_ad_rem_ibs or 0 + ) + etree.SubElement(gibscbs_mono, "vIBSMono").text = "{:.2f}".format( + produto_servico.ibscbs_v_ibs_mono or 0 + ) + etree.SubElement(gibscbs_mono, "adRemCBS").text = "{:.4f}".format( + produto_servico.ibscbs_ad_rem_cbs or 0 + ) + etree.SubElement(gibscbs_mono, "vCBSMono").text = "{:.2f}".format( + produto_servico.ibscbs_v_cbs_mono or 0 + ) def _serializar_is(self, produto_servico, tag_raiz): """Serializa (Imposto Seletivo) como filho direto de . diff --git a/tests/test_nfe_serializacao_reforma_tributaria.py b/tests/test_nfe_serializacao_reforma_tributaria.py index ee042b8a..a0f3b305 100644 --- a/tests/test_nfe_serializacao_reforma_tributaria.py +++ b/tests/test_nfe_serializacao_reforma_tributaria.py @@ -667,6 +667,162 @@ def test_cmunfgibs_emitido_no_ide(self): cmunfgibs_idx = tags.index("cMunFGIBS") self.assertGreater(cmunfgibs_idx, cmunfg_idx) + # ------------------------------------------------------------------ + # Test gIBSCBSMono: CST 620 emits monophasic group + # ------------------------------------------------------------------ + def test_cst620_monofasica_emite_gibscbsmono(self): + """CST 620 (tributacao monofasica) must emit with + qBCMono, adRemIBS, vIBSMono, adRemCBS, vCBSMono — NOT .""" + emitente = self._emitente() + cliente = self._cliente() + nf = self._nota_fiscal(emitente, cliente) + + kwargs = self._base_product_kwargs() + kwargs.update( + codigo="010", + descricao="GLP em Botijao 13KG (CST 620 monofasica)", + ncm="27111910", + quantidade_comercial=Decimal("18"), + valor_unitario_comercial=Decimal("74.04"), + valor_total_bruto=Decimal("1332.72"), + quantidade_tributavel=Decimal("18"), + valor_unitario_tributavel=Decimal("74.04"), + ibscbs_cst="620", + ibscbs_c_class_trib="620006", + # Monophasic fields + ibscbs_q_bc_mono=Decimal("18.0000"), + ibscbs_ad_rem_ibs=Decimal("0.0000"), + ibscbs_v_ibs_mono=Decimal("0.00"), + ibscbs_ad_rem_cbs=Decimal("0.0000"), + ibscbs_v_cbs_mono=Decimal("0.00"), + ) + nf.adicionar_produto_servico(**kwargs) + nf.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=1332.72, ind_pag=0) + + xml = self._serializar_e_assinar() + + # is emitted + ibscbs = xml.xpath("//ns:det/ns:imposto/ns:IBSCBS", namespaces=self.ns) + self.assertEqual(len(ibscbs), 1) + + # CST is 620 + cst = xml.xpath("//ns:IBSCBS/ns:CST", namespaces=self.ns)[0].text + self.assertEqual(cst, "620") + + # cClassTrib is 620006 + cclass = xml.xpath("//ns:IBSCBS/ns:cClassTrib", namespaces=self.ns)[0].text + self.assertEqual(cclass, "620006") + + # is emitted + gibscbs_mono = xml.xpath("//ns:IBSCBS/ns:gIBSCBSMono", namespaces=self.ns) + self.assertEqual(len(gibscbs_mono), 1) + + # is NOT emitted (we use monophasic instead) + gibscbs = xml.xpath("//ns:IBSCBS/ns:gIBSCBS", namespaces=self.ns) + self.assertEqual(len(gibscbs), 0) + + # Verify the 5 required monophasic fields in correct order + q_bc_mono = xml.xpath("//ns:gIBSCBSMono/ns:qBCMono", namespaces=self.ns)[0].text + self.assertEqual(q_bc_mono, "18.0000") + + ad_rem_ibs = xml.xpath("//ns:gIBSCBSMono/ns:adRemIBS", namespaces=self.ns)[0].text + self.assertEqual(ad_rem_ibs, "0.0000") + + v_ibs_mono = xml.xpath("//ns:gIBSCBSMono/ns:vIBSMono", namespaces=self.ns)[0].text + self.assertEqual(v_ibs_mono, "0.00") + + ad_rem_cbs = xml.xpath("//ns:gIBSCBSMono/ns:adRemCBS", namespaces=self.ns)[0].text + self.assertEqual(ad_rem_cbs, "0.0000") + + v_cbs_mono = xml.xpath("//ns:gIBSCBSMono/ns:vCBSMono", namespaces=self.ns)[0].text + self.assertEqual(v_cbs_mono, "0.00") + + # Field order: qBCMono, adRemIBS, vIBSMono, adRemCBS, vCBSMono + mono_elem = gibscbs_mono[0] + field_names = [child.tag.split("}")[-1] for child in mono_elem] + self.assertEqual( + field_names, + ["qBCMono", "adRemIBS", "vIBSMono", "adRemCBS", "vCBSMono"], + ) + + def test_cst620_monofasica_com_valores_calculados(self): + """CST 620 with non-zero ad rem rates produces non-zero monophasic values.""" + emitente = self._emitente() + cliente = self._cliente() + nf = self._nota_fiscal(emitente, cliente) + + kwargs = self._base_product_kwargs() + kwargs.update( + codigo="011", + descricao="Combustivel monofasico com ad rem", + ibscbs_cst="620", + ibscbs_c_class_trib="620001", + ibscbs_q_bc_mono=Decimal("100.0000"), + ibscbs_ad_rem_ibs=Decimal("0.1500"), + ibscbs_v_ibs_mono=Decimal("15.00"), + ibscbs_ad_rem_cbs=Decimal("0.8500"), + ibscbs_v_cbs_mono=Decimal("85.00"), + ) + nf.adicionar_produto_servico(**kwargs) + nf.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=1000.00, ind_pag=0) + + xml = self._serializar_e_assinar() + + self.assertEqual( + xml.xpath("//ns:gIBSCBSMono/ns:qBCMono", namespaces=self.ns)[0].text, "100.0000" + ) + self.assertEqual( + xml.xpath("//ns:gIBSCBSMono/ns:adRemIBS", namespaces=self.ns)[0].text, "0.1500" + ) + self.assertEqual( + xml.xpath("//ns:gIBSCBSMono/ns:vIBSMono", namespaces=self.ns)[0].text, "15.00" + ) + self.assertEqual( + xml.xpath("//ns:gIBSCBSMono/ns:adRemCBS", namespaces=self.ns)[0].text, "0.8500" + ) + self.assertEqual( + xml.xpath("//ns:gIBSCBSMono/ns:vCBSMono", namespaces=self.ns)[0].text, "85.00" + ) + + def test_cst000_nao_emite_gibscbsmono_regressao(self): + """Regression test: CST 000 (regular taxation) must still emit + and must NOT emit .""" + emitente = self._emitente() + cliente = self._cliente() + nf = self._nota_fiscal(emitente, cliente) + + kwargs = self._base_product_kwargs() + kwargs.update( + ibscbs_cst="000", + ibscbs_c_class_trib="000001", + ibscbs_vbc=Decimal("1000.00"), + ibscbs_p_ibs_uf=Decimal("0.1000"), + ibscbs_v_ibs_uf=Decimal("1.00"), + ibscbs_p_ibs_mun=Decimal("0.0000"), + ibscbs_v_ibs_mun=Decimal("0.00"), + ibscbs_v_ibs=Decimal("1.00"), + ibscbs_p_cbs=Decimal("0.9000"), + ibscbs_v_cbs=Decimal("9.00"), + ) + nf.adicionar_produto_servico(**kwargs) + nf.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=1000.00, ind_pag=0) + + xml = self._serializar_e_assinar() + + # is emitted (regular taxation path unchanged) + gibscbs = xml.xpath("//ns:IBSCBS/ns:gIBSCBS", namespaces=self.ns) + self.assertEqual(len(gibscbs), 1) + + # must NOT be emitted + gibscbs_mono = xml.xpath("//ns:IBSCBS/ns:gIBSCBSMono", namespaces=self.ns) + self.assertEqual(len(gibscbs_mono), 0) + + # Verify still has pIBSUF/pIBSMun/pCBS (regression) + p_ibs_uf = xml.xpath("//ns:gIBSCBS/ns:gIBSUF/ns:pIBSUF", namespaces=self.ns)[0].text + self.assertEqual(p_ibs_uf, "0.1000") + p_cbs = xml.xpath("//ns:gIBSCBS/ns:gCBS/ns:pCBS", namespaces=self.ns)[0].text + self.assertEqual(p_cbs, "0.9000") + # ------------------------------------------------------------------ # Test 10: cMunFGIBS NOT emitted when not set # ------------------------------------------------------------------ From 135ec04f22aaf625a5ce4c24f4908c5287502d85 Mon Sep 17 00:00:00 2001 From: Felipe Correa Date: Fri, 24 Apr 2026 21:24:20 -0300 Subject: [PATCH 2/4] fix(reforma): envolve gIBSCBSMono em gMonoPadrao conforme NT 2025.002-RTC [DEV-1953] (#3) Ao emitir NF-e com CST 620 (Tributacao monofasica), o grupo estava sendo serializado com os cinco campos direto (qBCMono, adRemIBS, vIBSMono, adRemCBS, vCBSMono), sem o wrapper exigido pelo schema oficial (DFeTiposBasicos_v1.00.xsd, type TMonofasia). SEFAZ rejeitava toda emissao com cStat 225: Falha no Schema XML da NFe Elemento: enviNFe/NFe[1]/infNFe/det[1]/imposto/IBSCBS/gIBSCBSMono/qBCMono Alem do wrapper ausente, a ordem dos campos tambem violava o schema (adRemCBS deve vir antes de vIBSMono, nao depois). Estrutura corrigida: TDec1104RTC TDec_0302_04RTC TDec_0302_04RTC TDec1302RTC TDec1302RTC Tests atualizados para validar wrapper + ordem correta conforme o schema TMonofasia/gMonoPadrao (xsd linhas 687-727). Co-authored-by: Claude Opus 4.7 (1M context) --- docs/reforma_tributaria.md | 24 ++++++++--- pynfe/processamento/serializacao.py | 43 +++++++++++++------ ...est_nfe_serializacao_reforma_tributaria.py | 36 +++++++++------- 3 files changed, 69 insertions(+), 34 deletions(-) diff --git a/docs/reforma_tributaria.md b/docs/reforma_tributaria.md index cb11c879..14c06627 100644 --- a/docs/reforma_tributaria.md +++ b/docs/reforma_tributaria.md @@ -43,15 +43,27 @@ A implementacao cobre: ### Tributacao monofasica — `gIBSCBSMono` -Para produtos com CST 620 (combustiveis, etc.) o grupo emitido dentro de `` e `` ao inves de ``. Campos obrigatorios: +Para produtos com CST 620 (combustiveis, etc.) o grupo emitido dentro de `` e `` ao inves de ``. Conforme o schema oficial (`DFeTiposBasicos_v1.00.xsd`, type `TMonofasia`), os cinco campos monofasicos vivem sob o wrapper obrigatorio `` e na ordem definida pelo schema: + +```xml + + + 18.0000 + 0.1000 + 0.0000 + 1.80 + 0.00 + + +``` | Campo | Tipo | Descricao | |-------|------|-----------| -| `qBCMono` | TDec_1104v | Quantidade tributada na base monofasica | -| `adRemIBS` | TDec_0302a10 | Aliquota ad rem IBS (valor em BRL por unidade) | -| `vIBSMono` | TDec_1302 | Valor IBS monofasico | -| `adRemCBS` | TDec_0302a10 | Aliquota ad rem CBS (valor em BRL por unidade) | -| `vCBSMono` | TDec_1302 | Valor CBS monofasico | +| `qBCMono` | TDec1104RTC | Quantidade tributada na base monofasica | +| `adRemIBS` | TDec_0302_04RTC | Aliquota ad rem IBS (valor em BRL por unidade) | +| `adRemCBS` | TDec_0302_04RTC | Aliquota ad rem CBS (valor em BRL por unidade) | +| `vIBSMono` | TDec1302RTC | Valor IBS monofasico | +| `vCBSMono` | TDec1302RTC | Valor CBS monofasico | Atributos na entidade `NotaFiscalProduto`: `ibscbs_q_bc_mono`, `ibscbs_ad_rem_ibs`, `ibscbs_v_ibs_mono`, `ibscbs_ad_rem_cbs`, `ibscbs_v_cbs_mono`. diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index 2ca28645..a8f60804 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -1397,29 +1397,46 @@ def _serializar_gibscbs(self, produto_servico, ibscbs): def _serializar_gibscbs_mono(self, produto_servico, ibscbs): """Serializa para CSTs monofasicas (620). - Estrutura obrigatoria por NT 2025.002-RTC: + Estrutura obrigatoria por NT 2025.002-RTC (type TMonofasia do + DFeTiposBasicos_v1.00.xsd, sequencia gMonoPadrao -> gMonoReten -> + gMonoRet -> gMonoDif). Para CST 620 "Tributacao monofasica padrao" + emitimos apenas com os cinco campos na ordem do + schema: + - TDec_1104v (4 casas) - TDec_0302a10 (4 casas) - TDec_1302 (2 casas) - TDec_0302a10 (4 casas) - TDec_1302 (2 casas) + + TDec1104RTC (4 casas) + TDec_0302_04RTC (4 casas) + TDec_0302_04RTC (4 casas) + TDec1302RTC (2 casas) + TDec1302RTC (2 casas) + + + IMPORTANT: + - O wrapper e obrigatorio. Antes desta correcao o + grupo era emitido flat (sem o wrapper) e SEFAZ rejeitava com + cStat 225 "Falha no Schema XML da NFe (Elemento: + enviNFe/NFe[1]/infNFe/det[1]/imposto/IBSCBS/gIBSCBSMono/qBCMono)". + - A ordem antes de segue o schema oficial + (linhas 701-725 de DFeTiposBasicos_v1.00.xsd). A ordem anterior + ( antes de ) tambem violava o schema. """ gibscbs_mono = etree.SubElement(ibscbs, "gIBSCBSMono") - etree.SubElement(gibscbs_mono, "qBCMono").text = "{:.4f}".format( + gmono_padrao = etree.SubElement(gibscbs_mono, "gMonoPadrao") + etree.SubElement(gmono_padrao, "qBCMono").text = "{:.4f}".format( produto_servico.ibscbs_q_bc_mono or 0 ) - etree.SubElement(gibscbs_mono, "adRemIBS").text = "{:.4f}".format( + etree.SubElement(gmono_padrao, "adRemIBS").text = "{:.4f}".format( produto_servico.ibscbs_ad_rem_ibs or 0 ) - etree.SubElement(gibscbs_mono, "vIBSMono").text = "{:.2f}".format( - produto_servico.ibscbs_v_ibs_mono or 0 - ) - etree.SubElement(gibscbs_mono, "adRemCBS").text = "{:.4f}".format( + etree.SubElement(gmono_padrao, "adRemCBS").text = "{:.4f}".format( produto_servico.ibscbs_ad_rem_cbs or 0 ) - etree.SubElement(gibscbs_mono, "vCBSMono").text = "{:.2f}".format( + etree.SubElement(gmono_padrao, "vIBSMono").text = "{:.2f}".format( + produto_servico.ibscbs_v_ibs_mono or 0 + ) + etree.SubElement(gmono_padrao, "vCBSMono").text = "{:.2f}".format( produto_servico.ibscbs_v_cbs_mono or 0 ) diff --git a/tests/test_nfe_serializacao_reforma_tributaria.py b/tests/test_nfe_serializacao_reforma_tributaria.py index a0f3b305..df076230 100644 --- a/tests/test_nfe_serializacao_reforma_tributaria.py +++ b/tests/test_nfe_serializacao_reforma_tributaria.py @@ -713,36 +713,41 @@ def test_cst620_monofasica_emite_gibscbsmono(self): cclass = xml.xpath("//ns:IBSCBS/ns:cClassTrib", namespaces=self.ns)[0].text self.assertEqual(cclass, "620006") - # is emitted + # is emitted with wrapper (NT 2025.002-RTC) gibscbs_mono = xml.xpath("//ns:IBSCBS/ns:gIBSCBSMono", namespaces=self.ns) self.assertEqual(len(gibscbs_mono), 1) + gmono_padrao = xml.xpath("//ns:IBSCBS/ns:gIBSCBSMono/ns:gMonoPadrao", namespaces=self.ns) + self.assertEqual(len(gmono_padrao), 1) + # is NOT emitted (we use monophasic instead) gibscbs = xml.xpath("//ns:IBSCBS/ns:gIBSCBS", namespaces=self.ns) self.assertEqual(len(gibscbs), 0) # Verify the 5 required monophasic fields in correct order - q_bc_mono = xml.xpath("//ns:gIBSCBSMono/ns:qBCMono", namespaces=self.ns)[0].text + q_bc_mono = xml.xpath("//ns:gMonoPadrao/ns:qBCMono", namespaces=self.ns)[0].text self.assertEqual(q_bc_mono, "18.0000") - ad_rem_ibs = xml.xpath("//ns:gIBSCBSMono/ns:adRemIBS", namespaces=self.ns)[0].text + ad_rem_ibs = xml.xpath("//ns:gMonoPadrao/ns:adRemIBS", namespaces=self.ns)[0].text self.assertEqual(ad_rem_ibs, "0.0000") - v_ibs_mono = xml.xpath("//ns:gIBSCBSMono/ns:vIBSMono", namespaces=self.ns)[0].text + v_ibs_mono = xml.xpath("//ns:gMonoPadrao/ns:vIBSMono", namespaces=self.ns)[0].text self.assertEqual(v_ibs_mono, "0.00") - ad_rem_cbs = xml.xpath("//ns:gIBSCBSMono/ns:adRemCBS", namespaces=self.ns)[0].text + ad_rem_cbs = xml.xpath("//ns:gMonoPadrao/ns:adRemCBS", namespaces=self.ns)[0].text self.assertEqual(ad_rem_cbs, "0.0000") - v_cbs_mono = xml.xpath("//ns:gIBSCBSMono/ns:vCBSMono", namespaces=self.ns)[0].text + v_cbs_mono = xml.xpath("//ns:gMonoPadrao/ns:vCBSMono", namespaces=self.ns)[0].text self.assertEqual(v_cbs_mono, "0.00") - # Field order: qBCMono, adRemIBS, vIBSMono, adRemCBS, vCBSMono - mono_elem = gibscbs_mono[0] - field_names = [child.tag.split("}")[-1] for child in mono_elem] + # Field order per schema TMonofasia/gMonoPadrao: + # qBCMono, adRemIBS, adRemCBS, vIBSMono, vCBSMono + # (adRemCBS must come BEFORE vIBSMono per DFeTiposBasicos_v1.00.xsd) + padrao_elem = gmono_padrao[0] + field_names = [child.tag.split("}")[-1] for child in padrao_elem] self.assertEqual( field_names, - ["qBCMono", "adRemIBS", "vIBSMono", "adRemCBS", "vCBSMono"], + ["qBCMono", "adRemIBS", "adRemCBS", "vIBSMono", "vCBSMono"], ) def test_cst620_monofasica_com_valores_calculados(self): @@ -768,20 +773,21 @@ def test_cst620_monofasica_com_valores_calculados(self): xml = self._serializar_e_assinar() + # Values live under / per NT 2025.002-RTC self.assertEqual( - xml.xpath("//ns:gIBSCBSMono/ns:qBCMono", namespaces=self.ns)[0].text, "100.0000" + xml.xpath("//ns:gMonoPadrao/ns:qBCMono", namespaces=self.ns)[0].text, "100.0000" ) self.assertEqual( - xml.xpath("//ns:gIBSCBSMono/ns:adRemIBS", namespaces=self.ns)[0].text, "0.1500" + xml.xpath("//ns:gMonoPadrao/ns:adRemIBS", namespaces=self.ns)[0].text, "0.1500" ) self.assertEqual( - xml.xpath("//ns:gIBSCBSMono/ns:vIBSMono", namespaces=self.ns)[0].text, "15.00" + xml.xpath("//ns:gMonoPadrao/ns:vIBSMono", namespaces=self.ns)[0].text, "15.00" ) self.assertEqual( - xml.xpath("//ns:gIBSCBSMono/ns:adRemCBS", namespaces=self.ns)[0].text, "0.8500" + xml.xpath("//ns:gMonoPadrao/ns:adRemCBS", namespaces=self.ns)[0].text, "0.8500" ) self.assertEqual( - xml.xpath("//ns:gIBSCBSMono/ns:vCBSMono", namespaces=self.ns)[0].text, "85.00" + xml.xpath("//ns:gMonoPadrao/ns:vCBSMono", namespaces=self.ns)[0].text, "85.00" ) def test_cst000_nao_emite_gibscbsmono_regressao(self): From fa099e8e067e713f9d6650da61f3eeeb4b14ec09 Mon Sep 17 00:00:00 2001 From: Felipe Correa Date: Sat, 25 Apr 2026 12:32:47 -0300 Subject: [PATCH 3/4] fix(reforma): adiciona vTotIBSMonoItem e vTotCBSMonoItem em gIBSCBSMono [DEV-1954] (#4) Schema TMonofasia (DFeTiposBasicos_v1.00.xsd) define que gIBSCBSMono possui duas tags REQUERIDAS (sem minOccurs=0) como siblings de gMonoPadrao: vTotIBSMonoItem e vTotCBSMonoItem (ambos TDec1302RTC). Antes deste fix, mesmo apos a DEV-1953 que adicionou o wrapper gMonoPadrao, o XML emitido por PyNFe ainda terminava em sem os dois totais, e SEFAZ rejeitava com cStat 225 "Falha no Schema XML da NFe (Elemento: ...gIBSCBSMono/)" para todas as notas com CST 620 do cliente E B DA FONSECA (loja 37, CNPJ 24712859000191). Mudancas: - NotaFiscalProduto: dois novos atributos ibscbs_v_tot_ibs_mono_item e ibscbs_v_tot_cbs_mono_item (default Decimal()) aceitos via kwargs em adicionar_produto_servico (Entidade.__setattr__ exige existencia previa). - _serializar_gibscbs_mono: emite os dois totais como filhos diretos de gIBSCBSMono apos o gMonoPadrao, formatados em 2 casas decimais. Para itens single-line (sem retencao/diferimento) os totais devem igualar vIBSMono / vCBSMono. Quando nao informados pelo caller, emite 0.00 (default seguro durante o Teste de Carga 2026 com ad rem zerados). - Tests: estende test_cst620_monofasica_emite_gibscbsmono e test_cst620_monofasica_com_valores_calculados para asserir presenca, ordem e tipo decimal dos totais. Adiciona test_cst620_v_tot_mono_item_default_zero_when_unset para garantir que o default seguro funciona quando o caller omite os campos. - Docs (reforma_tributaria.md): atualiza o exemplo XML e a tabela de campos com vTotIBSMonoItem / vTotCBSMonoItem. --- docs/reforma_tributaria.md | 10 ++- pynfe/entidades/notafiscal.py | 6 ++ pynfe/processamento/serializacao.py | 28 ++++-- ...est_nfe_serializacao_reforma_tributaria.py | 89 ++++++++++++++++++- 4 files changed, 123 insertions(+), 10 deletions(-) diff --git a/docs/reforma_tributaria.md b/docs/reforma_tributaria.md index 14c06627..cc2c410f 100644 --- a/docs/reforma_tributaria.md +++ b/docs/reforma_tributaria.md @@ -43,7 +43,7 @@ A implementacao cobre: ### Tributacao monofasica — `gIBSCBSMono` -Para produtos com CST 620 (combustiveis, etc.) o grupo emitido dentro de `` e `` ao inves de ``. Conforme o schema oficial (`DFeTiposBasicos_v1.00.xsd`, type `TMonofasia`), os cinco campos monofasicos vivem sob o wrapper obrigatorio `` e na ordem definida pelo schema: +Para produtos com CST 620 (combustiveis, etc.) o grupo emitido dentro de `` e `` ao inves de ``. Conforme o schema oficial (`DFeTiposBasicos_v1.00.xsd`, type `TMonofasia`), os cinco campos monofasicos vivem sob o wrapper obrigatorio `` e na ordem definida pelo schema. Alem disso, `` e `` sao SIBLINGS de `` (NAO filhos) e ambos sao OBRIGATORIOS por schema (sem `minOccurs=0`): ```xml @@ -54,6 +54,8 @@ Para produtos com CST 620 (combustiveis, etc.) o grupo emitido dentro de `1.80 0.00 + 1.80 + 0.00 ``` @@ -64,8 +66,12 @@ Para produtos com CST 620 (combustiveis, etc.) o grupo emitido dentro de ` inside (NOT children of ). For a + # single-line item without retencao/diferimento, they equal vIBSMono/vCBSMono. ibscbs_q_bc_mono = Decimal() # qBCMono ibscbs_ad_rem_ibs = Decimal() # adRemIBS ibscbs_v_ibs_mono = Decimal() # vIBSMono ibscbs_ad_rem_cbs = Decimal() # adRemCBS ibscbs_v_cbs_mono = Decimal() # vCBSMono + ibscbs_v_tot_ibs_mono_item = Decimal() # vTotIBSMonoItem + ibscbs_v_tot_cbs_mono_item = Decimal() # vTotCBSMonoItem # IS (Imposto Seletivo) - Group UB-IS is_cst_selec = str() # CSTSelec (2-digit) diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index a8f60804..c74f8cdb 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -1399,9 +1399,10 @@ def _serializar_gibscbs_mono(self, produto_servico, ibscbs): Estrutura obrigatoria por NT 2025.002-RTC (type TMonofasia do DFeTiposBasicos_v1.00.xsd, sequencia gMonoPadrao -> gMonoReten -> - gMonoRet -> gMonoDif). Para CST 620 "Tributacao monofasica padrao" - emitimos apenas com os cinco campos na ordem do - schema: + gMonoRet -> gMonoDif -> vTotIBSMonoItem -> vTotCBSMonoItem). Para + CST 620 "Tributacao monofasica padrao" emitimos apenas + com os cinco campos na ordem do schema, seguido pelos dois totais + item-level que sao REQUERIDOS pelo schema (sem minOccurs=0): @@ -1411,16 +1412,22 @@ def _serializar_gibscbs_mono(self, produto_servico, ibscbs): TDec1302RTC (2 casas) TDec1302RTC (2 casas) + TDec1302RTC (2 casas) + TDec1302RTC (2 casas) IMPORTANT: - - O wrapper e obrigatorio. Antes desta correcao o - grupo era emitido flat (sem o wrapper) e SEFAZ rejeitava com - cStat 225 "Falha no Schema XML da NFe (Elemento: + - O wrapper e obrigatorio. Antes da correcao da + DEV-1953 o grupo era emitido flat (sem o wrapper) e SEFAZ + rejeitava com cStat 225 "Falha no Schema XML da NFe (Elemento: enviNFe/NFe[1]/infNFe/det[1]/imposto/IBSCBS/gIBSCBSMono/qBCMono)". - A ordem antes de segue o schema oficial (linhas 701-725 de DFeTiposBasicos_v1.00.xsd). A ordem anterior ( antes de ) tambem violava o schema. + - e sao SIBLINGS de + (NAO filhos), ambos OBRIGATORIOS por schema (DEV-1954). + Sem eles SEFAZ rejeita com cStat 225 "Falha no Schema XML da NFe + (Elemento: ... gIBSCBSMono/)" (barra final indica fechamento). """ gibscbs_mono = etree.SubElement(ibscbs, "gIBSCBSMono") gmono_padrao = etree.SubElement(gibscbs_mono, "gMonoPadrao") @@ -1439,6 +1446,15 @@ def _serializar_gibscbs_mono(self, produto_servico, ibscbs): etree.SubElement(gmono_padrao, "vCBSMono").text = "{:.2f}".format( produto_servico.ibscbs_v_cbs_mono or 0 ) + # vTotIBSMonoItem / vTotCBSMonoItem: required siblings of + # per schema TMonofasia (DEV-1954). For a single-line mono item without + # retencao/diferimento, these equal vIBSMono/vCBSMono. + etree.SubElement(gibscbs_mono, "vTotIBSMonoItem").text = "{:.2f}".format( + produto_servico.ibscbs_v_tot_ibs_mono_item or 0 + ) + etree.SubElement(gibscbs_mono, "vTotCBSMonoItem").text = "{:.2f}".format( + produto_servico.ibscbs_v_tot_cbs_mono_item or 0 + ) def _serializar_is(self, produto_servico, tag_raiz): """Serializa (Imposto Seletivo) como filho direto de . diff --git a/tests/test_nfe_serializacao_reforma_tributaria.py b/tests/test_nfe_serializacao_reforma_tributaria.py index df076230..1179cec6 100644 --- a/tests/test_nfe_serializacao_reforma_tributaria.py +++ b/tests/test_nfe_serializacao_reforma_tributaria.py @@ -672,7 +672,11 @@ def test_cmunfgibs_emitido_no_ide(self): # ------------------------------------------------------------------ def test_cst620_monofasica_emite_gibscbsmono(self): """CST 620 (tributacao monofasica) must emit with - qBCMono, adRemIBS, vIBSMono, adRemCBS, vCBSMono — NOT .""" + qBCMono, adRemIBS, vIBSMono, adRemCBS, vCBSMono — NOT . + + DEV-1954: also asserts vTotIBSMonoItem / vTotCBSMonoItem siblings of + (required by schema TMonofasia). + """ emitente = self._emitente() cliente = self._cliente() nf = self._nota_fiscal(emitente, cliente) @@ -695,6 +699,9 @@ def test_cst620_monofasica_emite_gibscbsmono(self): ibscbs_v_ibs_mono=Decimal("0.00"), ibscbs_ad_rem_cbs=Decimal("0.0000"), ibscbs_v_cbs_mono=Decimal("0.00"), + # Item-level totals (DEV-1954) - required siblings of + ibscbs_v_tot_ibs_mono_item=Decimal("0.00"), + ibscbs_v_tot_cbs_mono_item=Decimal("0.00"), ) nf.adicionar_produto_servico(**kwargs) nf.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=1332.72, ind_pag=0) @@ -750,8 +757,37 @@ def test_cst620_monofasica_emite_gibscbsmono(self): ["qBCMono", "adRemIBS", "adRemCBS", "vIBSMono", "vCBSMono"], ) + # DEV-1954: vTotIBSMonoItem / vTotCBSMonoItem must be SIBLINGS of + # (children of ) — NOT children of + # . Both are REQUIRED by schema TMonofasia. + v_tot_ibs = xml.xpath("//ns:IBSCBS/ns:gIBSCBSMono/ns:vTotIBSMonoItem", namespaces=self.ns) + self.assertEqual(len(v_tot_ibs), 1) + self.assertEqual(v_tot_ibs[0].text, "0.00") + v_tot_cbs = xml.xpath("//ns:IBSCBS/ns:gIBSCBSMono/ns:vTotCBSMonoItem", namespaces=self.ns) + self.assertEqual(len(v_tot_cbs), 1) + self.assertEqual(v_tot_cbs[0].text, "0.00") + + # Totals must NOT be children of (common misreading of schema) + wrong_v_tot_ibs = xml.xpath("//ns:gMonoPadrao/ns:vTotIBSMonoItem", namespaces=self.ns) + self.assertEqual(len(wrong_v_tot_ibs), 0) + wrong_v_tot_cbs = xml.xpath("//ns:gMonoPadrao/ns:vTotCBSMonoItem", namespaces=self.ns) + self.assertEqual(len(wrong_v_tot_cbs), 0) + + # Direct children of must be in schema order: + # gMonoPadrao -> vTotIBSMonoItem -> vTotCBSMonoItem + gibscbs_mono_elem = gibscbs_mono[0] + direct_children = [child.tag.split("}")[-1] for child in gibscbs_mono_elem] + self.assertEqual( + direct_children, + ["gMonoPadrao", "vTotIBSMonoItem", "vTotCBSMonoItem"], + ) + def test_cst620_monofasica_com_valores_calculados(self): - """CST 620 with non-zero ad rem rates produces non-zero monophasic values.""" + """CST 620 with non-zero ad rem rates produces non-zero monophasic values. + + DEV-1954: For a single-line item without retencao/diferimento, + vTotIBSMonoItem == vIBSMono and vTotCBSMonoItem == vCBSMono. + """ emitente = self._emitente() cliente = self._cliente() nf = self._nota_fiscal(emitente, cliente) @@ -767,6 +803,9 @@ def test_cst620_monofasica_com_valores_calculados(self): ibscbs_v_ibs_mono=Decimal("15.00"), ibscbs_ad_rem_cbs=Decimal("0.8500"), ibscbs_v_cbs_mono=Decimal("85.00"), + # Item-level totals equal the mono values for single-line items + ibscbs_v_tot_ibs_mono_item=Decimal("15.00"), + ibscbs_v_tot_cbs_mono_item=Decimal("85.00"), ) nf.adicionar_produto_servico(**kwargs) nf.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=1000.00, ind_pag=0) @@ -789,6 +828,52 @@ def test_cst620_monofasica_com_valores_calculados(self): self.assertEqual( xml.xpath("//ns:gMonoPadrao/ns:vCBSMono", namespaces=self.ns)[0].text, "85.00" ) + # DEV-1954: item-level totals equal the mono values for single-line items + self.assertEqual( + xml.xpath("//ns:IBSCBS/ns:gIBSCBSMono/ns:vTotIBSMonoItem", namespaces=self.ns)[0].text, + "15.00", + ) + self.assertEqual( + xml.xpath("//ns:IBSCBS/ns:gIBSCBSMono/ns:vTotCBSMonoItem", namespaces=self.ns)[0].text, + "85.00", + ) + + def test_cst620_v_tot_mono_item_default_zero_when_unset(self): + """DEV-1954: vTotIBSMonoItem / vTotCBSMonoItem default to 0.00 when not provided. + + Both are REQUIRED by schema TMonofasia (no minOccurs=0). Falling back + to 0 keeps the XML schema-valid even when callers forget to set them. + """ + emitente = self._emitente() + cliente = self._cliente() + nf = self._nota_fiscal(emitente, cliente) + + kwargs = self._base_product_kwargs() + kwargs.update( + codigo="012", + descricao="Combustivel monofasico sem totais explicitos", + ibscbs_cst="620", + ibscbs_c_class_trib="620006", + ibscbs_q_bc_mono=Decimal("18.0000"), + ibscbs_ad_rem_ibs=Decimal("0.0000"), + ibscbs_v_ibs_mono=Decimal("0.00"), + ibscbs_ad_rem_cbs=Decimal("0.0000"), + ibscbs_v_cbs_mono=Decimal("0.00"), + # NOTE: ibscbs_v_tot_ibs_mono_item / ibscbs_v_tot_cbs_mono_item NOT set + ) + nf.adicionar_produto_servico(**kwargs) + nf.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=1332.72, ind_pag=0) + + xml = self._serializar_e_assinar() + + # Both totals MUST be emitted as direct children of + # with the schema-required default of 0.00. + v_tot_ibs = xml.xpath("//ns:IBSCBS/ns:gIBSCBSMono/ns:vTotIBSMonoItem", namespaces=self.ns) + self.assertEqual(len(v_tot_ibs), 1) + self.assertEqual(v_tot_ibs[0].text, "0.00") + v_tot_cbs = xml.xpath("//ns:IBSCBS/ns:gIBSCBSMono/ns:vTotCBSMonoItem", namespaces=self.ns) + self.assertEqual(len(v_tot_cbs), 1) + self.assertEqual(v_tot_cbs[0].text, "0.00") def test_cst000_nao_emite_gibscbsmono_regressao(self): """Regression test: CST 000 (regular taxation) must still emit From 354cb8ef6ec306e0c27c852df1208510cc096c86 Mon Sep 17 00:00:00 2001 From: Felipe Correa Date: Mon, 27 Apr 2026 16:08:25 -0300 Subject: [PATCH 4/4] fix(reforma): adiciona gMono em IBSCBSTot para itens monofasicos [DEV-1955] (#5) * fix(reforma): adiciona gMono em IBSCBSTot para itens monofasicos [DEV-1955] Quando uma NF-e contem itens com (CST 620), o totalizador precisa emitir o subgrupo com os seis campos obrigatorios (vIBSMono, vCBSMono, vIBSMonoReten, vCBSMonoReten, vIBSMonoRet, vCBSMonoRet) conforme schema TIBSCBSMonoTot/gMono em DFeTiposBasicos_v1.00.xsd. Sem o subgrupo, SEFAZ rejeita com cStat 1119 ("Total de IBS e CBS nao informado"). Mudancas: - pynfe/entidades/notafiscal.py: adiciona constante _IBSCBS_CST_MONOFASICO (espelho do set definido em SerializacaoXML), seis acumuladores totais_v_ibs_mono / totais_v_cbs_mono / totais_v_*_mono_reten / totais_v_*_mono_ret e contador totais_mono_item_count. Em adicionar_produto_servico, soma vTotIBSMonoItem / vTotCBSMonoItem dos itens e incrementa o contador quando o CST e monofasico. - pynfe/processamento/serializacao.py: forca emissao de quando ha pelo menos um item monofasico (mesmo com totais standard zerados) e emite com os seis filhos quando totais_mono_item_count > 0. Os filhos Reten/Ret ainda sao zero pois PyNFe so suporta a nivel de item, mas o schema exige todos os seis quando e emitido. - tests/test_nfe_serializacao_reforma_tributaria.py: 4 testes novos: test_ibscbstot_gmono_emitido_para_item_monofasico_unico (cenario E B DA FONSECA com qtde 18 e ad rem zero), test_ibscbstot_gmono_soma_ multiplos_itens_monofasicos, test_ibscbstot_sem_gmono_quando_so_itens_ padrao (regressao), test_ibscbstot_misto_emite_gibs_gcbs_e_gmono. - docs/reforma_tributaria.md: atualiza exemplo de IBSCBSTot para incluir e adiciona notas sobre quando o subgrupo e obrigatorio. Tests: 153 passed (149 baseline + 4 novos). ruff check + format clean. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(reforma): substitui em-dashes por ASCII em comentarios [DEV-1955] Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- docs/reforma_tributaria.md | 13 +- pynfe/entidades/notafiscal.py | 39 ++++ pynfe/processamento/serializacao.py | 41 +++- ...est_nfe_serializacao_reforma_tributaria.py | 221 ++++++++++++++++++ 4 files changed, 310 insertions(+), 4 deletions(-) diff --git a/docs/reforma_tributaria.md b/docs/reforma_tributaria.md index cc2c410f..2312ecb5 100644 --- a/docs/reforma_tributaria.md +++ b/docs/reforma_tributaria.md @@ -335,13 +335,22 @@ Os totais ficam em um grupo **separado** de ``, como irmao dentro de `< 0.00 0.00 - + + 0.00 + 0.00 + 0.00 + 0.00 + 0.00 + 0.00 + ``` > Os subgrupos `gIBS` e `gCBS` sao opcionais (`minOccurs="0"`) — emitidos apenas quando ha valores. Os campos `vDif`, `vDevTrib`, `vCredPres` e `vCredPresCondSus` sao obrigatorios dentro de cada subgrupo (emitidos como "0.00" quando nao utilizados). +> +> O subgrupo `gMono` e opcional, mas obrigatorio sempre que algum item da NF-e carregar `` (CST 620, etc.). Quando emitido, **todos os seis filhos** sao obrigatorios (`vIBSMono`, `vCBSMono`, `vIBSMonoReten`, `vCBSMonoReten`, `vIBSMonoRet`, `vCBSMonoRet`). Omitir `` em uma NF-e com items monofasicos faz a SEFAZ rejeitar com `cStat 1119 - "Total de IBS e CBS nao informado"`. Os totais `Reten`/`Ret` ainda nao sao acumulados a nivel de item (PyNFe ainda so emite ``), entao serao "0.00" ate que `` / `` / `` sejam suportados a nivel de item. ### Cabecalho — `cMunFGIBS` no `` @@ -373,7 +382,7 @@ Esses CSTs geram apenas `` e ``, sem ``. - **`cClassTrib`**: Emitido quando informado (campo obrigatorio na pratica) - **`cMunFGIBS`**: Emitido no `` apenas quando informado -- **``**: Tipo `TIBSCBSMonoTot`. Omitido se todos os totais forem zero. Quando emitido, `vBCIBSCBS` e obrigatorio como primeiro filho; `gIBS` e `gCBS` sao opcionais +- **``**: Tipo `TIBSCBSMonoTot`. Omitido se todos os totais forem zero E nenhum item carregar ``. Quando emitido, `vBCIBSCBS` e obrigatorio como primeiro filho; `gIBS`, `gCBS` e `gMono` sao opcionais (mas `gMono` e obrigatorio sempre que houver items monofasicos) - **``**: Tipo `TTribNFe`. Omitido completamente se `ibscbs_cst` nao for informado - **IS (``)**: Tipo `TIS`. **Nao emitido no XML** — serializacao desabilitada ate 2027 - **``**: Tipo `TISTot`. **Nao emitido** — sera irmao de `` (antes dele no schema) diff --git a/pynfe/entidades/notafiscal.py b/pynfe/entidades/notafiscal.py index 42727035..7407e44e 100644 --- a/pynfe/entidades/notafiscal.py +++ b/pynfe/entidades/notafiscal.py @@ -10,6 +10,12 @@ from .base import CampoDeprecated, Entidade +# CSTs IBS/CBS that route to instead of . Mirrors +# the constant in pynfe.processamento.serializacao but defined here so the +# NotaFiscal accumulator can detect monofasic items without importing the +# serializer. Keep in sync with SerializacaoXML._IBSCBS_CST_MONOFASICO. +_IBSCBS_CST_MONOFASICO = ("620",) + class NotaFiscal(Entidade): # campos deprecados @@ -310,6 +316,24 @@ class NotaFiscal(Entidade): totais_cbs = Decimal() totais_is = Decimal() + # Reforma Tributaria - Totais Monofasia (Group W03 - IBSCBSTot/gMono) + # Per NT 2025.002-RTC schema TIBSCBSMonoTot, when ANY item carries + # , the IBSCBSTot wrapper MUST emit a sibling with + # six TDec1302RTC values (all REQUIRED inside ): vIBSMono, + # vCBSMono, vIBSMonoReten, vCBSMonoReten, vIBSMonoRet, vCBSMonoRet. + # SEFAZ rejects with cStat 1119 ("Total de IBS e CBS nao informado") + # when items emit but the totalizer omits . + # ``totais_mono_item_count`` tracks the number of monofasic items so + # the serializer can decide to emit even when all six values + # are zero (Teste de Carga 2026 scenario, where every ad rem is 0). + totais_v_ibs_mono = Decimal() # sum of vTotIBSMonoItem (gMonoPadrao) + totais_v_cbs_mono = Decimal() # sum of vTotCBSMonoItem (gMonoPadrao) + totais_v_ibs_mono_reten = Decimal() # sum of vTotIBSMonoItem (gMonoReten) + totais_v_cbs_mono_reten = Decimal() # sum of vTotCBSMonoItem (gMonoReten) + totais_v_ibs_mono_ret = Decimal() # sum of vTotIBSMonoItem (gMonoRet) + totais_v_cbs_mono_ret = Decimal() # sum of vTotCBSMonoItem (gMonoRet) + totais_mono_item_count = int() + # Reforma Tributaria - cMunFGIBS (Group B) municipio_fato_gerador_ibs = str() @@ -483,6 +507,21 @@ def adicionar_produto_servico(self, **kwargs): self.totais_cbs += obj.ibscbs_v_cbs self.totais_is += obj.is_valor + # Reforma Tributaria - Totais Monofasia (IBSCBSTot/gMono per NT 2025.002-RTC) + # Aggregate item-level vTotIBSMonoItem / vTotCBSMonoItem so the + # serializer can emit with the required totals. PyNFe today + # only supports at item level (see _serializar_gibscbs_mono), + # so the Reten/Ret accumulators stay at zero, but the schema requires + # all six fields to be emitted whenever is present, so they + # are kept here to align with TIBSCBSMonoTot/gMono. + # ``totais_mono_item_count`` is incremented when CST routes to mono, + # giving the serializer a stable signal to emit even if every + # value is zero (Teste de Carga 2026 scenario). + self.totais_v_ibs_mono += obj.ibscbs_v_tot_ibs_mono_item + self.totais_v_cbs_mono += obj.ibscbs_v_tot_cbs_mono_item + if obj.ibscbs_cst in _IBSCBS_CST_MONOFASICO: + self.totais_mono_item_count += 1 + # TODO calcular impostos aproximados # self.totais_tributos_aproximado += obj.tributos diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index c74f8cdb..c94aec2e 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -1875,8 +1875,17 @@ def _serializar_nota_fiscal(self, nota_fiscal, tag_raiz="infNFe", retorna_string # Reforma Tributaria - Totais IVA Dual (Group W03 - IBSCBSTot) # Type: TIBSCBSMonoTot (PL 010b DFeTiposBasicos_v1.00.xsd) + # ``has_mono`` is true when at least one item routed to : + # SEFAZ rejects with cStat 1119 ("Total de IBS e CBS nao informado") + # when items emit monofasia but the IBSCBSTot/gMono total is missing. + # During Teste de Carga 2026, every ad rem is zero so the regular + # accumulators stay at 0; the count flag forces emission anyway. + has_mono = nota_fiscal.totais_mono_item_count > 0 has_reforma = ( - nota_fiscal.totais_vbc_ibscbs or nota_fiscal.totais_ibs or nota_fiscal.totais_cbs + nota_fiscal.totais_vbc_ibscbs + or nota_fiscal.totais_ibs + or nota_fiscal.totais_cbs + or has_mono ) if has_reforma: ibscbs_tot = etree.SubElement(total, "IBSCBSTot") @@ -1915,7 +1924,35 @@ def _serializar_nota_fiscal(self, nota_fiscal, tag_raiz="infNFe", retorna_string etree.SubElement(g_cbs, "vCredPres").text = "0.00" etree.SubElement(g_cbs, "vCredPresCondSus").text = "0.00" - # gMono: not implemented yet (monofasia totals) + # gMono - totals da monofasia (DEV-1955) + # Per NT 2025.002-RTC schema TIBSCBSMonoTot, the wrapper is + # itself optional (minOccurs=0) but, when present, ALL six children + # are REQUIRED (no minOccurs=0 inside). SEFAZ requires + # whenever the NF-e contains items with ; omitting it + # raises cStat 1119 ("Total de IBS e CBS nao informado"). For the + # Teste de Carga 2026 scenario every value is "0.00", but the + # block itself must still be emitted. + if has_mono: + g_mono = etree.SubElement(ibscbs_tot, "gMono") + etree.SubElement(g_mono, "vIBSMono").text = "{:.2f}".format( + nota_fiscal.totais_v_ibs_mono + ) + etree.SubElement(g_mono, "vCBSMono").text = "{:.2f}".format( + nota_fiscal.totais_v_cbs_mono + ) + etree.SubElement(g_mono, "vIBSMonoReten").text = "{:.2f}".format( + nota_fiscal.totais_v_ibs_mono_reten + ) + etree.SubElement(g_mono, "vCBSMonoReten").text = "{:.2f}".format( + nota_fiscal.totais_v_cbs_mono_reten + ) + etree.SubElement(g_mono, "vIBSMonoRet").text = "{:.2f}".format( + nota_fiscal.totais_v_ibs_mono_ret + ) + etree.SubElement(g_mono, "vCBSMonoRet").text = "{:.2f}".format( + nota_fiscal.totais_v_cbs_mono_ret + ) + # gEstornoCred: not implemented yet (estorno de credito totals) # Transporte diff --git a/tests/test_nfe_serializacao_reforma_tributaria.py b/tests/test_nfe_serializacao_reforma_tributaria.py index 1179cec6..4ce522f7 100644 --- a/tests/test_nfe_serializacao_reforma_tributaria.py +++ b/tests/test_nfe_serializacao_reforma_tributaria.py @@ -914,6 +914,227 @@ def test_cst000_nao_emite_gibscbsmono_regressao(self): p_cbs = xml.xpath("//ns:gIBSCBS/ns:gCBS/ns:pCBS", namespaces=self.ns)[0].text self.assertEqual(p_cbs, "0.9000") + # ------------------------------------------------------------------ + # DEV-1955 — IBSCBSTot/ totals when items carry + # ------------------------------------------------------------------ + def test_ibscbstot_gmono_emitido_para_item_monofasico_unico(self): + """A NF-e with a single item must emit IBSCBSTot/. + + Reproduces the cliente E B DA FONSECA (DEV-1955) scenario: GLP em Botijao + 13KG, qtde 18, CST 620, cClassTrib 620006, all ad-rem zero (Teste de Carga + 2026). Before the fix, the IBSCBSTot wrapper was suppressed entirely + (because totais_vbc_ibscbs / totais_ibs / totais_cbs were all 0) and + SEFAZ rejected with cStat 1119 "Total de IBS e CBS nao informado". + """ + emitente = self._emitente() + cliente = self._cliente() + nf = self._nota_fiscal(emitente, cliente) + + kwargs = self._base_product_kwargs() + kwargs.update( + codigo="020", + descricao="GLP em Botijao 13KG", + ncm="27111910", + quantidade_comercial=Decimal("18"), + valor_unitario_comercial=Decimal("74.04"), + valor_total_bruto=Decimal("1332.72"), + quantidade_tributavel=Decimal("18"), + valor_unitario_tributavel=Decimal("74.04"), + ibscbs_cst="620", + ibscbs_c_class_trib="620006", + ibscbs_q_bc_mono=Decimal("18.0000"), + ibscbs_ad_rem_ibs=Decimal("0.0000"), + ibscbs_v_ibs_mono=Decimal("0.00"), + ibscbs_ad_rem_cbs=Decimal("0.0000"), + ibscbs_v_cbs_mono=Decimal("0.00"), + ibscbs_v_tot_ibs_mono_item=Decimal("0.00"), + ibscbs_v_tot_cbs_mono_item=Decimal("0.00"), + ) + nf.adicionar_produto_servico(**kwargs) + nf.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=1332.72, ind_pag=0) + + xml = self._serializar_e_assinar() + + # IBSCBSTot must be present even when standard totals are zero, because + # at least one item carries . + ibscbs_tot = xml.xpath("//ns:total/ns:IBSCBSTot", namespaces=self.ns) + self.assertEqual(len(ibscbs_tot), 1) + + # gMono inside IBSCBSTot must contain the six required TDec1302RTC fields, + # all "0.00" for the Teste de Carga scenario. + g_mono = xml.xpath("//ns:IBSCBSTot/ns:gMono", namespaces=self.ns) + self.assertEqual(len(g_mono), 1) + + v_ibs_mono = xml.xpath("//ns:IBSCBSTot/ns:gMono/ns:vIBSMono", namespaces=self.ns) + self.assertEqual(len(v_ibs_mono), 1) + self.assertEqual(v_ibs_mono[0].text, "0.00") + + v_cbs_mono = xml.xpath("//ns:IBSCBSTot/ns:gMono/ns:vCBSMono", namespaces=self.ns) + self.assertEqual(v_cbs_mono[0].text, "0.00") + + # Reten/Ret children are required by schema TIBSCBSMonoTot/gMono even + # when the items only carry (PyNFe item-level Reten/Ret + # serialization is not implemented yet — totals stay at zero). + v_ibs_mono_reten = xml.xpath("//ns:IBSCBSTot/ns:gMono/ns:vIBSMonoReten", namespaces=self.ns) + self.assertEqual(v_ibs_mono_reten[0].text, "0.00") + v_cbs_mono_reten = xml.xpath("//ns:IBSCBSTot/ns:gMono/ns:vCBSMonoReten", namespaces=self.ns) + self.assertEqual(v_cbs_mono_reten[0].text, "0.00") + v_ibs_mono_ret = xml.xpath("//ns:IBSCBSTot/ns:gMono/ns:vIBSMonoRet", namespaces=self.ns) + self.assertEqual(v_ibs_mono_ret[0].text, "0.00") + v_cbs_mono_ret = xml.xpath("//ns:IBSCBSTot/ns:gMono/ns:vCBSMonoRet", namespaces=self.ns) + self.assertEqual(v_cbs_mono_ret[0].text, "0.00") + + # Field order must match TIBSCBSMonoTot/gMono per + # DFeTiposBasicos_v1.00.xsd: vIBSMono -> vCBSMono -> vIBSMonoReten -> + # vCBSMonoReten -> vIBSMonoRet -> vCBSMonoRet. + gmono_elem = g_mono[0] + gmono_children = [child.tag.split("}")[-1] for child in gmono_elem] + self.assertEqual( + gmono_children, + [ + "vIBSMono", + "vCBSMono", + "vIBSMonoReten", + "vCBSMonoReten", + "vIBSMonoRet", + "vCBSMonoRet", + ], + ) + + def test_ibscbstot_gmono_soma_multiplos_itens_monofasicos(self): + """Two items must sum into IBSCBSTot//.""" + emitente = self._emitente() + cliente = self._cliente() + nf = self._nota_fiscal(emitente, cliente) + + item1 = self._base_product_kwargs() + item1.update( + codigo="021", + descricao="Combustivel mono A", + ibscbs_cst="620", + ibscbs_c_class_trib="620001", + ibscbs_q_bc_mono=Decimal("100.0000"), + ibscbs_ad_rem_ibs=Decimal("0.1500"), + ibscbs_v_ibs_mono=Decimal("15.00"), + ibscbs_ad_rem_cbs=Decimal("0.8500"), + ibscbs_v_cbs_mono=Decimal("85.00"), + ibscbs_v_tot_ibs_mono_item=Decimal("15.00"), + ibscbs_v_tot_cbs_mono_item=Decimal("85.00"), + ) + item2 = self._base_product_kwargs() + item2.update( + codigo="022", + descricao="Combustivel mono B", + ibscbs_cst="620", + ibscbs_c_class_trib="620002", + ibscbs_q_bc_mono=Decimal("50.0000"), + ibscbs_ad_rem_ibs=Decimal("0.2000"), + ibscbs_v_ibs_mono=Decimal("10.00"), + ibscbs_ad_rem_cbs=Decimal("0.4000"), + ibscbs_v_cbs_mono=Decimal("20.00"), + ibscbs_v_tot_ibs_mono_item=Decimal("10.00"), + ibscbs_v_tot_cbs_mono_item=Decimal("20.00"), + ) + nf.adicionar_produto_servico(**item1) + nf.adicionar_produto_servico(**item2) + nf.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=2000.00, ind_pag=0) + + xml = self._serializar_e_assinar() + + v_ibs_mono = xml.xpath("//ns:IBSCBSTot/ns:gMono/ns:vIBSMono", namespaces=self.ns) + self.assertEqual(v_ibs_mono[0].text, "25.00") # 15.00 + 10.00 + v_cbs_mono = xml.xpath("//ns:IBSCBSTot/ns:gMono/ns:vCBSMono", namespaces=self.ns) + self.assertEqual(v_cbs_mono[0].text, "105.00") # 85.00 + 20.00 + + def test_ibscbstot_sem_gmono_quando_so_itens_padrao(self): + """Pure standard NF-e (no items) must NOT emit .""" + emitente = self._emitente() + cliente = self._cliente() + nf = self._nota_fiscal(emitente, cliente) + + kwargs = self._base_product_kwargs() + kwargs.update( + ibscbs_cst="000", + ibscbs_c_class_trib="000001", + ibscbs_vbc=Decimal("1000.00"), + ibscbs_p_ibs_uf=Decimal("0.1000"), + ibscbs_v_ibs_uf=Decimal("1.00"), + ibscbs_p_ibs_mun=Decimal("0.0000"), + ibscbs_v_ibs_mun=Decimal("0.00"), + ibscbs_v_ibs=Decimal("1.00"), + ibscbs_p_cbs=Decimal("0.9000"), + ibscbs_v_cbs=Decimal("9.00"), + ) + nf.adicionar_produto_servico(**kwargs) + nf.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=1000.00, ind_pag=0) + + xml = self._serializar_e_assinar() + + # IBSCBSTot is present (we have standard reforma values) + ibscbs_tot = xml.xpath("//ns:total/ns:IBSCBSTot", namespaces=self.ns) + self.assertEqual(len(ibscbs_tot), 1) + # But gMono must NOT be there for a pure-standard NF-e + g_mono = xml.xpath("//ns:IBSCBSTot/ns:gMono", namespaces=self.ns) + self.assertEqual(len(g_mono), 0) + + def test_ibscbstot_misto_emite_gibs_gcbs_e_gmono(self): + """Mixed NF-e (1 standard + 1 mono) emits gIBS, gCBS AND gMono.""" + emitente = self._emitente() + cliente = self._cliente() + nf = self._nota_fiscal(emitente, cliente) + + item_padrao = self._base_product_kwargs() + item_padrao.update( + codigo="030", + descricao="Item padrao", + ibscbs_cst="000", + ibscbs_c_class_trib="000001", + ibscbs_vbc=Decimal("1000.00"), + ibscbs_p_ibs_uf=Decimal("0.1000"), + ibscbs_v_ibs_uf=Decimal("1.00"), + ibscbs_p_ibs_mun=Decimal("0.0000"), + ibscbs_v_ibs_mun=Decimal("0.00"), + ibscbs_v_ibs=Decimal("1.00"), + ibscbs_p_cbs=Decimal("0.9000"), + ibscbs_v_cbs=Decimal("9.00"), + ) + item_mono = self._base_product_kwargs() + item_mono.update( + codigo="031", + descricao="Item monofasico", + ibscbs_cst="620", + ibscbs_c_class_trib="620001", + ibscbs_q_bc_mono=Decimal("10.0000"), + ibscbs_ad_rem_ibs=Decimal("0.5000"), + ibscbs_v_ibs_mono=Decimal("5.00"), + ibscbs_ad_rem_cbs=Decimal("1.0000"), + ibscbs_v_cbs_mono=Decimal("10.00"), + ibscbs_v_tot_ibs_mono_item=Decimal("5.00"), + ibscbs_v_tot_cbs_mono_item=Decimal("10.00"), + ) + nf.adicionar_produto_servico(**item_padrao) + nf.adicionar_produto_servico(**item_mono) + nf.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=2000.00, ind_pag=0) + + xml = self._serializar_e_assinar() + + # Standard groups present (gIBS, gCBS) AND gMono present + self.assertEqual(len(xml.xpath("//ns:IBSCBSTot/ns:gIBS", namespaces=self.ns)), 1) + self.assertEqual(len(xml.xpath("//ns:IBSCBSTot/ns:gCBS", namespaces=self.ns)), 1) + self.assertEqual(len(xml.xpath("//ns:IBSCBSTot/ns:gMono", namespaces=self.ns)), 1) + + # gMono carries the mono item totals + v_ibs_mono = xml.xpath("//ns:IBSCBSTot/ns:gMono/ns:vIBSMono", namespaces=self.ns) + self.assertEqual(v_ibs_mono[0].text, "5.00") + v_cbs_mono = xml.xpath("//ns:IBSCBSTot/ns:gMono/ns:vCBSMono", namespaces=self.ns) + self.assertEqual(v_cbs_mono[0].text, "10.00") + + # IBSCBSTot direct-child order per TIBSCBSMonoTot: + # vBCIBSCBS -> gIBS -> gCBS -> gMono (gEstornoCred not implemented) + ibscbs_tot_elem = xml.xpath("//ns:IBSCBSTot", namespaces=self.ns)[0] + ibscbs_tot_children = [child.tag.split("}")[-1] for child in ibscbs_tot_elem] + self.assertEqual(ibscbs_tot_children, ["vBCIBSCBS", "gIBS", "gCBS", "gMono"]) + # ------------------------------------------------------------------ # Test 10: cMunFGIBS NOT emitted when not set # ------------------------------------------------------------------