Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion docs/reforma_tributaria.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<IBSCBS>` e `<gIBSCBSMono>` ao inves de `<gIBSCBS>`. Campos obrigatorios:
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A frase "o grupo emitido dentro de e " fica ambigua/gramaticalmente incorreta (parece listar dois grupos). Sugestao: reescrever para deixar explicito que o grupo emitido dentro de e (em vez de ).

Suggested change
Para produtos com CST 620 (combustiveis, etc.) o grupo emitido dentro de `<IBSCBS>` e `<gIBSCBSMono>` ao inves de `<gIBSCBS>`. Campos obrigatorios:
Para produtos com CST 620 (combustiveis, etc.), o grupo emitido dentro de `<IBSCBS>` e `<gIBSCBSMono>`, em vez de `<gIBSCBS>`. Campos obrigatorios:

Copilot uses AI. Check for mistakes.

| 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

Expand Down
11 changes: 11 additions & 0 deletions pynfe/entidades/notafiscal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,17 @@ class NotaFiscalProduto(Entidade):
ibscbs_p_cbs = Decimal() # pCBS
ibscbs_v_cbs = Decimal() # vCBS

# gIBSCBSMono - Tributacao monofasica (CST 620)
# Emitted as <gIBSCBSMono> instead of <gIBSCBS> 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
Comment on lines +1097 to +1101
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Os novos campos ibscbs__mono foram adicionados na entidade, mas nao ha integracao com a acumulacao de totais da nota (ex: NotaFiscal.adicionar_produto_servico soma apenas ibscbs_vbc/ibscbs_v_ibs/ibscbs_v_cbs). Com CST 620 isso faz com que valores vIBSMono/vCBSMono nao sejam refletidos nos totais/IBSCBSTot, podendo gerar XML inconsistente (itens com valores monofasicos, mas totais zerados/ausentes). Sugestao: definir e acumular totais especificos (ex: totais_ibs_mono/totais_cbs_mono e/ou gMono) e/ou deixar explicitamente documentado/validado que totais monofasicos serao omitidos ate implementar gMono.

Copilot uses AI. Check for mistakes.

# IS (Imposto Seletivo) - Group UB-IS
is_cst_selec = str() # CSTSelec (2-digit)
is_c_class_trib = str() # cClassTribIS 6-digit
Expand Down
103 changes: 73 additions & 30 deletions pynfe/processamento/serializacao.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <gIBSCBS>
_IBSCBS_CST_TRIBUTADOS = ("000", "010", "200", "400", "510", "600", "800", "810", "900")

# CSTs that use the monophasic tax regime and emit <gIBSCBSMono> instead of <gIBSCBS>
# (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
Expand All @@ -1340,45 +1345,83 @@ def _serializar_imposto_ibscbs(
# self._serializar_is(produto_servico, tag_raiz)

def _serializar_ibscbs(self, produto_servico, tag_raiz):
"""Serializa <IBSCBS> com gIBSCBS contendo gIBSUF, gIBSMun e gCBS."""
"""Serializa <IBSCBS>.

Para CSTs monofasicas (ex: 620) emite <gIBSCBSMono> com qBCMono, adRemIBS,
vIBSMono, adRemCBS, vCBSMono. Para demais CSTs tributados emite <gIBSCBS>
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)
Comment on lines +1360 to +1363
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Para CST 620, o item passa a serializar vIBSMono/vCBSMono, mas o codigo de totais (IBSCBSTot) e habilitado somente quando existem totais_vbc_ibscbs/totais_ibs/totais_cbs. Como os itens monofasicos nao alimentam esses totais hoje, uma NF composta apenas por CST 620 pode sair sem IBSCBSTot mesmo com valores monofasicos nao-zero. Sugestao: propagar/contabilizar os valores monofasicos em campos de totais apropriados e ajustar o gate (has_reforma) e/ou emitir gMono quando houver CST monofasico.

Copilot uses AI. Check for mistakes.

etree.SubElement(gibscbs, "vBC").text = "{:.2f}".format(produto_servico.ibscbs_vbc or 0)
def _serializar_gibscbs(self, produto_servico, ibscbs):
"""Serializa <gIBSCBS> 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 <gIBSCBSMono> para CSTs monofasicas (620).

Estrutura obrigatoria por NT 2025.002-RTC:
<gIBSCBSMono>
<qBCMono>TDec_1104v (4 casas)</qBCMono>
<adRemIBS>TDec_0302a10 (4 casas)</adRemIBS>
<vIBSMono>TDec_1302 (2 casas)</vIBSMono>
<adRemCBS>TDec_0302a10 (4 casas)</adRemCBS>
<vCBSMono>TDec_1302 (2 casas)</vCBSMono>
</gIBSCBSMono>
"""
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 <IS> (Imposto Seletivo) como filho direto de <imposto>.
Expand Down
156 changes: 156 additions & 0 deletions tests/test_nfe_serializacao_reforma_tributaria.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ------------------------------------------------------------------
Comment on lines +670 to +672
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Os blocos de comentario de secao agora ficam com numeracao inconsistente (apos inserir os testes de gIBSCBSMono, o bloco seguinte ainda diz "Test 10"). Isso dificulta manutencao/leitura do arquivo. Sugestao: renumerar as secoes ou trocar para titulos descritivos sem numeros.

Copilot uses AI. Check for mistakes.
def test_cst620_monofasica_emite_gibscbsmono(self):
"""CST 620 (tributacao monofasica) must emit <gIBSCBSMono> with
qBCMono, adRemIBS, vIBSMono, adRemCBS, vCBSMono — NOT <gIBSCBS>."""
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()

# <IBSCBS> 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")

# <gIBSCBSMono> is emitted
gibscbs_mono = xml.xpath("//ns:IBSCBS/ns:gIBSCBSMono", namespaces=self.ns)
self.assertEqual(len(gibscbs_mono), 1)

# <gIBSCBS> 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"
)
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Os novos testes para CST 620 validam o XML por item, mas nao fixam a expectativa para os totais () quando vIBSMono/vCBSMono sao nao-zero. Como a implementacao atual pode omitir IBSCBSTot nesses casos (por nao acumular totais monofasicos), vale adicionar uma assercao explicita (seja para garantir que IBSCBSTot nao e emitido por enquanto, seja para validar gMono/totais quando forem implementados) para evitar regressao silenciosa.

Suggested change
)
)
self.assertEqual(
xml.xpath("//ns:IBSCBSTot", namespaces=self.ns),
[],
"Enquanto os totais monofasicos nao forem acumulados, <IBSCBSTot> nao deve ser emitido.",
)

Copilot uses AI. Check for mistakes.

def test_cst000_nao_emite_gibscbsmono_regressao(self):
"""Regression test: CST 000 (regular taxation) must still emit <gIBSCBS>
and must NOT emit <gIBSCBSMono>."""
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()

# <gIBSCBS> is emitted (regular taxation path unchanged)
gibscbs = xml.xpath("//ns:IBSCBS/ns:gIBSCBS", namespaces=self.ns)
self.assertEqual(len(gibscbs), 1)

# <gIBSCBSMono> must NOT be emitted
gibscbs_mono = xml.xpath("//ns:IBSCBS/ns:gIBSCBSMono", namespaces=self.ns)
self.assertEqual(len(gibscbs_mono), 0)

# Verify <gIBSCBS> 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
# ------------------------------------------------------------------
Expand Down
Loading