diff --git a/docs/reforma_tributaria.md b/docs/reforma_tributaria.md index cae67eb2..2312ecb5 100644 --- a/docs/reforma_tributaria.md +++ b/docs/reforma_tributaria.md @@ -37,8 +37,43 @@ 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 ``. 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 + + + 18.0000 + 0.1000 + 0.0000 + 1.80 + 0.00 + + 1.80 + 0.00 + +``` + +| Campo | Tipo | Descricao | +|-------|------|-----------| +| `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 | +| `vTotIBSMonoItem` | TDec1302RTC | Total IBS monofasico do item (sibling de `gMonoPadrao`) | +| `vTotCBSMonoItem` | TDec1302RTC | Total CBS monofasico do item (sibling de `gMonoPadrao`) | + +Atributos na entidade `NotaFiscalProduto`: `ibscbs_q_bc_mono`, `ibscbs_ad_rem_ibs`, `ibscbs_v_ibs_mono`, `ibscbs_ad_rem_cbs`, `ibscbs_v_cbs_mono`, `ibscbs_v_tot_ibs_mono_item`, `ibscbs_v_tot_cbs_mono_item`. + +Para itens single-line (sem retencao / retencao anterior / diferimento), `vTotIBSMonoItem == vIBSMono` e `vTotCBSMonoItem == vCBSMono`. Quando os atributos nao sao informados pelo caller, o serializador emite `0.00` (default seguro durante o Teste de Carga 2026 com ad rem zerados). + +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 @@ -300,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 `` @@ -338,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 7047138e..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 @@ -1046,6 +1085,23 @@ 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 + # vTotIBSMonoItem / vTotCBSMonoItem = item-level mono totals (TDec1302RTC). Per + # NT 2025.002-RTC schema TMonofasia, these are REQUIRED siblings of + # 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) is_c_class_trib = str() # cClassTribIS 6-digit diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index cb463000..c94aec2e 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,116 @@ 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 (type TMonofasia do + DFeTiposBasicos_v1.00.xsd, sequencia gMonoPadrao -> gMonoReten -> + 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): + + + + TDec1104RTC (4 casas) + TDec_0302_04RTC (4 casas) + TDec_0302_04RTC (4 casas) + TDec1302RTC (2 casas) + TDec1302RTC (2 casas) + + TDec1302RTC (2 casas) + TDec1302RTC (2 casas) + + + IMPORTANT: + - 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") + etree.SubElement(gmono_padrao, "qBCMono").text = "{:.4f}".format( + produto_servico.ibscbs_q_bc_mono or 0 + ) + etree.SubElement(gmono_padrao, "adRemIBS").text = "{:.4f}".format( + produto_servico.ibscbs_ad_rem_ibs or 0 + ) + etree.SubElement(gmono_padrao, "adRemCBS").text = "{:.4f}".format( + produto_servico.ibscbs_ad_rem_cbs or 0 + ) + 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 + ) + # 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 . @@ -1799,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") @@ -1839,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 ee042b8a..4ce522f7 100644 --- a/tests/test_nfe_serializacao_reforma_tributaria.py +++ b/tests/test_nfe_serializacao_reforma_tributaria.py @@ -667,6 +667,474 @@ 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 . + + DEV-1954: also asserts vTotIBSMonoItem / vTotCBSMonoItem siblings of + (required by schema TMonofasia). + """ + 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"), + # 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) + + 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 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:gMonoPadrao/ns:qBCMono", namespaces=self.ns)[0].text + self.assertEqual(q_bc_mono, "18.0000") + + 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:gMonoPadrao/ns:vIBSMono", namespaces=self.ns)[0].text + self.assertEqual(v_ibs_mono, "0.00") + + 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:gMonoPadrao/ns:vCBSMono", namespaces=self.ns)[0].text + self.assertEqual(v_cbs_mono, "0.00") + + # 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", "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. + + 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) + + 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"), + # 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) + + xml = self._serializar_e_assinar() + + # Values live under / per NT 2025.002-RTC + self.assertEqual( + xml.xpath("//ns:gMonoPadrao/ns:qBCMono", namespaces=self.ns)[0].text, "100.0000" + ) + self.assertEqual( + xml.xpath("//ns:gMonoPadrao/ns:adRemIBS", namespaces=self.ns)[0].text, "0.1500" + ) + self.assertEqual( + xml.xpath("//ns:gMonoPadrao/ns:vIBSMono", namespaces=self.ns)[0].text, "15.00" + ) + self.assertEqual( + xml.xpath("//ns:gMonoPadrao/ns:adRemCBS", namespaces=self.ns)[0].text, "0.8500" + ) + 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 + 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") + + # ------------------------------------------------------------------ + # 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 # ------------------------------------------------------------------