From 61eddb955c4d7bdbf9d69d5edf7c6316e59e598a Mon Sep 17 00:00:00 2001 From: Felipe Correa Date: Fri, 13 Feb 2026 00:08:06 -0300 Subject: [PATCH 1/7] docs: Add source maps for large files and CLAUDE.md for LLM navigation Source maps provide section-by-section breakdowns with exact line ranges for all files over 200 lines, enabling LLMs to navigate directly to specific code sections instead of reading entire files. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 114 +++++++++++++++++ pynfe/entidades/evento_map.md | 38 ++++++ pynfe/entidades/manifesto_map.md | 42 +++++++ pynfe/entidades/notafiscal_map.md | 79 ++++++++++++ pynfe/processamento/autorizador_nfse_map.md | 36 ++++++ pynfe/processamento/comunicacao_map.md | 120 ++++++++++++++++++ pynfe/processamento/serializacao_map.md | 129 ++++++++++++++++++++ pynfe/utils/flags_map.md | 37 ++++++ pynfe/utils/utils_map.md | 26 ++++ pynfe/utils/webservices_map.md | 63 ++++++++++ 10 files changed, 684 insertions(+) create mode 100644 CLAUDE.md create mode 100644 pynfe/entidades/evento_map.md create mode 100644 pynfe/entidades/manifesto_map.md create mode 100644 pynfe/entidades/notafiscal_map.md create mode 100644 pynfe/processamento/autorizador_nfse_map.md create mode 100644 pynfe/processamento/comunicacao_map.md create mode 100644 pynfe/processamento/serializacao_map.md create mode 100644 pynfe/utils/flags_map.md create mode 100644 pynfe/utils/utils_map.md create mode 100644 pynfe/utils/webservices_map.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..bc1f2bce --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,114 @@ +# CLAUDE.md - PyNFe + +Brazilian electronic fiscal document library (NF-e, NFC-e, NFS-e, MDF-e, CT-e) for SEFAZ webservice communication. + +## Source Map Navigation (MANDATORY) + +**Before reading any large file (>200 lines), you MUST first read its `{filename}_map.md` file** in the same directory. The source map contains: +- Section-by-section breakdown with exact line ranges +- Class/method index with purpose descriptions +- Field group documentation + +This allows you to navigate directly to the specific line-window you need instead of reading the entire file. + +### Available Source Maps + +| Source Map | File | Lines | Description | +|------------|------|-------|-------------| +| `pynfe/processamento/serializacao_map.md` | `serializacao.py` | 2630 | XML serialization (NF-e, MDF-e, QR codes) | +| `pynfe/processamento/comunicacao_map.md` | `comunicacao.py` | 1348 | SEFAZ webservice communication | +| `pynfe/processamento/autorizador_nfse_map.md` | `autorizador_nfse.py` | 538 | NFS-e authorization (Betha/Ginfes) | +| `pynfe/entidades/notafiscal_map.md` | `notafiscal.py` | 1253 | Invoice entities and tax fields | +| `pynfe/entidades/manifesto_map.md` | `manifesto.py` | 447 | MDF-e manifest entities | +| `pynfe/entidades/evento_map.md` | `evento.py` | 237 | Event entities (cancel, correction, etc.) | +| `pynfe/utils/flags_map.md` | `flags.py` | 645 | Constants, namespaces, tax codes | +| `pynfe/utils/webservices_map.md` | `webservices.py` | 572 | SEFAZ endpoint URLs by state | +| `pynfe/utils/utils_map.md` | `__init__.py` | 253 | Utility functions (municipality lookup, signing) | + +### How to Use Source Maps + +1. **Read the `_map.md` file first** to understand the file structure +2. **Identify the line range** you need from the map tables +3. **Read only that section** using `offset` and `limit` parameters +4. Example: To understand ICMS CST 60 serialization, read `serializacao_map.md`, find it's at lines 747-770, then read `serializacao.py` with `offset=747, limit=25` + +## Project Structure + +``` +pynfe/ +├── entidades/ # Domain entities (data models) +│ ├── base.py # Base entity class with kwargs init +│ ├── certificado.py # A1 certificate handling +│ ├── cliente.py # Customer entity +│ ├── emitente.py # Issuer entity +│ ├── evento.py # Event entities (cancel, correction, MDF-e events) +│ ├── manifesto.py # MDF-e manifest entity +│ ├── notafiscal.py # NF-e/NFC-e invoice entity + products + taxes +│ ├── produto.py # Product entity (standalone) +│ ├── servico.py # Service entity (NFS-e) +│ └── transportadora.py # Carrier entity +├── processamento/ # Core processing logic +│ ├── assinatura.py # XML digital signing with A1 certificates +│ ├── autorizador_nfse.py # NFS-e serialization (Betha/Ginfes PyXB bindings) +│ ├── comunicacao.py # SEFAZ SOAP webservice communication +│ ├── serializacao.py # XML serialization (entities → SEFAZ XML) +│ └── validacao.py # XML schema validation +├── utils/ # Utilities +│ ├── __init__.py # Municipality lookup, XML signing helpers +│ ├── flags.py # Constants, namespaces, tax code enumerations +│ ├── webservices.py # SEFAZ endpoint URLs by state/environment +│ ├── bar_code_128.py # Code 128 barcode generation for DANFE +│ ├── xml_writer.py # XML element writing helpers +│ └── nfse/ # NFS-e provider-specific PyXB bindings (GENERATED - do not edit) +│ ├── betha/ # Betha provider bindings (13,941+ lines) +│ └── ginfes/ # Ginfes provider bindings (8,028+ lines) +├── data/ # Reference data files +│ ├── IBPT/ # Tax tables by state (CSV) +│ ├── ISSQN/ # Service tax classification +│ ├── MunIBGE/ # Municipality IBGE codes by UF +│ └── XSDs/ # XML Schema definitions (NF-e, NFC-e, NFS-e, MDF-e, CT-e) +tests/ # Test suite (37 test files) +``` + +## Key Concepts + +- **NF-e** (modelo 55): Standard electronic invoice +- **NFC-e** (modelo 65): Consumer electronic invoice (retail) +- **NFS-e**: Municipal service invoice (Betha/Ginfes providers) +- **MDF-e** (modelo 58): Transport manifest +- **CT-e**: Transport knowledge document (partial support) +- **SEFAZ**: State tax authority webservices +- **Homologacao**: Test environment (`_ambiente=2`) +- **SVRS/SVAN**: Virtual SEFAZ environments for states without own webservices + +## Commands + +```bash +# Run tests +pytest tests/ + +# Run specific test +pytest tests/test_nfe_serializacao_geral.py + +# Lint +ruff check pynfe/ + +# Format +ruff format pynfe/ +``` + +## Dependencies + +- `lxml` — XML processing +- `signxml` — XML digital signatures +- `cryptography` / `pyopenssl` — Certificate handling +- `requests` — HTTP communication with SEFAZ +- `suds-community` — SOAP client (NFS-e only) +- `PyXB-X` — XML Schema bindings (NFS-e only) + +## Important Notes + +- Files under `pynfe/utils/nfse/` are **auto-generated** PyXB bindings — do not edit manually +- The `pynfe/data/` directory contains reference data files that should not be modified casually +- Tax code serialization follows strict SEFAZ XML schema ordering — field order matters +- Each Brazilian state has its own SEFAZ endpoint configuration in `webservices.py` diff --git a/pynfe/entidades/evento_map.md b/pynfe/entidades/evento_map.md new file mode 100644 index 00000000..844e8bb4 --- /dev/null +++ b/pynfe/entidades/evento_map.md @@ -0,0 +1,38 @@ +# Source Map: `evento.py` (237 lines) + +Event entity models for NF-e and MDF-e lifecycle operations. + +## Classes Overview + +| Class | Lines | Purpose | +|-------|-------|---------| +| `Evento` | 11-46 | Base event class | +| `EventoCancelarNota` | 49-60 | NF-e cancellation (tp_evento=110111) | +| `EventoCartaCorrecao` | 63-90 | NF-e correction letter (tp_evento=110110) | +| `EventoManifestacaoDest` | 93-130 | Recipient manifestation (confirm/unknown/not performed/unaware) | +| `EventoOperacaoNaoRealizada` | ~132-145 | Operation not performed | +| `EventoCancelarMDFe` | ~147-160 | MDF-e cancellation | +| `EventoEncerrarMDFe` | ~162-180 | MDF-e closure | +| `EventoIncluirCondutorMDFe` | ~182-195 | Add driver to MDF-e | +| `EventoIncluirDFeMDFe` | ~197-215 | Add DF-e to MDF-e | +| `EventoPagamentoMDFe` | ~217-237 | MDF-e operation payment | + +## `Evento` (base) — Lines 11-46 + +Fields: `id`, `orgao`, `cnpj`, `chave`, `data_emissao`, `uf`, `tp_evento`, `n_seq_evento`, `descricao`. + +Property `identificador` builds: `"ID" + tp_evento + chave + n_seq_evento(2 digits)`. + +## Key Event Types + +| Class | tp_evento | descricao | +|-------|-----------|-----------| +| `EventoCancelarNota` | 110111 | "Cancelamento" | +| `EventoCartaCorrecao` | 110110 | "Carta de Correcao" | +| `EventoManifestacaoDest` | 210200-210240 | Various manifestation types | +| `EventoOperacaoNaoRealizada` | 110112 | "Operacao nao Realizada" | +| `EventoCancelarMDFe` | 110111 | "Cancelamento" (MDF-e) | +| `EventoEncerrarMDFe` | 110112 | "Encerramento" | +| `EventoIncluirCondutorMDFe` | 110114 | "Inclusão Condutor" | +| `EventoIncluirDFeMDFe` | 110115 | "Inclusao DF-e" | +| `EventoPagamentoMDFe` | 110116 | "Pagamento Operacao MDF-e" | diff --git a/pynfe/entidades/manifesto_map.md b/pynfe/entidades/manifesto_map.md new file mode 100644 index 00000000..0c07f459 --- /dev/null +++ b/pynfe/entidades/manifesto_map.md @@ -0,0 +1,42 @@ +# Source Map: `manifesto.py` (447 lines) + +Entity models for MDF-e (Manifesto de Documentos Fiscais Eletrônicos). + +## Classes Overview + +| Class | Lines | Purpose | +|-------|-------|---------| +| `Manifesto` | 12-200 | Main MDF-e entity | +| `ManifestoEmitente` | ~203-230 | MDF-e issuer | +| `ManifestoMunicipioCarrega` | ~232-240 | Loading municipality | +| `ManifestoPercurso` | ~242-248 | Route waypoint (UF) | +| `ManifestoModalRodoviario` | ~250-290 | Road transport modal | +| `ManifestoCIOT` | ~292-300 | CIOT (toll system) | +| `ManifestoPedagio` | ~302-315 | Toll information | +| `ManifestoContratante` | ~317-330 | Contractor info | +| `ManifestoVeiculoTracao` | ~332-360 | Traction vehicle | +| `ManifestoVeiculoReboque` | ~362-385 | Trailer vehicle | +| `ManifestoProprietario` | ~387-400 | Vehicle owner | +| `ManifestoCondutor` | ~402-410 | Driver | +| `ManifestoDocumentos` | ~412-425 | Linked documents (NF-e/CT-e) | +| `ManifestoSeguradora` | ~427-440 | Insurance | +| `ManifestoTotais` | ~442-447 | Totals (weight, cargo value) | + +## `Manifesto` — Lines 12-200 + +### Field Groups +| Section | Lines | Fields | +|---------|-------|--------| +| Identity | 12-68 | uf, tipo_emitente, tipo_transportador, modelo (58), serie, numero_mdfe, codigo_numerico_aleatorio, modal, data_emissao, forma_emissao, processo_emissao, UFIni, UFFim | +| Read-only | 65-72 | digest_value, protocolo, data | +| Relationships | 74-110 | municipio_carrega, percurso, dhIniViagem, emitente, modal_rodoviario, documentos, seguradora, produto, totais, lacres, responsavel_tecnico | +| Additional info | ~112-120 | informacoes_adicionais_interesse_fisco, informacoes_complementares_interesse_contribuinte | + +### Methods +| Method | Lines | Purpose | +|--------|-------|---------| +| `__init__()` | ~122-140 | Initialize all list fields | +| `adicionar_*()` | ~142-185 | Add municipio_carrega, percurso, emitente, modal_rodoviario, documento, seguradora, produto, totais, lacre, responsavel_tecnico | +| `_codigo_numerico_aleatorio()` | ~187-190 | Generate random 8-digit code | +| `_dv_codigo_numerico()` | ~192-200 | Calculate check digit (mod 11) | +| `identificador_unico` (property) | ~200+ | Build 44-char MDF-e access key | diff --git a/pynfe/entidades/notafiscal_map.md b/pynfe/entidades/notafiscal_map.md new file mode 100644 index 00000000..5eae0d75 --- /dev/null +++ b/pynfe/entidades/notafiscal_map.md @@ -0,0 +1,79 @@ +# Source Map: `notafiscal.py` (1253 lines) + +Entity models for NF-e/NFC-e invoices and related objects (products, taxes, payments, transport, etc.). + +## Classes Overview + +| Class | Lines | Purpose | +|-------|-------|---------| +| `NotaFiscal` | 14-572 | Main invoice entity | +| `NotaFiscalReferenciada` | 575-603 | Referenced invoice | +| `NotaFiscalProduto` | 606-1019 | Product/service item with all tax fields | +| `NotaFiscalDeclaracaoImportacao` | 1022-1067 | Import declaration | +| `NotaFiscalDeclaracaoImportacaoAdicao` | 1070-1082 | Import declaration addition | +| `NotaFiscalTransporteVolume` | 1084-1113 | Transport volume | +| `NotaFiscalTransporteVolumeLacre` | 1116-1118 | Volume seal | +| `NotaFiscalCobrancaDuplicata` | 1121-1129 | Billing duplicate | +| `NotaFiscalObservacaoContribuinte` | 1132-1137 | Taxpayer note | +| `NotaFiscalProcessoReferenciado` | 1140-1150 | Referenced process | +| `NotaFiscalEntregaRetirada` | 1153-1190 | Delivery/withdrawal address | +| `NotaFiscalServico` | 1192-1220 | NFS-e service invoice | +| `NotaFiscalResponsavelTecnico` | 1223-1229 | Technical responsible (NT2018/003) | +| `AutorizadosBaixarXML` | 1232-1233 | Authorized XML downloaders | +| `NotaFiscalPagamentos` | 1236-1253 | Payment details | + +--- + +## `NotaFiscal` — Lines 14-572 + +### Field Groups +| Section | Lines | Fields | +|---------|-------|--------| +| Deprecated fields | 16-23 | `tipo_pagamento` deprecated | +| Invoice identity | 25-127 | status, codigo_numerico, modelo, serie, numero_nf, data_emissao, natureza_operacao, tipo_documento, processo_emissao, tipo_impressao_danfe, forma_emissao, finalidade_emissao, cliente_final, indicador_presencial, indicador_intermediador, indicador_destino, uf, municipio | +| Read-only fields | 128-144 | digest_value, valor_total_nota, valor_icms_nota, valor_icms_st_nota, protocolo, data | +| Relationships | 146-173 | notas_fiscais_referenciadas, emitente, destinatario_remetente, entrega, retirada, autorizados_baixar_xml, produtos_e_servicos | +| ICMS totals | 175-303 | totais_icms_base_calculo, totais_icms_total, totais_icms_desonerado, totais_icms_st_*, totais_icms_total_*, totais_fcp_*, totais_icms_inter_*, totais_icms_*_mono_* | +| Transport | 305-361 | transporte_modalidade_frete, transporte_transportadora, transporte_retencao_icms_*, transporte_veiculo_*, transporte_reboque_*, transporte_volumes | +| Billing | 363-378 | fatura_numero, fatura_valor_original, fatura_valor_desconto, fatura_valor_liquido, duplicatas | +| Additional info | 380-397 | informacoes_adicionais_*, observacoes_contribuinte, processos_referenciados, pagamentos, valor_troco | + +### Methods +| Method | Lines | Purpose | +|--------|-------|---------| +| `__init__()` | 399-410 | Initialize all list fields | +| `adicionar_pagamento()` | 415-419 | Add payment | +| `adicionar_autorizados_baixar_xml()` | 421-424 | Add authorized XML downloader | +| `adicionar_nota_fiscal_referenciada()` | 426-430 | Add referenced invoice | +| `adicionar_produto_servico()` | 432-484 | **Add product** — also accumulates all ICMS/tax totals | +| `adicionar_transporte_volume()` | 486-490 | Add transport volume | +| `adicionar_duplicata()` | 492-496 | Add billing duplicate | +| `adicionar_observacao_contribuinte()` | 498-502 | Add taxpayer note | +| `adicionar_processo_referenciado()` | 504-508 | Add referenced process | +| `adicionar_responsavel_tecnico()` | 510-514 | Add technical responsible | +| `_codigo_numerico_aleatorio()` | 516-519 | Generate random 8-digit code | +| `_dv_codigo_numerico()` | 521-543 | Calculate check digit (mod 11) | +| `identificador_unico` (property) | 545-572 | Build 44-char NF-e access key | + +--- + +## `NotaFiscalProduto` — Lines 606-1019 + +### Field Groups +| Section | Lines | Fields | +|---------|-------|--------| +| Product data | 612-686 | codigo, descricao, ean, ncm, cfop, unidade_comercial, quantidade_comercial, valor_unitario_comercial, unidade_tributavel, cbenef, quantidade_tributavel, valor_unitario_tributavel, ean_tributavel, total_frete, total_seguro, desconto, outras_despesas_acessorias, valor_total_bruto, numero_pedido, numero_item | +| Fuel data | 688-733 | cProdANP, descANP, pGLP, pGNn, pGNi, vPart, UFCons, comb_codif, comb_q_temp, comb_n_bico, comb_n_bomba, comb_n_tanque, comb_v_enc_ini, comb_v_enc_fin, comb_p_bio | +| ICMS fields | 735-826 | icms_modalidade, icms_origem, icms_modalidade_determinacao_bc, icms_percentual_reducao_bc, icms_valor_base_calculo, icms_aliquota, icms_valor, icms_desonerado, icms_motivo_desoneracao, icms_st_*, fcp_*, icms_inter_*, icms_st_ret_*, icms_*_mono_* | +| IPI fields | 828-877 | ipi_situacao_tributaria, ipi_classe_enquadramento, ipi_codigo_enquadramento, ipi_valor_base_calculo, ipi_aliquota, ipi_valor_ipi, pdevol, ipi_valor_ipi_dev | +| PIS fields | 879-923 | pis_modalidade (via pis_situacao_tributaria), pis_valor_base_calculo, pis_aliquota_percentual, pis_aliquota_reais, pis_valor, pis_st_* | +| COFINS fields | 925-969 | cofins_modalidade, cofins_valor_base_calculo, cofins_aliquota_percentual, cofins_aliquota_reais, cofins_valor, cofins_st_* | +| ISSQN fields | 971-990 | issqn_valor_base_calculo, issqn_aliquota, issqn_lista_servico, issqn_uf, issqn_municipio, issqn_valor | +| Import tax fields | 992-1003 | imposto_importacao_valor_base_calculo, imposto_importacao_valor_despesas_aduaneiras, imposto_importacao_valor_iof, imposto_importacao_valor | +| Additional info | 1005-1019 | informacoes_adicionais, declaracoes_importacao | + +--- + +## Supporting Entities (Lines 1022-1253) + +Small entity classes for nested data — see class table above for line ranges. All extend `Entidade` base class and use kwargs initialization. diff --git a/pynfe/processamento/autorizador_nfse_map.md b/pynfe/processamento/autorizador_nfse_map.md new file mode 100644 index 00000000..3144779e --- /dev/null +++ b/pynfe/processamento/autorizador_nfse_map.md @@ -0,0 +1,36 @@ +# Source Map: `autorizador_nfse.py` (538 lines) + +NFS-e authorization and serialization for Betha and Ginfes providers. + +## Classes Overview + +| Class | Lines | Purpose | +|-------|-------|---------| +| `InterfaceAutorizador` | 5-11 | Abstract interface (consultar_rps, cancelar) | +| `SerializacaoBetha` | 14-200 | Betha NFS-e XML generation using PyXB bindings | +| `SerializacaoGinfes` | ~202-538 | Ginfes NFS-e XML generation using PyXB bindings | + +## `SerializacaoBetha` — Lines 14-200 + +### Methods +| Method | Lines | Purpose | +|--------|-------|---------| +| `__init__()` | 15-18 | Import Betha NFS-e v2.02 schema | +| `gerar()` | 20-94 | Generate NFS-e XML (service, provider, customer, RPS) | +| `consultar_rps()` | 96-130 | Query NFS-e by RPS | +| `consultar_faixa()` | ~132-160 | Query NFS-e by range | +| `cancelar()` | ~162-200 | Cancel NFS-e | + +## `SerializacaoGinfes` — Lines ~202-538 + +### Methods +| Method | Lines | Purpose | +|--------|-------|---------| +| `__init__()` | ~202-210 | Import Ginfes schema modules | +| `cabecalho()` | ~212-220 | Generate XML header | +| `serializar_lote_assincrono()` | ~222-310 | Serialize async batch (RPS list) | +| `consultar_nfse()` | ~312-360 | Query NFS-e by provider/period | +| `consultar_lote()` | ~362-390 | Query batch by number | +| `consultar_rps()` | ~392-420 | Query NFS-e by RPS | +| `consultar_situacao_lote()` | ~422-450 | Query batch status | +| `cancelar()` / `cancelar_v2()` | ~452-538 | Cancel NFS-e (v2 and v3) | diff --git a/pynfe/processamento/comunicacao_map.md b/pynfe/processamento/comunicacao_map.md new file mode 100644 index 00000000..81288a73 --- /dev/null +++ b/pynfe/processamento/comunicacao_map.md @@ -0,0 +1,120 @@ +# Source Map: `comunicacao.py` (1348 lines) + +SEFAZ webservice communication for NF-e, NFC-e, NFS-e, MDF-e and CT-e. + +## Classes Overview + +| Class | Lines | Purpose | +|-------|-------|---------| +| `Comunicacao` | 31-47 | Abstract base class | +| `ComunicacaoSefaz` | 50-612 | NF-e/NFC-e SEFAZ communication | +| `ComunicacaoNfse` | 615-845 | NFS-e municipal communication (Betha/Ginfes) | +| `ComunicacaoMDFe` | 848-1119 | MDF-e SEFAZ communication | +| `ComunicacaoCTe` | 1121-1348 | CT-e SEFAZ communication | + +--- + +## `Comunicacao` (base) — Lines 31-47 + +Stores `uf`, `certificado`, `certificado_senha`, `_ambiente` (1=prod, 2=homolog). + +## `ComunicacaoSefaz` — Lines 50-612 + +### Public Methods +| Method | Lines | Purpose | +|--------|-------|---------| +| `autorizacao()` | 56-130 | Send NF-e for authorization (sync/async) | +| `consulta_recibo()` | 132-155 | Query batch processing result | +| `consulta_nota()` | 157-175 | Query NF-e status by access key | +| `consulta_distribuicao()` | 177-231 | DF-e distribution query (distNSU, consNSU, consChNFe) | +| `consulta_cadastro()` | 233-292 | Taxpayer registration query | +| `evento()` | 294-320 | Send NF-e events (cancel, correction letter) | +| `status_servico()` | 322-335 | Check SEFAZ server status | +| `inutilizacao()` | 337-410 | Number range invalidation | + +### Internal Methods +| Method | Lines | Purpose | +|--------|-------|---------| +| `_get_url_an()` | 412-424 | Get national environment URL (AN) | +| `_get_url()` | 426-551 | **URL resolver** — routes to correct SEFAZ by UF, model, environment, contingency | +| `_construir_xml_soap()` | 553-570 | Build SOAP XML envelope | +| `_post_header()` | 572-581 | HTTP headers (PE requires SOAPAction) | +| `_post()` | 583-612 | Execute HTTPS POST with A1 certificate | + +### URL Routing Detail (`_get_url`) +| Section | Lines | States | +|---------|-------|--------| +| Contingency SVRS | 429-462 | AM, BA, CE, GO, MA, MS, MT, PE, PR | +| Contingency SVAN | 463-476 | AC, AL, AP, DF, ES, MG, PA, PB, PI, RJ, RN, RO, RR, RS, SC, SE, SP, TO | +| Own webservices | 480-501 | PR, MS, SP, AM, CE, BA, GO, MG, MT, PE, RS | +| SVRS states | 504-533 | AC, AL, AP, DF, ES, PB, PI, RJ, RN, RO, RR, SC, SE, TO, PA | +| SVAN (MA only) | 536-548 | MA | + +--- + +## `ComunicacaoNfse` — Lines 615-845 + +### Public Methods +| Method | Lines | Purpose | +|--------|-------|---------| +| `autorizacao()` | 635-644 | Generate NFS-e (Betha only) | +| `enviar_lote()` | 646-655 | Send batch (Ginfes only) | +| `consultar()` | 657-666 | Query NFS-e (Ginfes only) | +| `consultar_rps()` | 668-678 | Query by RPS (Betha + Ginfes) | +| `consultar_faixa()` | 680-687 | Query range (Betha only) | +| `consultar_lote()` | 689-698 | Query batch (Ginfes only) | +| `consultar_situacao_lote()` | 700-707 | Query batch status (Ginfes only) | +| `cancelar()` | 709-722 | Cancel NFS-e (Betha + Ginfes) | + +### Internal Methods +| Method | Lines | Purpose | +|--------|-------|---------| +| `_cabecalho()` / `_cabecalho2()` | 724-762 | WSDL request header XML | +| `_cabecalho_ginfes()` | 764-768 | Ginfes-specific header via XSD | +| `_get_url()` | 770-780 | URL resolver for NFS-e | +| `_post()` | 782-804 | HTTP (no cert) WSDL communication | +| `_post_https()` | 806-845 | HTTPS (with cert) WSDL communication | + +--- + +## `ComunicacaoMDFe` — Lines 848-1119 + +### Public Methods +| Method | Lines | Purpose | +|--------|-------|---------| +| `autorizacao()` | 874-956 | Send MDF-e for authorization (sync/async) | +| `status_servico()` | 958-965 | Check MDF-e server status | +| `consulta()` | 967-976 | Query MDF-e by access key | +| `consulta_nao_encerrados()` | 978-990 | Query non-closed MDF-e | +| `consulta_recibo()` | 992-1000 | Query batch processing result | +| `evento()` | 1002-1017 | Send MDF-e events (cancel, close, add driver, add DF-e, payment) | + +### Internal Methods +| Method | Lines | Purpose | +|--------|-------|---------| +| `_construir_xml_soap()` | 1019-1044 | Build SOAP XML with header | +| `_post_header()` | 1046-1061 | HTTP headers | +| `_post()` | 1063-1099 | Execute HTTPS POST with cert | +| `_cabecalho_soap()` | 1101-1107 | SOAP header with cUF and versaoDados | +| `_get_url()` | 1109-1118 | URL resolver (SVRS only) | + +--- + +## `ComunicacaoCTe` — Lines 1121-1348 + +### Public Methods +| Method | Lines | Purpose | +|--------|-------|---------| +| `status_servico()` | 1136-1147 | Check CT-e server status | +| `consulta_distribuicao()` | 1149-1200 | CT-e distribution query | +| `consulta()` | 1202-1211 | Query CT-e by access key | + +### Internal Methods +| Method | Lines | Purpose | +|--------|-------|---------| +| `_get_url_an()` | 1213-1219 | National environment URL | +| `_cabecalho_soap()` | 1221-1227 | SOAP header | +| `_get_url()` | 1229-1281 | **URL resolver** — own (MT, MS, MG, PR, RS, SP), SVRS, SVSP | +| `_construir_xml_soap()` | 1283-1304 | Build SOAP XML envelope | +| `_post_header()` | 1306-1313 | HTTP headers | +| `_post()` | 1315-1348 | Execute HTTPS POST with cert | diff --git a/pynfe/processamento/serializacao_map.md b/pynfe/processamento/serializacao_map.md new file mode 100644 index 00000000..5c67a689 --- /dev/null +++ b/pynfe/processamento/serializacao_map.md @@ -0,0 +1,129 @@ +# Source Map: `serializacao.py` (2630 lines) + +XML serialization of NF-e, NFC-e, NFS-e and MDF-e documents into SEFAZ-compliant XML format. + +## Classes Overview + +| Class | Lines | Purpose | +|-------|-------|---------| +| `Serializacao` | 30-63 | Abstract base class (not instantiable directly) | +| `SerializacaoXML` | 66-1829 | Main NF-e/NFC-e XML serialization | +| `SerializacaoQrcode` | 1960-2064 | NFC-e QR Code generation | +| `SerializacaoNfse` | 2067-2133 | NFS-e serialization (Betha/Ginfes) | +| `SerializacaoQrcodeMDFe` | 2136-2159 | MDF-e QR Code generation | +| `SerializacaoMDFe` | 2162-2630 | MDF-e XML serialization | + +--- + +## `Serializacao` (base class) — Lines 30-63 + +Abstract base for all serializers. Stores `_fonte_dados`, `_ambiente` (1=prod, 2=homolog), `_contingencia`, `_so_cpf`. + +## `SerializacaoXML` — Lines 66-1829 + +### Exported Methods +| Method | Lines | Purpose | +|--------|-------|---------| +| `exportar()` | 71-97 | Main entry: exports NF-e XML from data source | +| `serializar_evento()` | 1831-1863 | Serializes NF-e events (cancellation, correction letter) | +| `serializar_evento_mdfe()` | 1865-1957 | Serializes MDF-e events (cancel, close, add driver, add DF-e, payment) | + +### Internal Serializers (NF-e) +| Method | Lines | Purpose | +|--------|-------|---------| +| `_serializar_emitente()` | 105-146 | Issuer data (CNPJ/CPF, address, IE, CRT) | +| `_serializar_cliente()` | 148-200 | Customer/destination data (document, address, IE indicator) | +| `_serializar_transportadora()` | 202-228 | Carrier data | +| `_serializar_entrega_retirada()` | 230-255 | Delivery/withdrawal address | +| `_serializar_autorizados_baixar_xml()` | 257-270 | Authorized XML downloaders | +| `_serializar_produto_servico()` | 272-459 | **Product/service item** (prod data, fuel, taxes dispatch) | +| `_serializar_responsavel_tecnico()` | 1347-1359 | Technical responsible (NT2018/003) | +| `_serializar_nota_fiscal()` | 1423-1829 | **Main NF-e assembly** (ide, emit, dest, items, totals, transport, billing, payment, additional info) | + +### Tax Serializers (within `SerializacaoXML`) +| Method | Lines | Purpose | +|--------|-------|---------| +| `_serializar_imposto_icms()` | 461-1078 | **ICMS tax** — all CST/CSOSN modalities | +| `_serializar_imposto_ipi()` | 1080-1118 | IPI tax | +| `_serializar_imposto_pis()` | 1120-1192 | PIS tax | +| `_serializar_imposto_cofins()` | 1194-1269 | COFINS tax | +| `_serializar_imposto_importacao()` | 1271-1293 | Import tax (II) | +| `_serializar_declaracao_importacao()` | 1295-1345 | Import declaration (DI) | + +### ICMS Modalities Detail (within `_serializar_imposto_icms`) +| CST/CSOSN | Lines | Description | +|-----------|-------|-------------| +| 00 | 465-489 | Fully taxed | +| 02 | 491-504 | Monophasic on fuels | +| 10 | 507-568 | Taxed + ICMS ST | +| 15 | 571-600 | Monophasic + retention on fuels | +| 20 | 603-642 | Reduced base | +| 30 | 645-685 | Exempt + ICMS ST | +| 40/41/50 | 688-699 | Exempt / Not taxed / Suspended | +| 51 | 702-719 | Deferral | +| 53 | 722-744 | Monophasic deferred on fuels | +| 60/ST | 747-770 | Previously charged by ST | +| 61 | 773-785 | Monophasic previously charged on fuels | +| 70 | 788-855 | Reduced base + ICMS ST | +| 90 | 858-930 | Other | +| 101 | 935-944 | Simples Nacional with credit | +| 102/103/300/400 | 951-954 | Simples Nacional without credit | +| 201/202/203 | 959-1004 | Simples Nacional + ICMS ST | +| 500 | 1007-1010 | SN previously charged by ST | +| 900 | 1013-1075 | SN Other | + +### Payment Serializers +| Method | Lines | Purpose | +|--------|-------|---------| +| `_serializar_pagamentos_antigo_deprecado()` | 1361-1390 | Legacy payment (deprecated) | +| `_serializar_pagamentos()` | 1392-1421 | Current payment serialization | + +### NF-e Assembly Detail (`_serializar_nota_fiscal`) +| Section | Lines | XML tags | +|---------|-------|----------| +| IDE (identification) | 1436-1499 | `` cUF, cNF, natOp, mod, serie, nNF, dhEmi, tpNF, etc. | +| Referenced NF-e | 1501-1537 | `` refNFe, refNF, refNFP, refCTe | +| Contingency | 1539-1547 | dhCont, xJust | +| Emitter | 1549-1550 | `` | +| Customer | 1552-1564 | `` | +| Withdrawal/Delivery | 1566-1583 | ``, `` | +| Authorized XML | 1586-1587 | `` | +| Items | 1589-1596 | `` with nItem | +| Totals | 1598-1691 | `` all tax totals | +| Transport | 1693-1747 | `` modFrete, carrier, vehicle, volumes | +| Billing | 1748-1774 | `` fat, dup | +| Payment | 1776-1801 | `` detPag | +| Additional info | 1803-1816 | `` | +| Technical responsible | 1818-1824 | `` | + +--- + +## `SerializacaoQrcode` — Lines 1960-2064 + +Generates NFC-e QR Code URL. Handles online/offline modes and state-specific URL patterns (SP, BA, MG, etc.). + +## `SerializacaoNfse` — Lines 2067-2133 + +Delegates to Betha or Ginfes serializers. Methods: `gerar`, `gerar_lote`, `consultar_nfse`, `consultar_lote`, `consultar_rps`, `consultar_situacao_lote`, `cancelar`. + +## `SerializacaoQrcodeMDFe` — Lines 2136-2159 + +Generates MDF-e QR Code URL using SVRS endpoint. + +## `SerializacaoMDFe` — Lines 2162-2630 + +### Methods +| Method | Lines | Purpose | +|--------|-------|---------| +| `exportar()` | 2167-2193 | Main entry: exports MDF-e XML | +| `_serializar_emitente()` | 2201-2233 | MDF-e issuer (CPF/CNPJ, IE, address) | +| `_serializar_municipio_carrega()` | 2235-2245 | Loading municipality | +| `_serializar_percurso()` | 2247-2254 | Route (UF waypoints) | +| `_serializar_modal_rodoviario()` | 2256-2415 | **Road modal** (ANTT, CIOT, tolls, contractors, traction vehicle, trailer) | +| `_serializar_documentos()` | 2417-2441 | Linked documents (NF-e or CT-e) | +| `_serializar_seguradora()` | 2443-2463 | Insurance info | +| `_serializar_produto()` | 2465-2475 | Predominant product | +| `_serializar_totais()` | 2477-2497 | Totals (weight, qty) | +| `_serializar_lacres()` | 2499-2506 | Seals | +| `_serializar_responsavel_tecnico()` | 2508-2520 | Technical responsible | +| `_serializar_manifesto()` | 2522-2629 | **Main MDF-e assembly** (ide, emit, modal, doc, seg, prod, tot, lacres, infAdic, infRespTec) | diff --git a/pynfe/utils/flags_map.md b/pynfe/utils/flags_map.md new file mode 100644 index 00000000..a6daa2f3 --- /dev/null +++ b/pynfe/utils/flags_map.md @@ -0,0 +1,37 @@ +# Source Map: `flags.py` (645 lines) + +Constants, namespaces, tax code enumerations, and state codes used throughout PyNFe. + +## Sections + +| Section | Lines | Purpose | +|---------|-------|---------| +| Namespaces | 1-24 | XML namespaces for NF-e, NFS-e, MDF-e, CT-e, SOAP, signatures | +| XSD paths | 26-39 | Paths to XML Schema files | +| ICMS types | 42-84 | `ICMS_TIPOS_TRIBUTACAO` — all CST/CSOSN codes | +| ICMS origins | 86-123 | `ICMS_ORIGENS` — product origin codes (0-8) | +| ICMS modalities | 125-140 | `ICMS_MODALIDADES` and `ICMS_ST_MODALIDADES` | +| ICMS exemption reasons | 142-161 | `ICMS_MOTIVO_DESONERACAO` | +| NF-e status | 163-182 | `NF_STATUS` and `MDFE_STATUS` tuples | +| NF-e enumerations | 184-246 | Document types, emission processes, DANFE types, payment forms, emission forms, emission purposes, referenced types, specific products, environments | +| IPI types | 248-268 | `IPI_TIPOS_TRIBUTACAO` and `IPI_TIPOS_CALCULO` | +| PIS types | 270-407 | `PIS_TIPOS_TRIBUTACAO` — all CST codes (01-99) | +| COFINS types | 409-546 | `COFINS_TIPOS_TRIBUTACAO` — all CST codes (01-99) | +| Freight modalities | 548-555 | `MODALIDADES_FRETE` (0-9) | +| Process origins | 557-563 | `ORIGENS_PROCESSO` | +| State codes | 565-597 | `CODIGOS_ESTADOS` — UF to IBGE code mapping | +| Card brands | 599-628 | `BANDEIRA_CARTAO` (01-99) | +| Payment methods | 630-645 | `FORMAS_PAGAMENTO` (01-99) | + +## Key Constants + +| Constant | Value | Used For | +|----------|-------|----------| +| `NAMESPACE_NFE` | `http://www.portalfiscal.inf.br/nfe` | NF-e XML | +| `NAMESPACE_MDFE` | `http://www.portalfiscal.inf.br/mdfe` | MDF-e XML | +| `NAMESPACE_CTE` | `http://www.portalfiscal.inf.br/cte` | CT-e XML | +| `VERSAO_PADRAO` | `4.00` | NF-e version | +| `VERSAO_MDFE` | `3.00` | MDF-e version | +| `VERSAO_CTE` | `3.00` | CT-e version | +| `VERSAO_QRCODE` | `2` | QR Code version | +| `CODIGO_BRASIL` | `1058` | Country code for Brazil | diff --git a/pynfe/utils/utils_map.md b/pynfe/utils/utils_map.md new file mode 100644 index 00000000..a4ca2e61 --- /dev/null +++ b/pynfe/utils/utils_map.md @@ -0,0 +1,26 @@ +# Source Map: `pynfe/utils/__init__.py` (253 lines) + +Utility functions for municipality/country lookups, XML signing, and helpers. + +## Functions + +| Function | Lines | Purpose | +|----------|-------|---------| +| `so_numeros()` | 22-29 | Extract only digits from string | +| `obter_pais_por_codigo()` | 33-41 | Get country name by code (default: 1058=Brasil) | +| `normalizar_municipio()` | 64-68 | Normalize municipality name (remove accents, uppercase) | +| `carregar_arquivo_municipios()` | 72-98 | Load IBGE municipality file for a given UF | +| `carregar_arquivo_pais()` | ~100-120 | Load country code file | +| `obter_municipio_por_codigo()` | ~122-140 | Get municipality name by IBGE code + UF | +| `obter_codigo_por_municipio()` | ~142-170 | Get IBGE code by municipality name + UF | +| `obter_municipio_e_codigo()` | ~172-190 | Get both municipality name and code | +| `assinar_com_a1()` | ~192-230 | Sign XML using A1 digital certificate | +| `validar_xml()` | ~232-253 | Validate XML against XSD schema | + +## Constants + +| Constant | Lines | Purpose | +|----------|-------|---------| +| `CAMINHO_DATA` | 44 | Path to `pynfe/data/` directory | +| `CAMINHO_MUNICIPIOS` | 45 | Path to `pynfe/data/MunIBGE/` | +| `CARACTERS_ACENTUADOS` | 46-60 | Accent character translation table | diff --git a/pynfe/utils/webservices_map.md b/pynfe/utils/webservices_map.md new file mode 100644 index 00000000..5ab7bc9e --- /dev/null +++ b/pynfe/utils/webservices_map.md @@ -0,0 +1,63 @@ +# Source Map: `webservices.py` (572 lines) + +SEFAZ webservice endpoint URLs organized by document type, state, and environment. + +## Sections + +| Section | Lines | Variable | Purpose | +|---------|-------|----------|---------| +| NFC-e endpoints | 8-295 | `NFCE` | NFC-e webservice URLs and QR Code URLs by state | +| NF-e endpoints | 297-471 | `NFE` | NF-e webservice URLs by state | +| NFS-e endpoints | 473-499 | `NFSE` | NFS-e URLs (Betha, Ginfes) | +| MDF-e endpoints | 501-516 | `MDFE` | MDF-e URLs (SVRS only) | +| CT-e endpoints | 518-572 | `CTE` | CT-e URLs by state | + +## URL Structure + +Each state/virtual environment entry contains: +- `STATUS` — Service status check endpoint +- `AUTORIZACAO` — Authorization endpoint +- `RECIBO` — Receipt query endpoint +- `CHAVE` — Access key query endpoint +- `INUTILIZACAO` — Number invalidation endpoint +- `EVENTOS` — Event reception endpoint +- `CADASTRO` — Registration query endpoint (some states) +- `HTTPS` — Production base URL prefix +- `HOMOLOGACAO` — Homologation base URL prefix +- `QR` — QR Code URL (NFC-e only) +- `URL` — Consultation URL (NFC-e only) + +## State/Virtual Environment Groups + +### NFC-e (`NFCE`) — Lines 8-295 +| Key | Lines | Description | +|-----|-------|-------------| +| Individual states | 9-278 | RO, AC, AM, RR, PA, AP, TO, MA, PI, CE, RN, PB, PE, AL, SE, BA, MG, ES, RJ, SP, PR, SC, RS, MS, MT, GO, DF | +| `SVRS` | 284-294 | Virtual SEFAZ RS (fallback for states without own NFC-e) | + +### NF-e (`NFE`) — Lines 297-471 +| Key | Lines | Description | +|-----|-------|-------------| +| `AN` | 302-309 | National environment (events, distribution) | +| Individual states | 310-428 | AM, MA, PE, BA, MG, SP, PR, RS, MS, MT, GO | +| `SVAN` | 430-440 | Virtual SEFAZ AN (MA for NF-e) | +| `SVRS` | 441-451 | Virtual SEFAZ RS (most states) | +| `SVC-AN` | 452-460 | Contingency AN | +| `SVC-RS` | 461-470 | Contingency RS | + +### NFS-e (`NFSE`) — Lines 473-499 +| Key | Lines | Description | +|-----|-------|-------------| +| `BETHA` | 476-486 | Betha provider (HTTP WSDL) | +| `GINFES` | 488-498 | Ginfes provider (HTTPS WSDL) | + +### MDF-e (`MDFE`) — Lines 501-516 +Only `SVRS` — single authorizer for all states. + +### CT-e (`CTE`) — Lines 518-572 +| Key | Lines | Description | +|-----|-------|-------------| +| `AN` | 519-523 | National environment (distribution) | +| Individual states | 524-558 | MT, MS, MG, PR, RS, SP | +| `SVRS` | 560-565 | Virtual SEFAZ RS | +| `SVSP` | 566-571 | Virtual SEFAZ SP (AP, PE, RR) | From 7c830b1acc8070dc5bcef18f7185c22c3a856afe Mon Sep 17 00:00:00 2001 From: Felipe Correa Date: Fri, 13 Feb 2026 00:45:39 -0300 Subject: [PATCH 2/7] feat: Add Reforma Tributaria IBS/CBS/IS support (EC 132/2023) Add core XML infrastructure for Brazil's dual VAT system (IVA Dual): - CST enumerations for IBS, CBS and IS in flags.py - Product-level and invoice-level entity fields in notafiscal.py - XML serialization (impostoMisto group) in serializacao.py - Totals accumulation and vNF formula updated - 5 test cases covering all serialization scenarios - Documentation in docs/reforma_tributaria.md Co-Authored-By: Claude Opus 4.6 --- docs/reforma_tributaria.md | 242 +++++++ pynfe/entidades/notafiscal.py | 36 ++ pynfe/entidades/notafiscal_map.md | 38 +- pynfe/processamento/serializacao.py | 101 +++ pynfe/processamento/serializacao_map.md | 16 +- pynfe/utils/flags.py | 25 + pynfe/utils/flags_map.md | 13 +- ...est_nfe_serializacao_reforma_tributaria.py | 602 ++++++++++++++++++ 8 files changed, 1043 insertions(+), 30 deletions(-) create mode 100644 docs/reforma_tributaria.md create mode 100644 tests/test_nfe_serializacao_reforma_tributaria.py diff --git a/docs/reforma_tributaria.md b/docs/reforma_tributaria.md new file mode 100644 index 00000000..c8f11ab2 --- /dev/null +++ b/docs/reforma_tributaria.md @@ -0,0 +1,242 @@ +# Reforma Tributaria - IBS/CBS/IS (EC 132/2023) + +Suporte ao IVA Dual brasileiro introduzido pela Emenda Constitucional 132/2023. + +## Contexto + +A Reforma Tributaria brasileira substitui gradualmente cinco tributos por tres novos impostos: + +| Imposto | Tipo | Substitui | Competencia | +|---------|------|-----------|-------------| +| **CBS** (Contribuicao sobre Bens e Servicos) | Federal | PIS, COFINS | Uniao | +| **IBS** (Imposto sobre Bens e Servicos) | Subnacional | ICMS, ISS | Estados + Municipios | +| **IS** (Imposto Seletivo) | Extrafiscal | IPI (parcial) | Uniao | + +### Cronograma de transicao + +- **2026**: Fase de testes (aliquotas reduzidas CBS 0.9%, IBS 0.1%) +- **2027-2028**: CBS integral, IBS em transicao +- **2029-2032**: Reducao gradual de ICMS/ISS +- **2033**: Extincao completa de ICMS/ISS + +Durante a transicao, os impostos legados (ICMS, PIS, COFINS) **coexistem** com os novos (IBS, CBS, IS) na mesma NF-e. + +## Escopo da implementacao + +A implementacao atual cobre a **infraestrutura XML basica**: + +- Campos de entidade (produto e nota fiscal) +- Serializacao XML (grupo `` contendo ``, ``, ``) +- Acumulacao de totais +- CSTs (Codigos de Situacao Tributaria) para IBS, CBS e IS + +**Nao inclui** (ainda): Split Payment, cashback, helpers de calculo da transicao, validacao XSD (schemas oficiais ainda nao publicados pela SEFAZ). + +## CSTs disponiveis + +### IBS e CBS (compartilham a mesma tabela) + +```python +from pynfe.utils.flags import IBS_CBS_TIPOS_TRIBUTACAO +``` + +| CST | Descricao | +|-----|-----------| +| 01 | Tributada Integralmente | +| 02 | Tributada com Reducao (Cesta Basica/Saude) | +| 03 | Isencao | +| 04 | Imunidade | +| 05 | Suspensao | +| 51 | Diferimento | +| 70 | Monofasica (Combustiveis) | + +### IS (Imposto Seletivo) + +```python +from pynfe.utils.flags import IS_TIPOS_TRIBUTACAO +``` + +| CST | Descricao | +|-----|-----------| +| 01 | Tributada Integralmente | +| 02 | Tributada com Reducao | +| 03 | Isencao | +| 04 | Imunidade | +| 05 | Suspensao | + +## Como usar + +### Exemplo basico: produto com CBS + IBS + IS + +```python +from decimal import Decimal + +nota_fiscal.adicionar_produto_servico( + # ... campos obrigatorios do produto (codigo, descricao, ncm, cfop, etc.) + codigo="001", + descricao="Produto exemplo", + ncm="99999999", + ean="SEM GTIN", + cfop="5102", + unidade_comercial="UN", + quantidade_comercial=Decimal("10"), + valor_unitario_comercial=Decimal("100.00"), + valor_total_bruto=Decimal("1000.00"), + unidade_tributavel="UN", + quantidade_tributavel=Decimal("10"), + valor_unitario_tributavel=Decimal("100.00"), + ean_tributavel="SEM GTIN", + ind_total=1, + + # ... impostos legados (ICMS, PIS, COFINS) continuam obrigatorios + icms_modalidade="00", + icms_origem=0, + icms_csosn="", + pis_modalidade="99", + cofins_modalidade="99", + pis_valor=Decimal("0.00"), + cofins_valor=Decimal("0.00"), + + # CBS - Contribuicao sobre Bens e Servicos (Federal) + cbs_situacao_tributaria="01", # CST 01 = Tributada integralmente + cbs_valor_base_calculo=Decimal("1000.00"), + cbs_aliquota=Decimal("8.8000"), # 4 casas decimais + cbs_valor=Decimal("88.00"), + + # IBS - Imposto sobre Bens e Servicos (Estadual/Municipal) + ibs_situacao_tributaria="01", + ibs_valor_base_calculo=Decimal("1000.00"), + ibs_aliquota=Decimal("17.7000"), + ibs_valor=Decimal("177.00"), + ibs_codigo_municipio_destino="4118402", # Codigo IBGE do municipio destino + + # IS - Imposto Seletivo (opcional, apenas para produtos sujeitos) + is_situacao_tributaria="01", + is_valor_base_calculo=Decimal("1000.00"), + is_aliquota=Decimal("1.0000"), + is_valor=Decimal("10.00"), +) +``` + +### Exemplo: produto isento (CBS + IBS com CST 03) + +```python +nota_fiscal.adicionar_produto_servico( + # ... campos do produto ... + + # CBS isenta - apenas o CST, sem valores + cbs_situacao_tributaria="03", + + # IBS isenta + ibs_situacao_tributaria="03", + + # IS nao se aplica (nao informar = nao serializado) +) +``` + +### Exemplo: sem reforma tributaria (compatibilidade retroativa) + +Se nenhum campo de IBS/CBS/IS for informado, o grupo `` **nao e emitido** no XML. Isso garante compatibilidade total com NF-e que nao precisam dos novos impostos. + +```python +# Produto sem campos de reforma tributaria = XML identico ao anterior +nota_fiscal.adicionar_produto_servico( + # ... apenas campos legados (ICMS, PIS, COFINS) ... +) +``` + +## Campos disponiveis + +### Campos do produto (`adicionar_produto_servico`) + +| Campo | Tipo | Descricao | +|-------|------|-----------| +| `cbs_situacao_tributaria` | `str` | CST da CBS (ex: "01", "03") | +| `cbs_valor_base_calculo` | `Decimal` | Base de calculo da CBS | +| `cbs_aliquota` | `Decimal` | Aliquota CBS (4 casas decimais) | +| `cbs_valor` | `Decimal` | Valor da CBS | +| `ibs_situacao_tributaria` | `str` | CST do IBS (ex: "01", "03") | +| `ibs_valor_base_calculo` | `Decimal` | Base de calculo do IBS | +| `ibs_aliquota` | `Decimal` | Aliquota IBS (4 casas decimais) | +| `ibs_valor` | `Decimal` | Valor do IBS | +| `ibs_codigo_municipio_destino` | `str` | Codigo IBGE do municipio destino | +| `is_situacao_tributaria` | `str` | CST do IS (ex: "01", "02") | +| `is_valor_base_calculo` | `Decimal` | Base de calculo do IS | +| `is_aliquota` | `Decimal` | Aliquota IS (4 casas decimais) | +| `is_valor` | `Decimal` | Valor do IS | + +### Totais da nota fiscal (acumulados automaticamente) + +| Campo | Descricao | +|-------|-----------| +| `totais_cbs` | Soma de `cbs_valor` de todos os produtos | +| `totais_ibs` | Soma de `ibs_valor` de todos os produtos | +| `totais_is` | Soma de `is_valor` de todos os produtos | + +Os totais sao acumulados automaticamente ao chamar `adicionar_produto_servico()`. Os valores de IBS, CBS e IS tambem sao somados ao `totais_icms_total_nota` (vNF), pois sao impostos "por fora". + +## Estrutura XML gerada + +O grupo `` e adicionado dentro de ``, apos `` e antes de ``: + +```xml + + ... + + ... + ... + ... + + + 01 + 1000.00 + 8.8000 + 88.00 + + + 01 + 1000.00 + 17.7000 + 177.00 + 4118402 + + + 01 + 1000.00 + 1.0000 + 10.00 + + + + +``` + +Os totais aparecem dentro de ``: + +```xml + + + + 1275.00 + 88.00 + 177.00 + 10.00 + + +``` + +## Regras de serializacao + +- **CSTs 01, 02, 51**: Serializam `vBC`, aliquota e valor (campos completos) +- **CSTs 03, 04, 05, 70**: Serializam apenas o `CST` (sem valores) +- **IS CSTs 01, 02**: Serializam campos completos; demais apenas `CST` +- **`cMunDest`** (IBS): Serializado apenas quando informado +- **Totais**: Emitidos apenas quando o valor e maior que zero +- **`impostoMisto`**: Omitido completamente se nenhum dos tres impostos for informado + +## Notas importantes + +- Os nomes das tags XML (`impostoMisto`, `CBS`, `IBS`, `IS`) sao baseados na especificacao preliminar. Podem ser renomeados quando a SEFAZ publicar a Nota Tecnica oficial. +- A validacao XSD e ignorada para os novos grupos, pois os schemas oficiais ainda nao existem. +- Durante a fase de transicao (2026-2032), os impostos legados e os novos coexistem no mesmo XML. +- O calculo dos valores (base de calculo, aliquota, valor) deve ser feito pela aplicacao consumidora. PyNFe apenas serializa os valores informados. diff --git a/pynfe/entidades/notafiscal.py b/pynfe/entidades/notafiscal.py index e458d6d5..c16cbfed 100644 --- a/pynfe/entidades/notafiscal.py +++ b/pynfe/entidades/notafiscal.py @@ -302,6 +302,11 @@ class NotaFiscal(Entidade): # - Valor total do ICMS monofásico sujeito a retenção totais_icms_v_icms_mono_reten = Decimal() + # Reforma Tributaria - Totais IVA Dual + totais_cbs = Decimal() + totais_ibs = Decimal() + totais_is = Decimal() + # Transporte # - Modalidade do Frete (obrigatorio - seleciona de lista) - MODALIDADES_FRETE # 0=Contratação do Frete por conta do Remetente (CIF); @@ -464,6 +469,11 @@ def adicionar_produto_servico(self, **kwargs): self.totais_icms_q_bc_mono_ret += obj.icms_q_bc_mono_ret self.totais_icms_v_icms_mono_ret += obj.icms_v_icms_mono_ret + # Reforma Tributaria - IVA Dual + self.totais_cbs += obj.cbs_valor + self.totais_ibs += obj.ibs_valor + self.totais_is += obj.is_valor + # TODO calcular impostos aproximados # self.totais_tributos_aproximado += obj.tributos @@ -477,6 +487,9 @@ def adicionar_produto_servico(self, **kwargs): + obj.imposto_importacao_valor + obj.ipi_valor_ipi + obj.ipi_valor_ipi_dev + + obj.ibs_valor + + obj.cbs_valor + + obj.is_valor - obj.desconto - obj.icms_desonerado ) @@ -1002,6 +1015,29 @@ class NotaFiscalProduto(Entidade): # - Valor imposto de importacao imposto_importacao_valor = Decimal() + # ============================================= + # Reforma Tributaria - IVA Dual (EC 132/2023) + # ============================================= + + # CBS (Contribuicao sobre Bens e Servicos) - Federal + cbs_situacao_tributaria = str() + cbs_valor_base_calculo = Decimal() + cbs_aliquota = Decimal() + cbs_valor = Decimal() + + # IBS (Imposto sobre Bens e Servicos) - Estadual/Municipal + ibs_situacao_tributaria = str() + ibs_valor_base_calculo = Decimal() + ibs_aliquota = Decimal() + ibs_valor = Decimal() + ibs_codigo_municipio_destino = str() # cMunDest - IBGE code + + # IS (Imposto Seletivo) - Federal + is_situacao_tributaria = str() + is_valor_base_calculo = Decimal() + is_aliquota = Decimal() + is_valor = Decimal() + # - Informacoes Adicionais # - Texto livre de informacoes adicionais informacoes_adicionais = str() diff --git a/pynfe/entidades/notafiscal_map.md b/pynfe/entidades/notafiscal_map.md index 5eae0d75..00701d24 100644 --- a/pynfe/entidades/notafiscal_map.md +++ b/pynfe/entidades/notafiscal_map.md @@ -1,4 +1,4 @@ -# Source Map: `notafiscal.py` (1253 lines) +# Source Map: `notafiscal.py` (1280 lines) Entity models for NF-e/NFC-e invoices and related objects (products, taxes, payments, transport, etc.). @@ -6,21 +6,21 @@ Entity models for NF-e/NFC-e invoices and related objects (products, taxes, paym | Class | Lines | Purpose | |-------|-------|---------| -| `NotaFiscal` | 14-572 | Main invoice entity | -| `NotaFiscalReferenciada` | 575-603 | Referenced invoice | -| `NotaFiscalProduto` | 606-1019 | Product/service item with all tax fields | -| `NotaFiscalDeclaracaoImportacao` | 1022-1067 | Import declaration | -| `NotaFiscalDeclaracaoImportacaoAdicao` | 1070-1082 | Import declaration addition | -| `NotaFiscalTransporteVolume` | 1084-1113 | Transport volume | -| `NotaFiscalTransporteVolumeLacre` | 1116-1118 | Volume seal | -| `NotaFiscalCobrancaDuplicata` | 1121-1129 | Billing duplicate | -| `NotaFiscalObservacaoContribuinte` | 1132-1137 | Taxpayer note | -| `NotaFiscalProcessoReferenciado` | 1140-1150 | Referenced process | -| `NotaFiscalEntregaRetirada` | 1153-1190 | Delivery/withdrawal address | -| `NotaFiscalServico` | 1192-1220 | NFS-e service invoice | -| `NotaFiscalResponsavelTecnico` | 1223-1229 | Technical responsible (NT2018/003) | -| `AutorizadosBaixarXML` | 1232-1233 | Authorized XML downloaders | -| `NotaFiscalPagamentos` | 1236-1253 | Payment details | +| `NotaFiscal` | 14-580 | Main invoice entity | +| `NotaFiscalReferenciada` | 583-611 | Referenced invoice | +| `NotaFiscalProduto` | 614-1046 | Product/service item with all tax fields | +| `NotaFiscalDeclaracaoImportacao` | 1049-1094 | Import declaration | +| `NotaFiscalDeclaracaoImportacaoAdicao` | 1097-1109 | Import declaration addition | +| `NotaFiscalTransporteVolume` | 1111-1140 | Transport volume | +| `NotaFiscalTransporteVolumeLacre` | 1143-1145 | Volume seal | +| `NotaFiscalCobrancaDuplicata` | 1148-1156 | Billing duplicate | +| `NotaFiscalObservacaoContribuinte` | 1159-1164 | Taxpayer note | +| `NotaFiscalProcessoReferenciado` | 1167-1177 | Referenced process | +| `NotaFiscalEntregaRetirada` | 1180-1217 | Delivery/withdrawal address | +| `NotaFiscalServico` | 1219-1247 | NFS-e service invoice | +| `NotaFiscalResponsavelTecnico` | 1250-1256 | Technical responsible (NT2018/003) | +| `AutorizadosBaixarXML` | 1259-1260 | Authorized XML downloaders | +| `NotaFiscalPagamentos` | 1263-1280 | Payment details | --- @@ -34,7 +34,8 @@ Entity models for NF-e/NFC-e invoices and related objects (products, taxes, paym | Read-only fields | 128-144 | digest_value, valor_total_nota, valor_icms_nota, valor_icms_st_nota, protocolo, data | | Relationships | 146-173 | notas_fiscais_referenciadas, emitente, destinatario_remetente, entrega, retirada, autorizados_baixar_xml, produtos_e_servicos | | ICMS totals | 175-303 | totais_icms_base_calculo, totais_icms_total, totais_icms_desonerado, totais_icms_st_*, totais_icms_total_*, totais_fcp_*, totais_icms_inter_*, totais_icms_*_mono_* | -| Transport | 305-361 | transporte_modalidade_frete, transporte_transportadora, transporte_retencao_icms_*, transporte_veiculo_*, transporte_reboque_*, transporte_volumes | +| Reforma Tributaria totals | 305-311 | totais_cbs, totais_ibs, totais_is — IVA Dual totals (EC 132/2023) | +| Transport | 313-369 | transporte_modalidade_frete, transporte_transportadora, transporte_retencao_icms_*, transporte_veiculo_*, transporte_reboque_*, transporte_volumes | | Billing | 363-378 | fatura_numero, fatura_valor_original, fatura_valor_desconto, fatura_valor_liquido, duplicatas | | Additional info | 380-397 | informacoes_adicionais_*, observacoes_contribuinte, processos_referenciados, pagamentos, valor_troco | @@ -70,7 +71,8 @@ Entity models for NF-e/NFC-e invoices and related objects (products, taxes, paym | COFINS fields | 925-969 | cofins_modalidade, cofins_valor_base_calculo, cofins_aliquota_percentual, cofins_aliquota_reais, cofins_valor, cofins_st_* | | ISSQN fields | 971-990 | issqn_valor_base_calculo, issqn_aliquota, issqn_lista_servico, issqn_uf, issqn_municipio, issqn_valor | | Import tax fields | 992-1003 | imposto_importacao_valor_base_calculo, imposto_importacao_valor_despesas_aduaneiras, imposto_importacao_valor_iof, imposto_importacao_valor | -| Additional info | 1005-1019 | informacoes_adicionais, declaracoes_importacao | +| Reforma Tributaria fields | 1005-1028 | cbs_situacao_tributaria, cbs_valor_base_calculo, cbs_aliquota, cbs_valor, ibs_situacao_tributaria, ibs_valor_base_calculo, ibs_aliquota, ibs_valor, ibs_codigo_municipio_destino, is_situacao_tributaria, is_valor_base_calculo, is_aliquota, is_valor | +| Additional info | 1030-1046 | informacoes_adicionais, declaracoes_importacao | --- diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index 7ab4042e..73fd272b 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -438,6 +438,14 @@ def _serializar_produto_servico( retorna_string=False, ) + # Reforma Tributaria - IVA Dual + self._serializar_imposto_ibscbs( + produto_servico=produto_servico, + modelo=modelo, + tag_raiz=imposto, + retorna_string=False, + ) + # tag impostoDevol if produto_servico.ipi_valor_ipi_dev: impostodevol = etree.SubElement(raiz, "impostoDevol") @@ -1292,6 +1300,85 @@ def _serializar_imposto_importacao( produto_servico.imposto_importacao_valor_iof ) + # ============================================= + # Reforma Tributaria - IVA Dual (EC 132/2023) + # ============================================= + + def _serializar_imposto_ibscbs( + self, produto_servico, modelo, tag_raiz="imposto", retorna_string=True + ): + """Serializa grupo impostoMisto contendo CBS, IBS e IS.""" + has_cbs = produto_servico.cbs_situacao_tributaria + has_ibs = produto_servico.ibs_situacao_tributaria + has_is = produto_servico.is_situacao_tributaria + + if not has_cbs and not has_ibs and not has_is: + return + + imposto_misto = etree.SubElement(tag_raiz, "impostoMisto") + + if has_cbs: + self._serializar_cbs(produto_servico, imposto_misto) + + if has_ibs: + self._serializar_ibs(produto_servico, imposto_misto) + + if has_is: + self._serializar_is(produto_servico, imposto_misto) + + def _serializar_cbs(self, produto_servico, tag_raiz): + """Serializa CBS (Contribuicao sobre Bens e Servicos).""" + cbs = etree.SubElement(tag_raiz, "CBS") + etree.SubElement(cbs, "CST").text = produto_servico.cbs_situacao_tributaria + + if produto_servico.cbs_situacao_tributaria in ("01", "02", "51"): + etree.SubElement(cbs, "vBC").text = "{:.2f}".format( + produto_servico.cbs_valor_base_calculo or 0 + ) + etree.SubElement(cbs, "pCBS").text = "{:.4f}".format( + produto_servico.cbs_aliquota or 0 + ) + etree.SubElement(cbs, "vCBS").text = "{:.2f}".format( + produto_servico.cbs_valor or 0 + ) + + def _serializar_ibs(self, produto_servico, tag_raiz): + """Serializa IBS (Imposto sobre Bens e Servicos).""" + ibs = etree.SubElement(tag_raiz, "IBS") + etree.SubElement(ibs, "CST").text = produto_servico.ibs_situacao_tributaria + + if produto_servico.ibs_situacao_tributaria in ("01", "02", "51"): + etree.SubElement(ibs, "vBC").text = "{:.2f}".format( + produto_servico.ibs_valor_base_calculo or 0 + ) + etree.SubElement(ibs, "pIBS").text = "{:.4f}".format( + produto_servico.ibs_aliquota or 0 + ) + etree.SubElement(ibs, "vIBS").text = "{:.2f}".format( + produto_servico.ibs_valor or 0 + ) + + if produto_servico.ibs_codigo_municipio_destino: + etree.SubElement(ibs, "cMunDest").text = ( + produto_servico.ibs_codigo_municipio_destino + ) + + def _serializar_is(self, produto_servico, tag_raiz): + """Serializa IS (Imposto Seletivo).""" + is_tag = etree.SubElement(tag_raiz, "IS") + etree.SubElement(is_tag, "CST").text = produto_servico.is_situacao_tributaria + + if produto_servico.is_situacao_tributaria in ("01", "02"): + etree.SubElement(is_tag, "vBC").text = "{:.2f}".format( + produto_servico.is_valor_base_calculo or 0 + ) + etree.SubElement(is_tag, "pIS").text = "{:.4f}".format( + produto_servico.is_aliquota or 0 + ) + etree.SubElement(is_tag, "vIS").text = "{:.2f}".format( + produto_servico.is_valor or 0 + ) + def _serializar_declaracao_importacao( self, produto_servico, tag_raiz="prod", retorna_string=True ): @@ -1690,6 +1777,20 @@ def _serializar_nota_fiscal(self, nota_fiscal, tag_raiz="infNFe", retorna_string nota_fiscal.totais_tributos_aproximado ) + # Reforma Tributaria - Totais IVA Dual + if nota_fiscal.totais_cbs: + etree.SubElement(icms_total, "vCBS").text = "{:.2f}".format( + nota_fiscal.totais_cbs + ) + if nota_fiscal.totais_ibs: + etree.SubElement(icms_total, "vIBS").text = "{:.2f}".format( + nota_fiscal.totais_ibs + ) + if nota_fiscal.totais_is: + etree.SubElement(icms_total, "vIS").text = "{:.2f}".format( + nota_fiscal.totais_is + ) + # Transporte transp = etree.SubElement(raiz, "transp") etree.SubElement(transp, "modFrete").text = str(nota_fiscal.transporte_modalidade_frete) diff --git a/pynfe/processamento/serializacao_map.md b/pynfe/processamento/serializacao_map.md index 5c67a689..86fa8a4e 100644 --- a/pynfe/processamento/serializacao_map.md +++ b/pynfe/processamento/serializacao_map.md @@ -1,4 +1,4 @@ -# Source Map: `serializacao.py` (2630 lines) +# Source Map: `serializacao.py` (2740 lines) XML serialization of NF-e, NFC-e, NFS-e and MDF-e documents into SEFAZ-compliant XML format. @@ -8,10 +8,10 @@ XML serialization of NF-e, NFC-e, NFS-e and MDF-e documents into SEFAZ-compliant |-------|-------|---------| | `Serializacao` | 30-63 | Abstract base class (not instantiable directly) | | `SerializacaoXML` | 66-1829 | Main NF-e/NFC-e XML serialization | -| `SerializacaoQrcode` | 1960-2064 | NFC-e QR Code generation | -| `SerializacaoNfse` | 2067-2133 | NFS-e serialization (Betha/Ginfes) | -| `SerializacaoQrcodeMDFe` | 2136-2159 | MDF-e QR Code generation | -| `SerializacaoMDFe` | 2162-2630 | MDF-e XML serialization | +| `SerializacaoQrcode` | 2070-2174 | NFC-e QR Code generation | +| `SerializacaoNfse` | 2177-2243 | NFS-e serialization (Betha/Ginfes) | +| `SerializacaoQrcodeMDFe` | 2246-2269 | MDF-e QR Code generation | +| `SerializacaoMDFe` | 2272-2740 | MDF-e XML serialization | --- @@ -48,7 +48,11 @@ Abstract base for all serializers. Stores `_fonte_dados`, `_ambiente` (1=prod, 2 | `_serializar_imposto_pis()` | 1120-1192 | PIS tax | | `_serializar_imposto_cofins()` | 1194-1269 | COFINS tax | | `_serializar_imposto_importacao()` | 1271-1293 | Import tax (II) | -| `_serializar_declaracao_importacao()` | 1295-1345 | Import declaration (DI) | +| `_serializar_imposto_ibscbs()` | 1295-1322 | Reforma Tributaria — impostoMisto wrapper (CBS + IBS + IS) | +| `_serializar_cbs()` | 1324-1339 | CBS (Contribuicao sobre Bens e Servicos) | +| `_serializar_ibs()` | 1341-1361 | IBS (Imposto sobre Bens e Servicos) | +| `_serializar_is()` | 1363-1378 | IS (Imposto Seletivo) | +| `_serializar_declaracao_importacao()` | 1380-1430 | Import declaration (DI) | ### ICMS Modalities Detail (within `_serializar_imposto_icms`) | CST/CSOSN | Lines | Description | diff --git a/pynfe/utils/flags.py b/pynfe/utils/flags.py index 58519687..36ede00f 100644 --- a/pynfe/utils/flags.py +++ b/pynfe/utils/flags.py @@ -545,6 +545,31 @@ COFINS_TIPOS_CALCULO = IPI_TIPOS_CALCULO +# ============================================= +# Reforma Tributaria - IVA Dual (EC 132/2023) +# ============================================= + +# CST para IBS (Imposto sobre Bens e Servicos) e CBS (Contribuicao sobre Bens e Servicos) +# Tabela preliminar - sujeita a ajuste na regulamentacao da LC +IBS_CBS_TIPOS_TRIBUTACAO = ( + ("01", "Tributada Integralmente"), + ("02", "Tributada com Reducao (Cesta Basica/Saude)"), + ("03", "Isencao"), + ("04", "Imunidade"), + ("05", "Suspensao"), + ("51", "Diferimento"), + ("70", "Monofasica (Combustiveis)"), +) + +# CST para IS (Imposto Seletivo) +IS_TIPOS_TRIBUTACAO = ( + ("01", "Tributada Integralmente"), + ("02", "Tributada com Reducao"), + ("03", "Isencao"), + ("04", "Imunidade"), + ("05", "Suspensao"), +) + MODALIDADES_FRETE = ( (0, "0 - Contratação por conta do Remetente (CIF)"), (1, "1 - Por conta do destinatário"), diff --git a/pynfe/utils/flags_map.md b/pynfe/utils/flags_map.md index a6daa2f3..e4296693 100644 --- a/pynfe/utils/flags_map.md +++ b/pynfe/utils/flags_map.md @@ -1,4 +1,4 @@ -# Source Map: `flags.py` (645 lines) +# Source Map: `flags.py` (671 lines) Constants, namespaces, tax code enumerations, and state codes used throughout PyNFe. @@ -17,11 +17,12 @@ Constants, namespaces, tax code enumerations, and state codes used throughout Py | IPI types | 248-268 | `IPI_TIPOS_TRIBUTACAO` and `IPI_TIPOS_CALCULO` | | PIS types | 270-407 | `PIS_TIPOS_TRIBUTACAO` — all CST codes (01-99) | | COFINS types | 409-546 | `COFINS_TIPOS_TRIBUTACAO` — all CST codes (01-99) | -| Freight modalities | 548-555 | `MODALIDADES_FRETE` (0-9) | -| Process origins | 557-563 | `ORIGENS_PROCESSO` | -| State codes | 565-597 | `CODIGOS_ESTADOS` — UF to IBGE code mapping | -| Card brands | 599-628 | `BANDEIRA_CARTAO` (01-99) | -| Payment methods | 630-645 | `FORMAS_PAGAMENTO` (01-99) | +| Reforma Tributaria | 548-572 | `IBS_CBS_TIPOS_TRIBUTACAO` and `IS_TIPOS_TRIBUTACAO` — IVA Dual CSTs (EC 132/2023) | +| Freight modalities | 574-581 | `MODALIDADES_FRETE` (0-9) | +| Process origins | 583-589 | `ORIGENS_PROCESSO` | +| State codes | 591-623 | `CODIGOS_ESTADOS` — UF to IBGE code mapping | +| Card brands | 625-654 | `BANDEIRA_CARTAO` (01-99) | +| Payment methods | 656-671 | `FORMAS_PAGAMENTO` (01-99) | ## Key Constants diff --git a/tests/test_nfe_serializacao_reforma_tributaria.py b/tests/test_nfe_serializacao_reforma_tributaria.py new file mode 100644 index 00000000..a8732386 --- /dev/null +++ b/tests/test_nfe_serializacao_reforma_tributaria.py @@ -0,0 +1,602 @@ +#!/usr/bin/env python +# *-* encoding: utf8 *-* + +"""Tests for Reforma Tributaria IBS/CBS/IS serialization (EC 132/2023).""" + +import datetime +import unittest +from decimal import Decimal + +from pynfe.entidades.cliente import Cliente +from pynfe.entidades.emitente import Emitente +from pynfe.entidades.notafiscal import NotaFiscal +from pynfe.entidades.fonte_dados import _fonte_dados +from pynfe.processamento.assinatura import AssinaturaA1 +from pynfe.processamento.serializacao import SerializacaoXML +from pynfe.utils.flags import CODIGO_BRASIL, NAMESPACE_NFE + + +class ReformaTributariaSerializacaoTestCase(unittest.TestCase): + """Tests for IBS/CBS/IS (Reforma Tributaria) XML serialization.""" + + def setUp(self): + self.certificado = "./tests/certificado.pfx" + self.senha = bytes("123456", "utf-8") + self.uf = "pr" + self.homologacao = True + self.ns = {"ns": NAMESPACE_NFE} + + def _emitente(self): + return Emitente( + razao_social="NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL", + nome_fantasia="Empresa Teste", + cnpj="99999999000199", + codigo_de_regime_tributario="3", + inscricao_estadual="9999999999", + inscricao_municipal="12345", + cnae_fiscal="9999999", + endereco_logradouro="Rua da Paz", + endereco_numero="666", + endereco_bairro="Sossego", + endereco_municipio="Paranavaí", + endereco_uf="PR", + endereco_cep="87704000", + endereco_pais=CODIGO_BRASIL, + ) + + def _cliente(self): + return Cliente( + razao_social="JOSE DA SILVA", + tipo_documento="CPF", + email="email@email.com", + numero_documento="12345678900", + indicador_ie=9, + endereco_logradouro="Rua dos Bobos", + endereco_numero="Zero", + endereco_bairro="Aquele Mesmo", + endereco_municipio="Brasilia", + endereco_uf="DF", + endereco_cep="12345123", + endereco_pais=CODIGO_BRASIL, + ) + + def _nota_fiscal(self, emitente, cliente): + utc = datetime.timezone.utc + return NotaFiscal( + emitente=emitente, + cliente=cliente, + uf="PR", + natureza_operacao="VENDA", + forma_pagamento=0, + modelo=55, + serie="1", + numero_nf="111", + data_emissao=datetime.datetime(2026, 1, 15, 12, 0, 0, tzinfo=utc), + data_saida_entrada=datetime.datetime(2026, 1, 15, 13, 0, 0, tzinfo=utc), + tipo_documento=1, + municipio="4118402", + tipo_impressao_danfe=1, + forma_emissao="1", + cliente_final=1, + indicador_destino=1, + indicador_presencial=1, + finalidade_emissao="1", + processo_emissao="0", + transporte_modalidade_frete=9, + informacoes_adicionais_interesse_fisco="Reforma Tributaria teste", + totais_tributos_aproximado=Decimal("0.00"), + ) + + def _serializar_e_assinar(self): + serializador = SerializacaoXML(_fonte_dados, homologacao=self.homologacao) + xml = serializador.exportar() + a1 = AssinaturaA1(self.certificado, self.senha) + return a1.assinar(xml) + + # ------------------------------------------------------------------ + # Test 1: CST 01 - Tributada Integralmente (CBS + IBS + IS) + # ------------------------------------------------------------------ + def test_cst01_cbs_ibs_is_tributada_integralmente(self): + emitente = self._emitente() + cliente = self._cliente() + nf = self._nota_fiscal(emitente, cliente) + + nf.adicionar_produto_servico( + codigo="001", + descricao="Produto teste reforma tributaria", + ncm="99999999", + ean="SEM GTIN", + cfop="5102", + unidade_comercial="UN", + quantidade_comercial=Decimal("10"), + valor_unitario_comercial=Decimal("100.00"), + valor_total_bruto=Decimal("1000.00"), + unidade_tributavel="UN", + quantidade_tributavel=Decimal("10"), + valor_unitario_tributavel=Decimal("100.00"), + ean_tributavel="SEM GTIN", + ind_total=1, + icms_modalidade="00", + icms_origem=0, + icms_csosn="", + pis_modalidade="99", + cofins_modalidade="99", + pis_valor_base_calculo=Decimal("0.00"), + pis_aliquota_percentual=Decimal("0.00"), + pis_valor=Decimal("0.00"), + cofins_valor_base_calculo=Decimal("0.00"), + cofins_aliquota_percentual=Decimal("0.00"), + cofins_valor=Decimal("0.00"), + valor_tributos_aprox="0", + # CBS + cbs_situacao_tributaria="01", + cbs_valor_base_calculo=Decimal("1000.00"), + cbs_aliquota=Decimal("8.8000"), + cbs_valor=Decimal("88.00"), + # IBS + ibs_situacao_tributaria="01", + ibs_valor_base_calculo=Decimal("1000.00"), + ibs_aliquota=Decimal("17.7000"), + ibs_valor=Decimal("177.00"), + ibs_codigo_municipio_destino="4118402", + # IS + is_situacao_tributaria="01", + is_valor_base_calculo=Decimal("1000.00"), + is_aliquota=Decimal("1.0000"), + is_valor=Decimal("10.00"), + ) + + nf.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=1275.00, ind_pag=0) + + xml = self._serializar_e_assinar() + + # impostoMisto group exists + imposto_misto = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto", namespaces=self.ns + ) + self.assertEqual(len(imposto_misto), 1) + + # CBS + cst_cbs = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto/ns:CBS/ns:CST", namespaces=self.ns + )[0].text + self.assertEqual(cst_cbs, "01") + + vbc_cbs = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto/ns:CBS/ns:vBC", namespaces=self.ns + )[0].text + self.assertEqual(vbc_cbs, "1000.00") + + pcbs = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto/ns:CBS/ns:pCBS", namespaces=self.ns + )[0].text + self.assertEqual(pcbs, "8.8000") + + vcbs = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto/ns:CBS/ns:vCBS", namespaces=self.ns + )[0].text + self.assertEqual(vcbs, "88.00") + + # IBS + cst_ibs = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto/ns:IBS/ns:CST", namespaces=self.ns + )[0].text + self.assertEqual(cst_ibs, "01") + + vbc_ibs = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto/ns:IBS/ns:vBC", namespaces=self.ns + )[0].text + self.assertEqual(vbc_ibs, "1000.00") + + pibs = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto/ns:IBS/ns:pIBS", namespaces=self.ns + )[0].text + self.assertEqual(pibs, "17.7000") + + vibs = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto/ns:IBS/ns:vIBS", namespaces=self.ns + )[0].text + self.assertEqual(vibs, "177.00") + + cmun = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto/ns:IBS/ns:cMunDest", namespaces=self.ns + )[0].text + self.assertEqual(cmun, "4118402") + + # IS + cst_is = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto/ns:IS/ns:CST", namespaces=self.ns + )[0].text + self.assertEqual(cst_is, "01") + + vbc_is = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto/ns:IS/ns:vBC", namespaces=self.ns + )[0].text + self.assertEqual(vbc_is, "1000.00") + + pis_val = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto/ns:IS/ns:pIS", namespaces=self.ns + )[0].text + self.assertEqual(pis_val, "1.0000") + + vis = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto/ns:IS/ns:vIS", namespaces=self.ns + )[0].text + self.assertEqual(vis, "10.00") + + # Totals + vcbs_total = xml.xpath( + "//ns:total/ns:ICMSTot/ns:vCBS", namespaces=self.ns + )[0].text + self.assertEqual(vcbs_total, "88.00") + + vibs_total = xml.xpath( + "//ns:total/ns:ICMSTot/ns:vIBS", namespaces=self.ns + )[0].text + self.assertEqual(vibs_total, "177.00") + + vis_total = xml.xpath( + "//ns:total/ns:ICMSTot/ns:vIS", namespaces=self.ns + )[0].text + self.assertEqual(vis_total, "10.00") + + # vNF includes IBS/CBS/IS + vnf = xml.xpath( + "//ns:total/ns:ICMSTot/ns:vNF", namespaces=self.ns + )[0].text + self.assertEqual(vnf, "1275.00") + + # ------------------------------------------------------------------ + # Test 2: CST 03 - Isencao (CBS + IBS isentas, no IS) + # ------------------------------------------------------------------ + def test_cst03_isencao_sem_valores(self): + emitente = self._emitente() + cliente = self._cliente() + nf = self._nota_fiscal(emitente, cliente) + + nf.adicionar_produto_servico( + codigo="002", + descricao="Produto isento reforma tributaria", + ncm="99999999", + ean="SEM GTIN", + cfop="5102", + unidade_comercial="UN", + quantidade_comercial=Decimal("1"), + valor_unitario_comercial=Decimal("50.00"), + valor_total_bruto=Decimal("50.00"), + unidade_tributavel="UN", + quantidade_tributavel=Decimal("1"), + valor_unitario_tributavel=Decimal("50.00"), + ean_tributavel="SEM GTIN", + ind_total=1, + icms_modalidade="00", + icms_origem=0, + icms_csosn="", + pis_modalidade="99", + cofins_modalidade="99", + pis_valor_base_calculo=Decimal("0.00"), + pis_aliquota_percentual=Decimal("0.00"), + pis_valor=Decimal("0.00"), + cofins_valor_base_calculo=Decimal("0.00"), + cofins_aliquota_percentual=Decimal("0.00"), + cofins_valor=Decimal("0.00"), + valor_tributos_aprox="0", + # CBS isenta + cbs_situacao_tributaria="03", + # IBS isenta + ibs_situacao_tributaria="03", + # No IS + ) + + nf.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=50.00, ind_pag=0) + + xml = self._serializar_e_assinar() + + # impostoMisto group exists (CBS + IBS present even if exempt) + imposto_misto = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto", namespaces=self.ns + ) + self.assertEqual(len(imposto_misto), 1) + + # CBS CST present, no value tags + cst_cbs = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto/ns:CBS/ns:CST", namespaces=self.ns + )[0].text + self.assertEqual(cst_cbs, "03") + + vbc_cbs = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto/ns:CBS/ns:vBC", namespaces=self.ns + ) + self.assertEqual(len(vbc_cbs), 0) # No vBC for CST 03 + + # IBS CST present, no value tags + cst_ibs = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto/ns:IBS/ns:CST", namespaces=self.ns + )[0].text + self.assertEqual(cst_ibs, "03") + + vbc_ibs = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto/ns:IBS/ns:vBC", namespaces=self.ns + ) + self.assertEqual(len(vbc_ibs), 0) + + # No IS group + is_group = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto/ns:IS", namespaces=self.ns + ) + self.assertEqual(len(is_group), 0) + + # No totals for CBS/IBS/IS (all zero) + vcbs_total = xml.xpath( + "//ns:total/ns:ICMSTot/ns:vCBS", namespaces=self.ns + ) + self.assertEqual(len(vcbs_total), 0) + + # ------------------------------------------------------------------ + # Test 3: No reforma data - impostoMisto NOT emitted + # ------------------------------------------------------------------ + def test_sem_reforma_tributaria_sem_imposto_misto(self): + emitente = self._emitente() + cliente = self._cliente() + nf = self._nota_fiscal(emitente, cliente) + + nf.adicionar_produto_servico( + codigo="003", + descricao="Produto sem reforma", + ncm="99999999", + ean="SEM GTIN", + cfop="5102", + unidade_comercial="UN", + quantidade_comercial=Decimal("1"), + valor_unitario_comercial=Decimal("100.00"), + valor_total_bruto=Decimal("100.00"), + unidade_tributavel="UN", + quantidade_tributavel=Decimal("1"), + valor_unitario_tributavel=Decimal("100.00"), + ean_tributavel="SEM GTIN", + ind_total=1, + icms_modalidade="00", + icms_origem=0, + icms_csosn="", + pis_modalidade="51", + cofins_modalidade="51", + pis_valor_base_calculo=Decimal("100.00"), + pis_aliquota_percentual=Decimal("0.65"), + pis_valor=Decimal("0.65"), + cofins_valor_base_calculo=Decimal("100.00"), + cofins_aliquota_percentual=Decimal("3.00"), + cofins_valor=Decimal("3.00"), + valor_tributos_aprox="0", + ) + + nf.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=100.00, ind_pag=0) + + xml = self._serializar_e_assinar() + + # impostoMisto should NOT exist + imposto_misto = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto", namespaces=self.ns + ) + self.assertEqual(len(imposto_misto), 0) + + # No reform totals + vcbs_total = xml.xpath( + "//ns:total/ns:ICMSTot/ns:vCBS", namespaces=self.ns + ) + self.assertEqual(len(vcbs_total), 0) + + vibs_total = xml.xpath( + "//ns:total/ns:ICMSTot/ns:vIBS", namespaces=self.ns + ) + self.assertEqual(len(vibs_total), 0) + + # ------------------------------------------------------------------ + # Test 4: Totals accumulation - multiple products + # ------------------------------------------------------------------ + def test_totais_acumulacao_multiplos_produtos(self): + emitente = self._emitente() + cliente = self._cliente() + nf = self._nota_fiscal(emitente, cliente) + + # Product 1: CBS=88, IBS=177, IS=10 + nf.adicionar_produto_servico( + codigo="004", + descricao="Produto 1 reforma", + ncm="99999999", + ean="SEM GTIN", + cfop="5102", + unidade_comercial="UN", + quantidade_comercial=Decimal("10"), + valor_unitario_comercial=Decimal("100.00"), + valor_total_bruto=Decimal("1000.00"), + unidade_tributavel="UN", + quantidade_tributavel=Decimal("10"), + valor_unitario_tributavel=Decimal("100.00"), + ean_tributavel="SEM GTIN", + ind_total=1, + icms_modalidade="00", + icms_origem=0, + icms_csosn="", + pis_modalidade="99", + cofins_modalidade="99", + pis_valor=Decimal("0.00"), + cofins_valor=Decimal("0.00"), + valor_tributos_aprox="0", + cbs_situacao_tributaria="01", + cbs_valor_base_calculo=Decimal("1000.00"), + cbs_aliquota=Decimal("8.8000"), + cbs_valor=Decimal("88.00"), + ibs_situacao_tributaria="01", + ibs_valor_base_calculo=Decimal("1000.00"), + ibs_aliquota=Decimal("17.7000"), + ibs_valor=Decimal("177.00"), + is_situacao_tributaria="01", + is_valor_base_calculo=Decimal("1000.00"), + is_aliquota=Decimal("1.0000"), + is_valor=Decimal("10.00"), + ) + + # Product 2: CBS=44, IBS=88.50, IS=5 + nf.adicionar_produto_servico( + codigo="005", + descricao="Produto 2 reforma", + ncm="99999999", + ean="SEM GTIN", + cfop="5102", + unidade_comercial="UN", + quantidade_comercial=Decimal("5"), + valor_unitario_comercial=Decimal("100.00"), + valor_total_bruto=Decimal("500.00"), + unidade_tributavel="UN", + quantidade_tributavel=Decimal("5"), + valor_unitario_tributavel=Decimal("100.00"), + ean_tributavel="SEM GTIN", + ind_total=1, + icms_modalidade="00", + icms_origem=0, + icms_csosn="", + pis_modalidade="99", + cofins_modalidade="99", + pis_valor=Decimal("0.00"), + cofins_valor=Decimal("0.00"), + valor_tributos_aprox="0", + cbs_situacao_tributaria="01", + cbs_valor_base_calculo=Decimal("500.00"), + cbs_aliquota=Decimal("8.8000"), + cbs_valor=Decimal("44.00"), + ibs_situacao_tributaria="01", + ibs_valor_base_calculo=Decimal("500.00"), + ibs_aliquota=Decimal("17.7000"), + ibs_valor=Decimal("88.50"), + is_situacao_tributaria="01", + is_valor_base_calculo=Decimal("500.00"), + is_aliquota=Decimal("1.0000"), + is_valor=Decimal("5.00"), + ) + + nf.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=1907.50, ind_pag=0) + + xml = self._serializar_e_assinar() + + # Accumulated totals: CBS=132.00, IBS=265.50, IS=15.00 + vcbs_total = xml.xpath( + "//ns:total/ns:ICMSTot/ns:vCBS", namespaces=self.ns + )[0].text + self.assertEqual(vcbs_total, "132.00") + + vibs_total = xml.xpath( + "//ns:total/ns:ICMSTot/ns:vIBS", namespaces=self.ns + )[0].text + self.assertEqual(vibs_total, "265.50") + + vis_total = xml.xpath( + "//ns:total/ns:ICMSTot/ns:vIS", namespaces=self.ns + )[0].text + self.assertEqual(vis_total, "15.00") + + # vNF = 1000 + 500 + 132 + 265.50 + 15 = 1912.50 + # Wait: vNF = sum of (vProd + IBS + CBS + IS) for each product + # Prod1: 1000 + 177 + 88 + 10 = 1275 + # Prod2: 500 + 88.50 + 44 + 5 = 637.50 + # Total: 1275 + 637.50 = 1912.50 + vnf = xml.xpath( + "//ns:total/ns:ICMSTot/ns:vNF", namespaces=self.ns + )[0].text + self.assertEqual(vnf, "1912.50") + + # ------------------------------------------------------------------ + # Test 5: Mixed legacy + reform (ICMS/PIS/COFINS + IBS/CBS) + # ------------------------------------------------------------------ + def test_misto_legacy_icms_pis_cofins_com_ibs_cbs(self): + emitente = self._emitente() + cliente = self._cliente() + nf = self._nota_fiscal(emitente, cliente) + + nf.adicionar_produto_servico( + codigo="006", + descricao="Produto misto legado e reforma", + ncm="99999999", + ean="SEM GTIN", + cfop="5102", + unidade_comercial="UN", + quantidade_comercial=Decimal("1"), + valor_unitario_comercial=Decimal("200.00"), + valor_total_bruto=Decimal("200.00"), + unidade_tributavel="UN", + quantidade_tributavel=Decimal("1"), + valor_unitario_tributavel=Decimal("200.00"), + ean_tributavel="SEM GTIN", + ind_total=1, + # Legacy ICMS + icms_modalidade="00", + icms_origem=0, + icms_csosn="", + icms_valor_base_calculo=Decimal("200.00"), + icms_aliquota=Decimal("18.00"), + icms_valor=Decimal("36.00"), + # Legacy PIS + pis_modalidade="01", + pis_valor_base_calculo=Decimal("200.00"), + pis_aliquota_percentual=Decimal("1.65"), + pis_valor=Decimal("3.30"), + # Legacy COFINS + cofins_modalidade="01", + cofins_valor_base_calculo=Decimal("200.00"), + cofins_aliquota_percentual=Decimal("7.60"), + cofins_valor=Decimal("15.20"), + valor_tributos_aprox="0", + # Reform CBS (coexisting during transition) + cbs_situacao_tributaria="02", + cbs_valor_base_calculo=Decimal("200.00"), + cbs_aliquota=Decimal("4.4000"), + cbs_valor=Decimal("8.80"), + # Reform IBS (coexisting during transition) + ibs_situacao_tributaria="02", + ibs_valor_base_calculo=Decimal("200.00"), + ibs_aliquota=Decimal("8.8500"), + ibs_valor=Decimal("17.70"), + ) + + nf.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=226.50, ind_pag=0) + + xml = self._serializar_e_assinar() + + # Legacy ICMS still present + icms_cst = xml.xpath( + "//ns:det/ns:imposto/ns:ICMS/ns:ICMS00/ns:CST", namespaces=self.ns + )[0].text + self.assertEqual(icms_cst, "00") + + # Legacy PIS still present + pis_cst = xml.xpath( + "//ns:det/ns:imposto/ns:PIS/ns:PISAliq/ns:CST", namespaces=self.ns + )[0].text + self.assertEqual(pis_cst, "01") + + # Legacy COFINS still present + cofins_cst = xml.xpath( + "//ns:det/ns:imposto/ns:COFINS/ns:COFINSAliq/ns:CST", namespaces=self.ns + )[0].text + self.assertEqual(cofins_cst, "01") + + # Reform CBS also present + cbs_cst = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto/ns:CBS/ns:CST", namespaces=self.ns + )[0].text + self.assertEqual(cbs_cst, "02") + + # Reform IBS also present + ibs_cst = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto/ns:IBS/ns:CST", namespaces=self.ns + )[0].text + self.assertEqual(ibs_cst, "02") + + # No IS in this test + is_group = xml.xpath( + "//ns:det/ns:imposto/ns:impostoMisto/ns:IS", namespaces=self.ns + ) + self.assertEqual(len(is_group), 0) + + +if __name__ == "__main__": + unittest.main() From 19840a9b44e4e3c8a4270a9990f76cf65c237b19 Mon Sep 17 00:00:00 2001 From: Felipe Correa Date: Fri, 13 Feb 2026 00:51:37 -0300 Subject: [PATCH 3/7] style: Fix ruff format issues in serializacao.py and reforma tributaria tests Co-Authored-By: Claude Opus 4.6 --- pynfe/processamento/serializacao.py | 40 ++---- ...est_nfe_serializacao_reforma_tributaria.py | 132 +++++++----------- 2 files changed, 58 insertions(+), 114 deletions(-) diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index 73fd272b..23419592 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -1335,12 +1335,8 @@ def _serializar_cbs(self, produto_servico, tag_raiz): etree.SubElement(cbs, "vBC").text = "{:.2f}".format( produto_servico.cbs_valor_base_calculo or 0 ) - etree.SubElement(cbs, "pCBS").text = "{:.4f}".format( - produto_servico.cbs_aliquota or 0 - ) - etree.SubElement(cbs, "vCBS").text = "{:.2f}".format( - produto_servico.cbs_valor or 0 - ) + etree.SubElement(cbs, "pCBS").text = "{:.4f}".format(produto_servico.cbs_aliquota or 0) + etree.SubElement(cbs, "vCBS").text = "{:.2f}".format(produto_servico.cbs_valor or 0) def _serializar_ibs(self, produto_servico, tag_raiz): """Serializa IBS (Imposto sobre Bens e Servicos).""" @@ -1351,17 +1347,11 @@ def _serializar_ibs(self, produto_servico, tag_raiz): etree.SubElement(ibs, "vBC").text = "{:.2f}".format( produto_servico.ibs_valor_base_calculo or 0 ) - etree.SubElement(ibs, "pIBS").text = "{:.4f}".format( - produto_servico.ibs_aliquota or 0 - ) - etree.SubElement(ibs, "vIBS").text = "{:.2f}".format( - produto_servico.ibs_valor or 0 - ) + etree.SubElement(ibs, "pIBS").text = "{:.4f}".format(produto_servico.ibs_aliquota or 0) + etree.SubElement(ibs, "vIBS").text = "{:.2f}".format(produto_servico.ibs_valor or 0) if produto_servico.ibs_codigo_municipio_destino: - etree.SubElement(ibs, "cMunDest").text = ( - produto_servico.ibs_codigo_municipio_destino - ) + etree.SubElement(ibs, "cMunDest").text = produto_servico.ibs_codigo_municipio_destino def _serializar_is(self, produto_servico, tag_raiz): """Serializa IS (Imposto Seletivo).""" @@ -1372,12 +1362,8 @@ def _serializar_is(self, produto_servico, tag_raiz): etree.SubElement(is_tag, "vBC").text = "{:.2f}".format( produto_servico.is_valor_base_calculo or 0 ) - etree.SubElement(is_tag, "pIS").text = "{:.4f}".format( - produto_servico.is_aliquota or 0 - ) - etree.SubElement(is_tag, "vIS").text = "{:.2f}".format( - produto_servico.is_valor or 0 - ) + etree.SubElement(is_tag, "pIS").text = "{:.4f}".format(produto_servico.is_aliquota or 0) + etree.SubElement(is_tag, "vIS").text = "{:.2f}".format(produto_servico.is_valor or 0) def _serializar_declaracao_importacao( self, produto_servico, tag_raiz="prod", retorna_string=True @@ -1779,17 +1765,11 @@ def _serializar_nota_fiscal(self, nota_fiscal, tag_raiz="infNFe", retorna_string # Reforma Tributaria - Totais IVA Dual if nota_fiscal.totais_cbs: - etree.SubElement(icms_total, "vCBS").text = "{:.2f}".format( - nota_fiscal.totais_cbs - ) + etree.SubElement(icms_total, "vCBS").text = "{:.2f}".format(nota_fiscal.totais_cbs) if nota_fiscal.totais_ibs: - etree.SubElement(icms_total, "vIBS").text = "{:.2f}".format( - nota_fiscal.totais_ibs - ) + etree.SubElement(icms_total, "vIBS").text = "{:.2f}".format(nota_fiscal.totais_ibs) if nota_fiscal.totais_is: - etree.SubElement(icms_total, "vIS").text = "{:.2f}".format( - nota_fiscal.totais_is - ) + etree.SubElement(icms_total, "vIS").text = "{:.2f}".format(nota_fiscal.totais_is) # Transporte transp = etree.SubElement(raiz, "transp") diff --git a/tests/test_nfe_serializacao_reforma_tributaria.py b/tests/test_nfe_serializacao_reforma_tributaria.py index a8732386..cf866c42 100644 --- a/tests/test_nfe_serializacao_reforma_tributaria.py +++ b/tests/test_nfe_serializacao_reforma_tributaria.py @@ -151,9 +151,7 @@ def test_cst01_cbs_ibs_is_tributada_integralmente(self): xml = self._serializar_e_assinar() # impostoMisto group exists - imposto_misto = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto", namespaces=self.ns - ) + imposto_misto = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto", namespaces=self.ns) self.assertEqual(len(imposto_misto), 1) # CBS @@ -167,14 +165,14 @@ def test_cst01_cbs_ibs_is_tributada_integralmente(self): )[0].text self.assertEqual(vbc_cbs, "1000.00") - pcbs = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto/ns:CBS/ns:pCBS", namespaces=self.ns - )[0].text + pcbs = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:CBS/ns:pCBS", namespaces=self.ns)[ + 0 + ].text self.assertEqual(pcbs, "8.8000") - vcbs = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto/ns:CBS/ns:vCBS", namespaces=self.ns - )[0].text + vcbs = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:CBS/ns:vCBS", namespaces=self.ns)[ + 0 + ].text self.assertEqual(vcbs, "88.00") # IBS @@ -188,14 +186,14 @@ def test_cst01_cbs_ibs_is_tributada_integralmente(self): )[0].text self.assertEqual(vbc_ibs, "1000.00") - pibs = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto/ns:IBS/ns:pIBS", namespaces=self.ns - )[0].text + pibs = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:IBS/ns:pIBS", namespaces=self.ns)[ + 0 + ].text self.assertEqual(pibs, "17.7000") - vibs = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto/ns:IBS/ns:vIBS", namespaces=self.ns - )[0].text + vibs = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:IBS/ns:vIBS", namespaces=self.ns)[ + 0 + ].text self.assertEqual(vibs, "177.00") cmun = xml.xpath( @@ -204,46 +202,38 @@ def test_cst01_cbs_ibs_is_tributada_integralmente(self): self.assertEqual(cmun, "4118402") # IS - cst_is = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto/ns:IS/ns:CST", namespaces=self.ns - )[0].text + cst_is = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:IS/ns:CST", namespaces=self.ns)[ + 0 + ].text self.assertEqual(cst_is, "01") - vbc_is = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto/ns:IS/ns:vBC", namespaces=self.ns - )[0].text + vbc_is = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:IS/ns:vBC", namespaces=self.ns)[ + 0 + ].text self.assertEqual(vbc_is, "1000.00") - pis_val = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto/ns:IS/ns:pIS", namespaces=self.ns - )[0].text + pis_val = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:IS/ns:pIS", namespaces=self.ns)[ + 0 + ].text self.assertEqual(pis_val, "1.0000") - vis = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto/ns:IS/ns:vIS", namespaces=self.ns - )[0].text + vis = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:IS/ns:vIS", namespaces=self.ns)[ + 0 + ].text self.assertEqual(vis, "10.00") # Totals - vcbs_total = xml.xpath( - "//ns:total/ns:ICMSTot/ns:vCBS", namespaces=self.ns - )[0].text + vcbs_total = xml.xpath("//ns:total/ns:ICMSTot/ns:vCBS", namespaces=self.ns)[0].text self.assertEqual(vcbs_total, "88.00") - vibs_total = xml.xpath( - "//ns:total/ns:ICMSTot/ns:vIBS", namespaces=self.ns - )[0].text + vibs_total = xml.xpath("//ns:total/ns:ICMSTot/ns:vIBS", namespaces=self.ns)[0].text self.assertEqual(vibs_total, "177.00") - vis_total = xml.xpath( - "//ns:total/ns:ICMSTot/ns:vIS", namespaces=self.ns - )[0].text + vis_total = xml.xpath("//ns:total/ns:ICMSTot/ns:vIS", namespaces=self.ns)[0].text self.assertEqual(vis_total, "10.00") # vNF includes IBS/CBS/IS - vnf = xml.xpath( - "//ns:total/ns:ICMSTot/ns:vNF", namespaces=self.ns - )[0].text + vnf = xml.xpath("//ns:total/ns:ICMSTot/ns:vNF", namespaces=self.ns)[0].text self.assertEqual(vnf, "1275.00") # ------------------------------------------------------------------ @@ -293,9 +283,7 @@ def test_cst03_isencao_sem_valores(self): xml = self._serializar_e_assinar() # impostoMisto group exists (CBS + IBS present even if exempt) - imposto_misto = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto", namespaces=self.ns - ) + imposto_misto = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto", namespaces=self.ns) self.assertEqual(len(imposto_misto), 1) # CBS CST present, no value tags @@ -304,9 +292,7 @@ def test_cst03_isencao_sem_valores(self): )[0].text self.assertEqual(cst_cbs, "03") - vbc_cbs = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto/ns:CBS/ns:vBC", namespaces=self.ns - ) + vbc_cbs = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:CBS/ns:vBC", namespaces=self.ns) self.assertEqual(len(vbc_cbs), 0) # No vBC for CST 03 # IBS CST present, no value tags @@ -315,21 +301,15 @@ def test_cst03_isencao_sem_valores(self): )[0].text self.assertEqual(cst_ibs, "03") - vbc_ibs = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto/ns:IBS/ns:vBC", namespaces=self.ns - ) + vbc_ibs = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:IBS/ns:vBC", namespaces=self.ns) self.assertEqual(len(vbc_ibs), 0) # No IS group - is_group = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto/ns:IS", namespaces=self.ns - ) + is_group = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:IS", namespaces=self.ns) self.assertEqual(len(is_group), 0) # No totals for CBS/IBS/IS (all zero) - vcbs_total = xml.xpath( - "//ns:total/ns:ICMSTot/ns:vCBS", namespaces=self.ns - ) + vcbs_total = xml.xpath("//ns:total/ns:ICMSTot/ns:vCBS", namespaces=self.ns) self.assertEqual(len(vcbs_total), 0) # ------------------------------------------------------------------ @@ -374,20 +354,14 @@ def test_sem_reforma_tributaria_sem_imposto_misto(self): xml = self._serializar_e_assinar() # impostoMisto should NOT exist - imposto_misto = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto", namespaces=self.ns - ) + imposto_misto = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto", namespaces=self.ns) self.assertEqual(len(imposto_misto), 0) # No reform totals - vcbs_total = xml.xpath( - "//ns:total/ns:ICMSTot/ns:vCBS", namespaces=self.ns - ) + vcbs_total = xml.xpath("//ns:total/ns:ICMSTot/ns:vCBS", namespaces=self.ns) self.assertEqual(len(vcbs_total), 0) - vibs_total = xml.xpath( - "//ns:total/ns:ICMSTot/ns:vIBS", namespaces=self.ns - ) + vibs_total = xml.xpath("//ns:total/ns:ICMSTot/ns:vIBS", namespaces=self.ns) self.assertEqual(len(vibs_total), 0) # ------------------------------------------------------------------ @@ -479,19 +453,13 @@ def test_totais_acumulacao_multiplos_produtos(self): xml = self._serializar_e_assinar() # Accumulated totals: CBS=132.00, IBS=265.50, IS=15.00 - vcbs_total = xml.xpath( - "//ns:total/ns:ICMSTot/ns:vCBS", namespaces=self.ns - )[0].text + vcbs_total = xml.xpath("//ns:total/ns:ICMSTot/ns:vCBS", namespaces=self.ns)[0].text self.assertEqual(vcbs_total, "132.00") - vibs_total = xml.xpath( - "//ns:total/ns:ICMSTot/ns:vIBS", namespaces=self.ns - )[0].text + vibs_total = xml.xpath("//ns:total/ns:ICMSTot/ns:vIBS", namespaces=self.ns)[0].text self.assertEqual(vibs_total, "265.50") - vis_total = xml.xpath( - "//ns:total/ns:ICMSTot/ns:vIS", namespaces=self.ns - )[0].text + vis_total = xml.xpath("//ns:total/ns:ICMSTot/ns:vIS", namespaces=self.ns)[0].text self.assertEqual(vis_total, "15.00") # vNF = 1000 + 500 + 132 + 265.50 + 15 = 1912.50 @@ -499,9 +467,7 @@ def test_totais_acumulacao_multiplos_produtos(self): # Prod1: 1000 + 177 + 88 + 10 = 1275 # Prod2: 500 + 88.50 + 44 + 5 = 637.50 # Total: 1275 + 637.50 = 1912.50 - vnf = xml.xpath( - "//ns:total/ns:ICMSTot/ns:vNF", namespaces=self.ns - )[0].text + vnf = xml.xpath("//ns:total/ns:ICMSTot/ns:vNF", namespaces=self.ns)[0].text self.assertEqual(vnf, "1912.50") # ------------------------------------------------------------------ @@ -562,15 +528,15 @@ def test_misto_legacy_icms_pis_cofins_com_ibs_cbs(self): xml = self._serializar_e_assinar() # Legacy ICMS still present - icms_cst = xml.xpath( - "//ns:det/ns:imposto/ns:ICMS/ns:ICMS00/ns:CST", namespaces=self.ns - )[0].text + icms_cst = xml.xpath("//ns:det/ns:imposto/ns:ICMS/ns:ICMS00/ns:CST", namespaces=self.ns)[ + 0 + ].text self.assertEqual(icms_cst, "00") # Legacy PIS still present - pis_cst = xml.xpath( - "//ns:det/ns:imposto/ns:PIS/ns:PISAliq/ns:CST", namespaces=self.ns - )[0].text + pis_cst = xml.xpath("//ns:det/ns:imposto/ns:PIS/ns:PISAliq/ns:CST", namespaces=self.ns)[ + 0 + ].text self.assertEqual(pis_cst, "01") # Legacy COFINS still present @@ -592,9 +558,7 @@ def test_misto_legacy_icms_pis_cofins_com_ibs_cbs(self): self.assertEqual(ibs_cst, "02") # No IS in this test - is_group = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto/ns:IS", namespaces=self.ns - ) + is_group = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:IS", namespaces=self.ns) self.assertEqual(len(is_group), 0) From 628941e3614e9c342fa4bea5e669595fee7b4748 Mon Sep 17 00:00:00 2001 From: Felipe Correa Date: Fri, 13 Feb 2026 22:49:10 -0300 Subject: [PATCH 4/7] docs: Move source maps to docs/ and rename CLAUDE.md to AGENTS.md Address PR #448 review feedback: consolidate documentation files under docs/ directory and use model-agnostic AGENTS.md naming. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md => AGENTS.md | 25 ++++++++++--------- .../autorizador_nfse_map.md | 0 .../processamento => docs}/comunicacao_map.md | 0 {pynfe/entidades => docs}/evento_map.md | 0 {pynfe/utils => docs}/flags_map.md | 0 {pynfe/entidades => docs}/manifesto_map.md | 0 {pynfe/entidades => docs}/notafiscal_map.md | 0 .../serializacao_map.md | 0 {pynfe/utils => docs}/utils_map.md | 0 {pynfe/utils => docs}/webservices_map.md | 0 10 files changed, 13 insertions(+), 12 deletions(-) rename CLAUDE.md => AGENTS.md (76%) rename {pynfe/processamento => docs}/autorizador_nfse_map.md (100%) rename {pynfe/processamento => docs}/comunicacao_map.md (100%) rename {pynfe/entidades => docs}/evento_map.md (100%) rename {pynfe/utils => docs}/flags_map.md (100%) rename {pynfe/entidades => docs}/manifesto_map.md (100%) rename {pynfe/entidades => docs}/notafiscal_map.md (100%) rename {pynfe/processamento => docs}/serializacao_map.md (100%) rename {pynfe/utils => docs}/utils_map.md (100%) rename {pynfe/utils => docs}/webservices_map.md (100%) diff --git a/CLAUDE.md b/AGENTS.md similarity index 76% rename from CLAUDE.md rename to AGENTS.md index bc1f2bce..63cc9e21 100644 --- a/CLAUDE.md +++ b/AGENTS.md @@ -1,10 +1,10 @@ -# CLAUDE.md - PyNFe +# AGENTS.md - PyNFe Brazilian electronic fiscal document library (NF-e, NFC-e, NFS-e, MDF-e, CT-e) for SEFAZ webservice communication. ## Source Map Navigation (MANDATORY) -**Before reading any large file (>200 lines), you MUST first read its `{filename}_map.md` file** in the same directory. The source map contains: +**Before reading any large file (>200 lines), you MUST first read its `{filename}_map.md` file** in the `docs/` directory. The source map contains: - Section-by-section breakdown with exact line ranges - Class/method index with purpose descriptions - Field group documentation @@ -15,26 +15,27 @@ This allows you to navigate directly to the specific line-window you need instea | Source Map | File | Lines | Description | |------------|------|-------|-------------| -| `pynfe/processamento/serializacao_map.md` | `serializacao.py` | 2630 | XML serialization (NF-e, MDF-e, QR codes) | -| `pynfe/processamento/comunicacao_map.md` | `comunicacao.py` | 1348 | SEFAZ webservice communication | -| `pynfe/processamento/autorizador_nfse_map.md` | `autorizador_nfse.py` | 538 | NFS-e authorization (Betha/Ginfes) | -| `pynfe/entidades/notafiscal_map.md` | `notafiscal.py` | 1253 | Invoice entities and tax fields | -| `pynfe/entidades/manifesto_map.md` | `manifesto.py` | 447 | MDF-e manifest entities | -| `pynfe/entidades/evento_map.md` | `evento.py` | 237 | Event entities (cancel, correction, etc.) | -| `pynfe/utils/flags_map.md` | `flags.py` | 645 | Constants, namespaces, tax codes | -| `pynfe/utils/webservices_map.md` | `webservices.py` | 572 | SEFAZ endpoint URLs by state | -| `pynfe/utils/utils_map.md` | `__init__.py` | 253 | Utility functions (municipality lookup, signing) | +| `docs/serializacao_map.md` | `pynfe/processamento/serializacao.py` | 2630 | XML serialization (NF-e, MDF-e, QR codes) | +| `docs/comunicacao_map.md` | `pynfe/processamento/comunicacao.py` | 1348 | SEFAZ webservice communication | +| `docs/autorizador_nfse_map.md` | `pynfe/processamento/autorizador_nfse.py` | 538 | NFS-e authorization (Betha/Ginfes) | +| `docs/notafiscal_map.md` | `pynfe/entidades/notafiscal.py` | 1253 | Invoice entities and tax fields | +| `docs/manifesto_map.md` | `pynfe/entidades/manifesto.py` | 447 | MDF-e manifest entities | +| `docs/evento_map.md` | `pynfe/entidades/evento.py` | 237 | Event entities (cancel, correction, etc.) | +| `docs/flags_map.md` | `pynfe/utils/flags.py` | 645 | Constants, namespaces, tax codes | +| `docs/webservices_map.md` | `pynfe/utils/webservices.py` | 572 | SEFAZ endpoint URLs by state | +| `docs/utils_map.md` | `pynfe/utils/__init__.py` | 253 | Utility functions (municipality lookup, signing) | ### How to Use Source Maps 1. **Read the `_map.md` file first** to understand the file structure 2. **Identify the line range** you need from the map tables 3. **Read only that section** using `offset` and `limit` parameters -4. Example: To understand ICMS CST 60 serialization, read `serializacao_map.md`, find it's at lines 747-770, then read `serializacao.py` with `offset=747, limit=25` +4. Example: To understand ICMS CST 60 serialization, read `docs/serializacao_map.md`, find it's at lines 747-770, then read `pynfe/processamento/serializacao.py` with `offset=747, limit=25` ## Project Structure ``` +docs/ # Documentation and source maps (*_map.md, reforma_tributaria.md) pynfe/ ├── entidades/ # Domain entities (data models) │ ├── base.py # Base entity class with kwargs init diff --git a/pynfe/processamento/autorizador_nfse_map.md b/docs/autorizador_nfse_map.md similarity index 100% rename from pynfe/processamento/autorizador_nfse_map.md rename to docs/autorizador_nfse_map.md diff --git a/pynfe/processamento/comunicacao_map.md b/docs/comunicacao_map.md similarity index 100% rename from pynfe/processamento/comunicacao_map.md rename to docs/comunicacao_map.md diff --git a/pynfe/entidades/evento_map.md b/docs/evento_map.md similarity index 100% rename from pynfe/entidades/evento_map.md rename to docs/evento_map.md diff --git a/pynfe/utils/flags_map.md b/docs/flags_map.md similarity index 100% rename from pynfe/utils/flags_map.md rename to docs/flags_map.md diff --git a/pynfe/entidades/manifesto_map.md b/docs/manifesto_map.md similarity index 100% rename from pynfe/entidades/manifesto_map.md rename to docs/manifesto_map.md diff --git a/pynfe/entidades/notafiscal_map.md b/docs/notafiscal_map.md similarity index 100% rename from pynfe/entidades/notafiscal_map.md rename to docs/notafiscal_map.md diff --git a/pynfe/processamento/serializacao_map.md b/docs/serializacao_map.md similarity index 100% rename from pynfe/processamento/serializacao_map.md rename to docs/serializacao_map.md diff --git a/pynfe/utils/utils_map.md b/docs/utils_map.md similarity index 100% rename from pynfe/utils/utils_map.md rename to docs/utils_map.md diff --git a/pynfe/utils/webservices_map.md b/docs/webservices_map.md similarity index 100% rename from pynfe/utils/webservices_map.md rename to docs/webservices_map.md From 6b17dc2ad973ad592e209656e6753dff426d711b Mon Sep 17 00:00:00 2001 From: Felipe Correa Date: Sat, 14 Feb 2026 22:46:48 -0300 Subject: [PATCH 5/7] fix: Correct Reforma Tributaria IBSCBS serialization per NT 2025.002-RTC schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove impostoMisto wrapper — IBSCBS is now direct child of . Unify entity field naming to ibscbs_* pattern with shared vBC. Add IBSCBSTot totals with nested gIBS/gCBS structure, cMunFGIBS in , finNFe 5/6, and IS entity fields ready for 2027. Update docs and source maps. Co-Authored-By: Claude Opus 4.6 --- docs/flags_map.md | 22 +- docs/notafiscal_map.md | 92 +-- docs/reforma_tributaria.md | 389 +++++++---- docs/serializacao_map.md | 144 ++-- pynfe/entidades/notafiscal.py | 69 +- pynfe/processamento/serializacao.py | 153 +++-- pynfe/utils/flags.py | 39 +- ...est_nfe_serializacao_reforma_tributaria.py | 621 ++++++++++-------- 8 files changed, 917 insertions(+), 612 deletions(-) diff --git a/docs/flags_map.md b/docs/flags_map.md index e4296693..c76fc581 100644 --- a/docs/flags_map.md +++ b/docs/flags_map.md @@ -1,4 +1,4 @@ -# Source Map: `flags.py` (671 lines) +# Source Map: `flags.py` (681 lines) Constants, namespaces, tax code enumerations, and state codes used throughout PyNFe. @@ -13,16 +13,16 @@ Constants, namespaces, tax code enumerations, and state codes used throughout Py | ICMS modalities | 125-140 | `ICMS_MODALIDADES` and `ICMS_ST_MODALIDADES` | | ICMS exemption reasons | 142-161 | `ICMS_MOTIVO_DESONERACAO` | | NF-e status | 163-182 | `NF_STATUS` and `MDFE_STATUS` tuples | -| NF-e enumerations | 184-246 | Document types, emission processes, DANFE types, payment forms, emission forms, emission purposes, referenced types, specific products, environments | -| IPI types | 248-268 | `IPI_TIPOS_TRIBUTACAO` and `IPI_TIPOS_CALCULO` | -| PIS types | 270-407 | `PIS_TIPOS_TRIBUTACAO` — all CST codes (01-99) | -| COFINS types | 409-546 | `COFINS_TIPOS_TRIBUTACAO` — all CST codes (01-99) | -| Reforma Tributaria | 548-572 | `IBS_CBS_TIPOS_TRIBUTACAO` and `IS_TIPOS_TRIBUTACAO` — IVA Dual CSTs (EC 132/2023) | -| Freight modalities | 574-581 | `MODALIDADES_FRETE` (0-9) | -| Process origins | 583-589 | `ORIGENS_PROCESSO` | -| State codes | 591-623 | `CODIGOS_ESTADOS` — UF to IBGE code mapping | -| Card brands | 625-654 | `BANDEIRA_CARTAO` (01-99) | -| Payment methods | 656-671 | `FORMAS_PAGAMENTO` (01-99) | +| NF-e enumerations | 184-248 | Document types, emission processes, DANFE types, payment forms, emission forms, **emission purposes (incl. finNFe 5/6)**, referenced types, specific products, environments | +| IPI types | 250-270 | `IPI_TIPOS_TRIBUTACAO` and `IPI_TIPOS_CALCULO` | +| PIS types | 272-407 | `PIS_TIPOS_TRIBUTACAO` — all CST codes (01-99) | +| COFINS types | 411-548 | `COFINS_TIPOS_TRIBUTACAO` — all CST codes (01-99) | +| Reforma Tributaria | 550-582 | `IBS_CBS_TIPOS_TRIBUTACAO` (15 CSTs, 3-digit) and `IS_TIPOS_TRIBUTACAO` (7 CSTs, 2-digit) — IVA Dual (EC 132/2023) | +| Freight modalities | 584-591 | `MODALIDADES_FRETE` (0-9) | +| Process origins | 593-599 | `ORIGENS_PROCESSO` | +| State codes | 601-633 | `CODIGOS_ESTADOS` — UF to IBGE code mapping | +| Card brands | 635-664 | `BANDEIRA_CARTAO` (01-99) | +| Payment methods | 666-681 | `FORMAS_PAGAMENTO` (01-99) | ## Key Constants diff --git a/docs/notafiscal_map.md b/docs/notafiscal_map.md index 00701d24..fcf8c0a9 100644 --- a/docs/notafiscal_map.md +++ b/docs/notafiscal_map.md @@ -1,4 +1,4 @@ -# Source Map: `notafiscal.py` (1280 lines) +# Source Map: `notafiscal.py` (1303 lines) Entity models for NF-e/NFC-e invoices and related objects (products, taxes, payments, transport, etc.). @@ -6,25 +6,25 @@ Entity models for NF-e/NFC-e invoices and related objects (products, taxes, paym | Class | Lines | Purpose | |-------|-------|---------| -| `NotaFiscal` | 14-580 | Main invoice entity | -| `NotaFiscalReferenciada` | 583-611 | Referenced invoice | -| `NotaFiscalProduto` | 614-1046 | Product/service item with all tax fields | -| `NotaFiscalDeclaracaoImportacao` | 1049-1094 | Import declaration | -| `NotaFiscalDeclaracaoImportacaoAdicao` | 1097-1109 | Import declaration addition | -| `NotaFiscalTransporteVolume` | 1111-1140 | Transport volume | -| `NotaFiscalTransporteVolumeLacre` | 1143-1145 | Volume seal | -| `NotaFiscalCobrancaDuplicata` | 1148-1156 | Billing duplicate | -| `NotaFiscalObservacaoContribuinte` | 1159-1164 | Taxpayer note | -| `NotaFiscalProcessoReferenciado` | 1167-1177 | Referenced process | -| `NotaFiscalEntregaRetirada` | 1180-1217 | Delivery/withdrawal address | -| `NotaFiscalServico` | 1219-1247 | NFS-e service invoice | -| `NotaFiscalResponsavelTecnico` | 1250-1256 | Technical responsible (NT2018/003) | -| `AutorizadosBaixarXML` | 1259-1260 | Authorized XML downloaders | -| `NotaFiscalPagamentos` | 1263-1280 | Payment details | +| `NotaFiscal` | 14-593 | Main invoice entity | +| `NotaFiscalReferenciada` | 595-624 | Referenced invoice | +| `NotaFiscalProduto` | 626-1071 | Product/service item with all tax fields | +| `NotaFiscalDeclaracaoImportacao` | 1073-1119 | Import declaration | +| `NotaFiscalDeclaracaoImportacaoAdicao` | 1121-1133 | Import declaration addition | +| `NotaFiscalTransporteVolume` | 1135-1164 | Transport volume | +| `NotaFiscalTransporteVolumeLacre` | 1167-1169 | Volume seal | +| `NotaFiscalCobrancaDuplicata` | 1172-1180 | Billing duplicate | +| `NotaFiscalObservacaoContribuinte` | 1183-1188 | Taxpayer note | +| `NotaFiscalProcessoReferenciado` | 1191-1201 | Referenced process | +| `NotaFiscalEntregaRetirada` | 1204-1240 | Delivery/withdrawal address | +| `NotaFiscalServico` | 1243-1271 | NFS-e service invoice | +| `NotaFiscalResponsavelTecnico` | 1274-1280 | Technical responsible (NT2018/003) | +| `AutorizadosBaixarXML` | 1283-1284 | Authorized XML downloaders | +| `NotaFiscalPagamentos` | 1287-1303 | Payment details | --- -## `NotaFiscal` — Lines 14-572 +## `NotaFiscal` — Lines 14-593 ### Field Groups | Section | Lines | Fields | @@ -34,48 +34,48 @@ Entity models for NF-e/NFC-e invoices and related objects (products, taxes, paym | Read-only fields | 128-144 | digest_value, valor_total_nota, valor_icms_nota, valor_icms_st_nota, protocolo, data | | Relationships | 146-173 | notas_fiscais_referenciadas, emitente, destinatario_remetente, entrega, retirada, autorizados_baixar_xml, produtos_e_servicos | | ICMS totals | 175-303 | totais_icms_base_calculo, totais_icms_total, totais_icms_desonerado, totais_icms_st_*, totais_icms_total_*, totais_fcp_*, totais_icms_inter_*, totais_icms_*_mono_* | -| Reforma Tributaria totals | 305-311 | totais_cbs, totais_ibs, totais_is — IVA Dual totals (EC 132/2023) | -| Transport | 313-369 | transporte_modalidade_frete, transporte_transportadora, transporte_retencao_icms_*, transporte_veiculo_*, transporte_reboque_*, transporte_volumes | -| Billing | 363-378 | fatura_numero, fatura_valor_original, fatura_valor_desconto, fatura_valor_liquido, duplicatas | -| Additional info | 380-397 | informacoes_adicionais_*, observacoes_contribuinte, processos_referenciados, pagamentos, valor_troco | +| **Reforma Tributaria totals** | 305-314 | totais_vbc_ibscbs, totais_ibs_uf, totais_ibs_mun, totais_ibs, totais_cbs, totais_is, **municipio_fato_gerador_ibs** — IVA Dual totals + Group B field (EC 132/2023) | +| Transport | 317-372 | transporte_modalidade_frete, transporte_transportadora, transporte_retencao_icms_*, transporte_veiculo_*, transporte_reboque_*, transporte_volumes | +| Billing | 374-389 | fatura_numero, fatura_valor_original, fatura_valor_desconto, fatura_valor_liquido, duplicatas | +| Additional info | 391-408 | informacoes_adicionais_*, observacoes_contribuinte, processos_referenciados, pagamentos, valor_troco | ### Methods | Method | Lines | Purpose | |--------|-------|---------| -| `__init__()` | 399-410 | Initialize all list fields | -| `adicionar_pagamento()` | 415-419 | Add payment | -| `adicionar_autorizados_baixar_xml()` | 421-424 | Add authorized XML downloader | -| `adicionar_nota_fiscal_referenciada()` | 426-430 | Add referenced invoice | -| `adicionar_produto_servico()` | 432-484 | **Add product** — also accumulates all ICMS/tax totals | -| `adicionar_transporte_volume()` | 486-490 | Add transport volume | -| `adicionar_duplicata()` | 492-496 | Add billing duplicate | -| `adicionar_observacao_contribuinte()` | 498-502 | Add taxpayer note | -| `adicionar_processo_referenciado()` | 504-508 | Add referenced process | -| `adicionar_responsavel_tecnico()` | 510-514 | Add technical responsible | -| `_codigo_numerico_aleatorio()` | 516-519 | Generate random 8-digit code | -| `_dv_codigo_numerico()` | 521-543 | Calculate check digit (mod 11) | -| `identificador_unico` (property) | 545-572 | Build 44-char NF-e access key | +| `__init__()` | 410-421 | Initialize all list fields | +| `adicionar_pagamento()` | 426-430 | Add payment | +| `adicionar_autorizados_baixar_xml()` | 432-435 | Add authorized XML downloader | +| `adicionar_nota_fiscal_referenciada()` | 437-441 | Add referenced invoice | +| `adicionar_produto_servico()` | 443-504 | **Add product** — accumulates all ICMS/tax totals + **IBSCBS/IS totals** (vNF excludes IBS/CBS/IS) | +| `adicionar_transporte_volume()` | 506-510 | Add transport volume | +| `adicionar_duplicata()` | 512-516 | Add billing duplicate | +| `adicionar_observacao_contribuinte()` | 518-522 | Add taxpayer note | +| `adicionar_processo_referenciado()` | 524-528 | Add referenced process | +| `adicionar_responsavel_tecnico()` | 530-534 | Add technical responsible | +| `_codigo_numerico_aleatorio()` | 536-539 | Generate random 8-digit code | +| `_dv_codigo_numerico()` | 541-563 | Calculate check digit (mod 11) | +| `identificador_unico` (property) | 565-593 | Build 44-char NF-e access key | --- -## `NotaFiscalProduto` — Lines 606-1019 +## `NotaFiscalProduto` — Lines 626-1071 ### Field Groups | Section | Lines | Fields | |---------|-------|--------| -| Product data | 612-686 | codigo, descricao, ean, ncm, cfop, unidade_comercial, quantidade_comercial, valor_unitario_comercial, unidade_tributavel, cbenef, quantidade_tributavel, valor_unitario_tributavel, ean_tributavel, total_frete, total_seguro, desconto, outras_despesas_acessorias, valor_total_bruto, numero_pedido, numero_item | -| Fuel data | 688-733 | cProdANP, descANP, pGLP, pGNn, pGNi, vPart, UFCons, comb_codif, comb_q_temp, comb_n_bico, comb_n_bomba, comb_n_tanque, comb_v_enc_ini, comb_v_enc_fin, comb_p_bio | -| ICMS fields | 735-826 | icms_modalidade, icms_origem, icms_modalidade_determinacao_bc, icms_percentual_reducao_bc, icms_valor_base_calculo, icms_aliquota, icms_valor, icms_desonerado, icms_motivo_desoneracao, icms_st_*, fcp_*, icms_inter_*, icms_st_ret_*, icms_*_mono_* | -| IPI fields | 828-877 | ipi_situacao_tributaria, ipi_classe_enquadramento, ipi_codigo_enquadramento, ipi_valor_base_calculo, ipi_aliquota, ipi_valor_ipi, pdevol, ipi_valor_ipi_dev | -| PIS fields | 879-923 | pis_modalidade (via pis_situacao_tributaria), pis_valor_base_calculo, pis_aliquota_percentual, pis_aliquota_reais, pis_valor, pis_st_* | -| COFINS fields | 925-969 | cofins_modalidade, cofins_valor_base_calculo, cofins_aliquota_percentual, cofins_aliquota_reais, cofins_valor, cofins_st_* | -| ISSQN fields | 971-990 | issqn_valor_base_calculo, issqn_aliquota, issqn_lista_servico, issqn_uf, issqn_municipio, issqn_valor | -| Import tax fields | 992-1003 | imposto_importacao_valor_base_calculo, imposto_importacao_valor_despesas_aduaneiras, imposto_importacao_valor_iof, imposto_importacao_valor | -| Reforma Tributaria fields | 1005-1028 | cbs_situacao_tributaria, cbs_valor_base_calculo, cbs_aliquota, cbs_valor, ibs_situacao_tributaria, ibs_valor_base_calculo, ibs_aliquota, ibs_valor, ibs_codigo_municipio_destino, is_situacao_tributaria, is_valor_base_calculo, is_aliquota, is_valor | -| Additional info | 1030-1046 | informacoes_adicionais, declaracoes_importacao | +| Product data | 632-706 | codigo, descricao, ean, ncm, cfop, unidade_comercial, quantidade_comercial, valor_unitario_comercial, unidade_tributavel, cbenef, quantidade_tributavel, valor_unitario_tributavel, ean_tributavel, total_frete, total_seguro, desconto, outras_despesas_acessorias, valor_total_bruto, numero_pedido, numero_item | +| Fuel data | 708-753 | cProdANP, descANP, pGLP, pGNn, pGNi, vPart, UFCons, comb_codif, comb_q_temp, comb_n_bico, comb_n_bomba, comb_n_tanque, comb_v_enc_ini, comb_v_enc_fin, comb_p_bio | +| ICMS fields | 755-846 | icms_modalidade, icms_origem, icms_modalidade_determinacao_bc, icms_percentual_reducao_bc, icms_valor_base_calculo, icms_aliquota, icms_valor, icms_desonerado, icms_motivo_desoneracao, icms_st_*, fcp_*, icms_inter_*, icms_st_ret_*, icms_*_mono_* | +| IPI fields | 848-897 | ipi_situacao_tributaria, ipi_classe_enquadramento, ipi_codigo_enquadramento, ipi_valor_base_calculo, ipi_aliquota, ipi_valor_ipi, pdevol, ipi_valor_ipi_dev | +| PIS fields | 899-943 | pis_modalidade (via pis_situacao_tributaria), pis_valor_base_calculo, pis_aliquota_percentual, pis_aliquota_reais, pis_valor, pis_st_* | +| COFINS fields | 945-989 | cofins_modalidade, cofins_valor_base_calculo, cofins_aliquota_percentual, cofins_aliquota_reais, cofins_valor, cofins_st_* | +| ISSQN fields | 991-1010 | issqn_valor_base_calculo, issqn_aliquota, issqn_lista_servico, issqn_uf, issqn_municipio, issqn_valor | +| Import tax fields | 1012-1023 | imposto_importacao_valor_base_calculo, imposto_importacao_valor_despesas_aduaneiras, imposto_importacao_valor_iof, imposto_importacao_valor | +| **Reforma Tributaria fields** | 1025-1054 | **IBSCBS**: ibscbs_cst, ibscbs_c_class_trib, ibscbs_vbc, ibscbs_p_ibs_uf, ibscbs_v_ibs_uf, ibscbs_p_ibs_mun, ibscbs_v_ibs_mun, ibscbs_v_ibs, ibscbs_p_cbs, ibscbs_v_cbs; **IS**: is_cst_selec, is_c_class_trib, is_vbc, is_aliquota, is_valor | +| Additional info | 1056-1071 | informacoes_adicionais, declaracoes_importacao | --- -## Supporting Entities (Lines 1022-1253) +## Supporting Entities (Lines 1073-1303) Small entity classes for nested data — see class table above for line ranges. All extend `Entidade` base class and use kwargs initialization. diff --git a/docs/reforma_tributaria.md b/docs/reforma_tributaria.md index c8f11ab2..cae67eb2 100644 --- a/docs/reforma_tributaria.md +++ b/docs/reforma_tributaria.md @@ -1,40 +1,48 @@ -# Reforma Tributaria - IBS/CBS/IS (EC 132/2023) +# Reforma Tributaria - IBS/CBS (NT 2025.002-RTC) -Suporte ao IVA Dual brasileiro introduzido pela Emenda Constitucional 132/2023. +Suporte ao IVA Dual brasileiro conforme a Emenda Constitucional 132/2023, Lei Complementar 214/2025 e Nota Tecnica NT 2025.002-RTC. ## Contexto -A Reforma Tributaria brasileira substitui gradualmente cinco tributos por tres novos impostos: +A Reforma Tributaria substitui gradualmente cinco tributos por um modelo de IVA Dual: -| Imposto | Tipo | Substitui | Competencia | -|---------|------|-----------|-------------| -| **CBS** (Contribuicao sobre Bens e Servicos) | Federal | PIS, COFINS | Uniao | -| **IBS** (Imposto sobre Bens e Servicos) | Subnacional | ICMS, ISS | Estados + Municipios | -| **IS** (Imposto Seletivo) | Extrafiscal | IPI (parcial) | Uniao | +| Imposto | Tipo | Substitui | Competencia | Inicio | +|---------|------|-----------|-------------|--------| +| **CBS** (Contribuicao sobre Bens e Servicos) | Federal | PIS, COFINS | Uniao | 2026 (teste 0,9%) | +| **IBS** (Imposto sobre Bens e Servicos) | Subnacional | ICMS, ISS | Estados + Municipios | 2026 (teste 0,1% UF) | +| **IS** (Imposto Seletivo) | Extrafiscal | IPI (parcial) | Uniao | **2027** | + +> **IBS e dividido em duas parcelas**: IBS-UF (estadual) e IBS-Mun (municipal), informadas separadamente no XML. ### Cronograma de transicao -- **2026**: Fase de testes (aliquotas reduzidas CBS 0.9%, IBS 0.1%) -- **2027-2028**: CBS integral, IBS em transicao -- **2029-2032**: Reducao gradual de ICMS/ISS -- **2033**: Extincao completa de ICMS/ISS +- **2026**: Fase de testes — CBS 0,9%, IBS-UF 0,1%, IBS-Mun 0%. Obrigatorio para CRT=3. +- **2027**: CBS em aliquota plena. IS entra em vigor. Split Payment inicia. +- **2029-2032**: Reducao gradual de ICMS/ISS, aumento proporcional do IBS. +- **2033**: Extincao completa de ICMS, ISS, PIS, COFINS e IPI. -Durante a transicao, os impostos legados (ICMS, PIS, COFINS) **coexistem** com os novos (IBS, CBS, IS) na mesma NF-e. +Durante a transicao, os impostos legados (ICMS, PIS, COFINS) **coexistem** com os novos (IBS, CBS) na mesma NF-e. ## Escopo da implementacao -A implementacao atual cobre a **infraestrutura XML basica**: +A implementacao cobre: -- Campos de entidade (produto e nota fiscal) -- Serializacao XML (grupo `` contendo ``, ``, ``) -- Acumulacao de totais -- CSTs (Codigos de Situacao Tributaria) para IBS, CBS e IS +- Grupo `` como filho direto de `` (sem wrapper ``) +- CSTs de 3 digitos conforme NT 2025.002-RTC (ex: "000", "010", "222") +- `cClassTrib` (codigo de classificacao tributaria de 6 digitos) +- IBS dividido em `gIBSUF` e `gIBSMun` +- Base de calculo compartilhada (`vBC`) para IBS e CBS +- Totais em `` separado de `` +- `cMunFGIBS` no cabecalho (Group B) +- `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) -**Nao inclui** (ainda): Split Payment, cashback, helpers de calculo da transicao, validacao XSD (schemas oficiais ainda nao publicados pela SEFAZ). +**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`). ## CSTs disponiveis -### IBS e CBS (compartilham a mesma tabela) +### IBS e CBS — CST de 3 digitos (compartilham a mesma tabela) ```python from pynfe.utils.flags import IBS_CBS_TIPOS_TRIBUTACAO @@ -42,37 +50,49 @@ from pynfe.utils.flags import IBS_CBS_TIPOS_TRIBUTACAO | CST | Descricao | |-----|-----------| -| 01 | Tributada Integralmente | -| 02 | Tributada com Reducao (Cesta Basica/Saude) | -| 03 | Isencao | -| 04 | Imunidade | -| 05 | Suspensao | -| 51 | Diferimento | -| 70 | Monofasica (Combustiveis) | - -### IS (Imposto Seletivo) +| 000 | Tributacao integral | +| 010 | Tributacao com aliquota reduzida | +| 100 | Tributacao com suspensao | +| 110 | Imunidade — exportacao | +| 200 | Tributacao com diferimento | +| 222 | Isencao | +| 300 | Nao incidencia | +| 400 | Tributacao por substituicao | +| 410 | Tributacao com aliquota zero | +| 510 | Tributacao integral — regime especifico | +| 600 | Tributacao monofasica — incidencia padrao | +| 620 | Tributacao monofasica — demais operacoes | +| 800 | Credito presumido | +| 810 | Credito presumido — ZFM | +| 900 | Outros | + +### IS (Imposto Seletivo) — CSTIS de 2 digitos ```python from pynfe.utils.flags import IS_TIPOS_TRIBUTACAO ``` -| CST | Descricao | -|-----|-----------| -| 01 | Tributada Integralmente | -| 02 | Tributada com Reducao | +| CSTIS | Descricao | +|-------|-----------| +| 01 | Tributada integralmente | +| 02 | Tributada com reducao | | 03 | Isencao | | 04 | Imunidade | | 05 | Suspensao | +| 06 | Diferimento | +| 90 | Outros | + +> **IS nao e serializado no XML em 2026.** O schema SEFAZ (PL 010b) inclui o tipo `TIS` com tags `CSTIS`, `cClassTribIS`, `vBCIS`, `pIS`, `vIS`, porem a serializacao esta desabilitada ate 2027. Os campos de entidade estao prontos e serao ativados quando necessario. ## Como usar -### Exemplo basico: produto com CBS + IBS + IS +### Exemplo: produto com IBSCBS (tributacao integral CST 000) ```python from decimal import Decimal nota_fiscal.adicionar_produto_servico( - # ... campos obrigatorios do produto (codigo, descricao, ncm, cfop, etc.) + # ... campos obrigatorios do produto ... codigo="001", descricao="Produto exemplo", ncm="99999999", @@ -88,7 +108,7 @@ nota_fiscal.adicionar_produto_servico( ean_tributavel="SEM GTIN", ind_total=1, - # ... impostos legados (ICMS, PIS, COFINS) continuam obrigatorios + # ... impostos legados (ICMS, PIS, COFINS) continuam obrigatorios ... icms_modalidade="00", icms_origem=0, icms_csosn="", @@ -97,87 +117,114 @@ nota_fiscal.adicionar_produto_servico( pis_valor=Decimal("0.00"), cofins_valor=Decimal("0.00"), - # CBS - Contribuicao sobre Bens e Servicos (Federal) - cbs_situacao_tributaria="01", # CST 01 = Tributada integralmente - cbs_valor_base_calculo=Decimal("1000.00"), - cbs_aliquota=Decimal("8.8000"), # 4 casas decimais - cbs_valor=Decimal("88.00"), - - # IBS - Imposto sobre Bens e Servicos (Estadual/Municipal) - ibs_situacao_tributaria="01", - ibs_valor_base_calculo=Decimal("1000.00"), - ibs_aliquota=Decimal("17.7000"), - ibs_valor=Decimal("177.00"), - ibs_codigo_municipio_destino="4118402", # Codigo IBGE do municipio destino - - # IS - Imposto Seletivo (opcional, apenas para produtos sujeitos) - is_situacao_tributaria="01", - is_valor_base_calculo=Decimal("1000.00"), - is_aliquota=Decimal("1.0000"), - is_valor=Decimal("10.00"), + # IBSCBS — Group UB + ibscbs_cst="000", # CST 3 digitos + ibscbs_c_class_trib="000001", # cClassTrib 6 digitos + ibscbs_vbc=Decimal("1000.00"), # Base de calculo compartilhada IBS+CBS + ibscbs_p_ibs_uf=Decimal("0.1000"), # Aliquota IBS UF (4 casas) + ibscbs_v_ibs_uf=Decimal("1.00"), # Valor IBS UF + ibscbs_p_ibs_mun=Decimal("0.0000"), # Aliquota IBS Municipal + ibscbs_v_ibs_mun=Decimal("0.00"), # Valor IBS Municipal + ibscbs_v_ibs=Decimal("1.00"), # Valor total IBS (UF + Mun) + ibscbs_p_cbs=Decimal("0.9000"), # Aliquota CBS (4 casas) + ibscbs_v_cbs=Decimal("9.00"), # Valor CBS + + # IS — armazenado na entidade mas NAO serializado no XML (2026) + # is_cst_selec="01", + # is_c_class_trib="010001", + # is_vbc=Decimal("1000.00"), + # is_aliquota=Decimal("1.0000"), + # is_valor=Decimal("10.00"), ) ``` -### Exemplo: produto isento (CBS + IBS com CST 03) +### Exemplo: produto isento (CST 222) ```python nota_fiscal.adicionar_produto_servico( # ... campos do produto ... - # CBS isenta - apenas o CST, sem valores - cbs_situacao_tributaria="03", - - # IBS isenta - ibs_situacao_tributaria="03", - - # IS nao se aplica (nao informar = nao serializado) + # IBSCBS isento — apenas CST e cClassTrib, sem valores + ibscbs_cst="222", + ibscbs_c_class_trib="000002", ) ``` ### Exemplo: sem reforma tributaria (compatibilidade retroativa) -Se nenhum campo de IBS/CBS/IS for informado, o grupo `` **nao e emitido** no XML. Isso garante compatibilidade total com NF-e que nao precisam dos novos impostos. +Se nenhum campo `ibscbs_*` for informado, o grupo `` **nao e emitido** no XML. Isso garante compatibilidade total com NF-e que nao precisam dos novos impostos. ```python -# Produto sem campos de reforma tributaria = XML identico ao anterior +# Produto sem campos de reforma = XML identico ao anterior nota_fiscal.adicionar_produto_servico( # ... apenas campos legados (ICMS, PIS, COFINS) ... ) ``` +### cMunFGIBS no cabecalho + +```python +nota_fiscal = NotaFiscal( + # ... demais campos ... + municipio_fato_gerador_ibs="4118402", # Codigo IBGE — gera no +) +``` + ## Campos disponiveis ### Campos do produto (`adicionar_produto_servico`) +#### IBSCBS (Group UB) + | Campo | Tipo | Descricao | |-------|------|-----------| -| `cbs_situacao_tributaria` | `str` | CST da CBS (ex: "01", "03") | -| `cbs_valor_base_calculo` | `Decimal` | Base de calculo da CBS | -| `cbs_aliquota` | `Decimal` | Aliquota CBS (4 casas decimais) | -| `cbs_valor` | `Decimal` | Valor da CBS | -| `ibs_situacao_tributaria` | `str` | CST do IBS (ex: "01", "03") | -| `ibs_valor_base_calculo` | `Decimal` | Base de calculo do IBS | -| `ibs_aliquota` | `Decimal` | Aliquota IBS (4 casas decimais) | -| `ibs_valor` | `Decimal` | Valor do IBS | -| `ibs_codigo_municipio_destino` | `str` | Codigo IBGE do municipio destino | -| `is_situacao_tributaria` | `str` | CST do IS (ex: "01", "02") | -| `is_valor_base_calculo` | `Decimal` | Base de calculo do IS | -| `is_aliquota` | `Decimal` | Aliquota IS (4 casas decimais) | -| `is_valor` | `Decimal` | Valor do IS | +| `ibscbs_cst` | `str` | CST 3 digitos (ex: "000", "222") | +| `ibscbs_c_class_trib` | `str` | cClassTrib 6 digitos (classificacao tributaria) | +| `ibscbs_vbc` | `Decimal` | Base de calculo compartilhada IBS + CBS | +| `ibscbs_p_ibs_uf` | `Decimal` | Aliquota IBS estadual (4 casas decimais) | +| `ibscbs_v_ibs_uf` | `Decimal` | Valor IBS estadual | +| `ibscbs_p_ibs_mun` | `Decimal` | Aliquota IBS municipal (4 casas decimais) | +| `ibscbs_v_ibs_mun` | `Decimal` | Valor IBS municipal | +| `ibscbs_v_ibs` | `Decimal` | Valor total IBS (UF + Mun) | +| `ibscbs_p_cbs` | `Decimal` | Aliquota CBS (4 casas decimais) | +| `ibscbs_v_cbs` | `Decimal` | Valor CBS | + +#### IS (Imposto Seletivo) — armazenado, nao serializado em 2026 + +| Campo | XML Tag | Descricao | +|-------|---------|-----------| +| `is_cst_selec` | `CSTIS` | CSTIS 2 digitos (ex: "01", "02") | +| `is_c_class_trib` | `cClassTribIS` | cClassTribIS 6 digitos | +| `is_vbc` | `vBCIS` | Base de calculo IS | +| `is_aliquota` | `pIS` | Aliquota IS (4 casas decimais) | +| `is_valor` | `vIS` | Valor IS | ### Totais da nota fiscal (acumulados automaticamente) -| Campo | Descricao | -|-------|-----------| -| `totais_cbs` | Soma de `cbs_valor` de todos os produtos | -| `totais_ibs` | Soma de `ibs_valor` de todos os produtos | -| `totais_is` | Soma de `is_valor` de todos os produtos | +| Campo | XML Path em `IBSCBSTot` | Descricao | +|-------|-------------------------|-----------| +| `totais_vbc_ibscbs` | `vBCIBSCBS` | Soma de `ibscbs_vbc` de todos os produtos | +| `totais_ibs_uf` | `gIBS/gIBSUF/vIBSUF` | Soma de `ibscbs_v_ibs_uf` de todos os produtos | +| `totais_ibs_mun` | `gIBS/gIBSMun/vIBSMun` | Soma de `ibscbs_v_ibs_mun` de todos os produtos | +| `totais_ibs` | `gIBS/vIBS` | Soma de `ibscbs_v_ibs` de todos os produtos | +| `totais_cbs` | `gCBS/vCBS` | Soma de `ibscbs_v_cbs` de todos os produtos | +| `totais_is` | *(nao emitido)* | Soma de `is_valor` — acumulado internamente, nao no XML | + +Os totais sao acumulados automaticamente ao chamar `adicionar_produto_servico()`. -Os totais sao acumulados automaticamente ao chamar `adicionar_produto_servico()`. Os valores de IBS, CBS e IS tambem sao somados ao `totais_icms_total_nota` (vNF), pois sao impostos "por fora". +> **IMPORTANTE**: IBS/CBS/IS **NAO sao somados** ao `vNF` (proibido em 2025-2026 conforme NT 2025.002-RTC). + +### Campo do cabecalho + +| Campo | XML Tag | Descricao | +|-------|---------|-----------| +| `municipio_fato_gerador_ibs` | `cMunFGIBS` | Codigo IBGE do municipio do fato gerador IBS (7 digitos) | ## Estrutura XML gerada -O grupo `` e adicionado dentro de ``, apos `` e antes de ``: +### Item — dentro de `` + +O grupo `` e adicionado como filho direto de ``, apos ``: ```xml @@ -186,57 +233,161 @@ O grupo `` e adicionado dentro de ``, apos `` e a ... ... ... - - - 01 + + 000 + 000001 + 1000.00 - 8.8000 - 88.00 - - - 01 - 1000.00 - 17.7000 - 177.00 - 4118402 - - - 01 - 1000.00 - 1.0000 - 10.00 - - + + 0.1000 + 1.00 + + + 0.0000 + 0.00 + + 1.00 + + 0.9000 + 9.00 + + + ``` -Os totais aparecem dentro de ``: +Para CSTs nao tributados (ex: 222 — isencao): + +```xml + + 222 + 000002 + +``` + +### Totais — Group W03 (``) + +Os totais ficam em um grupo **separado** de ``, como irmao dentro de ``. O tipo XSD e `TIBSCBSMonoTot` (definido em `DFeTiposBasicos_v1.00.xsd`). ```xml - - 1275.00 - 88.00 - 177.00 - 10.00 + + 1250.00 + + 1000.00 + + + 0.00 + 0.00 + 1.00 + + + 0.00 + 0.00 + 0.50 + + 1.50 + 0.00 + 0.00 + + + 0.00 + 0.00 + 9.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). + +### Cabecalho — `cMunFGIBS` no `` + +```xml + + + 4118402 + 4118402 + 1 + + +``` + ## Regras de serializacao -- **CSTs 01, 02, 51**: Serializam `vBC`, aliquota e valor (campos completos) -- **CSTs 03, 04, 05, 70**: Serializam apenas o `CST` (sem valores) -- **IS CSTs 01, 02**: Serializam campos completos; demais apenas `CST` -- **`cMunDest`** (IBS): Serializado apenas quando informado -- **Totais**: Emitidos apenas quando o valor e maior que zero -- **`impostoMisto`**: Omitido completamente se nenhum dos tres impostos for informado +### CSTs tributados (emitem `gIBSCBS` com valores) + +CSTs: 000, 010, 200, 400, 510, 600, 620, 800, 810, 900 + +Esses CSTs geram o subgrupo `` completo com `vBC`, `gIBSUF`, `gIBSMun`, `vIBS` e `gCBS`. + +### CSTs nao tributados (apenas CST + cClassTrib) + +CSTs: 100, 110, 222, 300, 410 + +Esses CSTs geram apenas `` e ``, sem ``. + +### Outras regras + +- **`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 `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) +- **`vNF`**: NAO inclui valores de IBS, CBS ou IS (proibido em 2025-2026) + +## IS (Imposto Seletivo) — preparado para 2027 + +O IS esta implementado nas entidades mas desabilitado na serializacao: + +- **Entidade** (`NotaFiscalProduto`): campos `is_cst_selec`, `is_c_class_trib`, `is_vbc`, `is_aliquota`, `is_valor` — podem ser preenchidos normalmente +- **Totais** (`NotaFiscal`): `totais_is` e acumulado internamente +- **Serializacao**: `_serializar_is()` existe e usa as tags corretas do schema (`CSTIS`, `cClassTribIS`, `vBCIS`, `pIS`, `vIS`) mas nao e chamado +- **XML**: nao emite `` nem `` + +Quando a serializacao do IS for ativada (previsto para 2027): +1. Descomentar a chamada em `_serializar_imposto_ibscbs()` +2. Adicionar serializacao de `` em `_serializar_nota_fiscal()` (irmao de ``, antes dele) +3. Atualizar os testes + +## Referencia tecnica + +| Recurso | Descricao | +|---------|-----------| +| NT 2025.002-RTC (v1.30+) | Nota Tecnica com alteracoes nos layouts XML | +| PL 010b | Pacote de schemas XML (`leiauteNFe_v4.00.xsd` + `DFeTiposBasicos_v1.00.xsd`) | +| LC 214/2025 | Lei Complementar que regulamenta a reforma | +| [Tabela cClassTrib](https://dfe-portal.svrs.rs.gov.br/DFE/TabelaClassificacaoTributaria) | Tabela oficial de classificacao tributaria (JSON disponivel) | +| [Tabela cCredPres](https://dfe-portal.svrs.rs.gov.br/DFE/TabelaCreditoPresumido) | Tabela de credito presumido | + +### Tipos XSD relevantes (DFeTiposBasicos_v1.00.xsd) + +| Tipo XSD | Elemento XML | Uso | +|----------|-------------|-----| +| `TTribNFe` | `IBSCBS` | Per-item: CST, cClassTrib, choice(gIBSCBS/gIBSCBSMono/...) | +| `TCIBS` | `gIBSCBS` | Per-item: vBC, gIBSUF, gIBSMun, vIBS, gCBS | +| `TIS` | `IS` | Per-item: CSTIS, cClassTribIS, vBCIS, pIS, vIS | +| `TIBSCBSMonoTot` | `IBSCBSTot` | Totais: vBCIBSCBS, gIBS, gCBS, gMono, gEstornoCred | +| `TISTot` | `ISTot` | Totais: vIS | +| `TDif` | `gDif` | Diferimento (pDif, vDif) | +| `TDevTrib` | `gDevTrib` | Devolucao de tributo (vDevTrib) | +| `TRed` | `gRed` | Reducao de aliquota (pRedAliq, pAliqEfet) | +| `TCredPres` | `gCredPresOper` | Credito presumido (pCredPres, vCredPres/vCredPresCondSus) | ## Notas importantes -- Os nomes das tags XML (`impostoMisto`, `CBS`, `IBS`, `IS`) sao baseados na especificacao preliminar. Podem ser renomeados quando a SEFAZ publicar a Nota Tecnica oficial. -- A validacao XSD e ignorada para os novos grupos, pois os schemas oficiais ainda nao existem. -- Durante a fase de transicao (2026-2032), os impostos legados e os novos coexistem no mesmo XML. +- Os nomes das tags XML seguem estritamente os tipos XSD do PL 010b (`DFeTiposBasicos_v1.00.xsd`): + - Per-item: `TTribNFe` (`IBSCBS`), `TCIBS` (`gIBSCBS`), `TIS` (`IS`) + - Totais: `TIBSCBSMonoTot` (`IBSCBSTot`), `TISTot` (`ISTot`) - O calculo dos valores (base de calculo, aliquota, valor) deve ser feito pela aplicacao consumidora. PyNFe apenas serializa os valores informados. +- Durante a fase de transicao (2026-2033), os impostos legados e os novos coexistem no mesmo XML. +- Aliquotas de teste para 2026: CBS=0,9%, IBS-UF=0,1%, IBS-Mun=0%. +- Campos de diferimento (`vDif`), devolucao de tributos (`vDevTrib`) e credito presumido (`vCredPres`, `vCredPresCondSus`) sao emitidos como "0.00" nos totais. Suporte completo para esses campos sera adicionado conforme necessidade. diff --git a/docs/serializacao_map.md b/docs/serializacao_map.md index 86fa8a4e..f4594710 100644 --- a/docs/serializacao_map.md +++ b/docs/serializacao_map.md @@ -1,4 +1,4 @@ -# Source Map: `serializacao.py` (2740 lines) +# Source Map: `serializacao.py` (2771 lines) XML serialization of NF-e, NFC-e, NFS-e and MDF-e documents into SEFAZ-compliant XML format. @@ -7,11 +7,11 @@ XML serialization of NF-e, NFC-e, NFS-e and MDF-e documents into SEFAZ-compliant | Class | Lines | Purpose | |-------|-------|---------| | `Serializacao` | 30-63 | Abstract base class (not instantiable directly) | -| `SerializacaoXML` | 66-1829 | Main NF-e/NFC-e XML serialization | -| `SerializacaoQrcode` | 2070-2174 | NFC-e QR Code generation | -| `SerializacaoNfse` | 2177-2243 | NFS-e serialization (Betha/Ginfes) | -| `SerializacaoQrcodeMDFe` | 2246-2269 | MDF-e QR Code generation | -| `SerializacaoMDFe` | 2272-2740 | MDF-e XML serialization | +| `SerializacaoXML` | 66-1860 | Main NF-e/NFC-e XML serialization | +| `SerializacaoQrcode` | 2102-2206 | NFC-e QR Code generation | +| `SerializacaoNfse` | 2209-2275 | NFS-e serialization (Betha/Ginfes) | +| `SerializacaoQrcodeMDFe` | 2278-2301 | MDF-e QR Code generation | +| `SerializacaoMDFe` | 2304-2771 | MDF-e XML serialization | --- @@ -19,14 +19,14 @@ XML serialization of NF-e, NFC-e, NFS-e and MDF-e documents into SEFAZ-compliant Abstract base for all serializers. Stores `_fonte_dados`, `_ambiente` (1=prod, 2=homolog), `_contingencia`, `_so_cpf`. -## `SerializacaoXML` — Lines 66-1829 +## `SerializacaoXML` — Lines 66-1860 ### Exported Methods | Method | Lines | Purpose | |--------|-------|---------| | `exportar()` | 71-97 | Main entry: exports NF-e XML from data source | -| `serializar_evento()` | 1831-1863 | Serializes NF-e events (cancellation, correction letter) | -| `serializar_evento_mdfe()` | 1865-1957 | Serializes MDF-e events (cancel, close, add driver, add DF-e, payment) | +| `serializar_evento()` | ~1862 | Serializes NF-e events (cancellation, correction letter) | +| `serializar_evento_mdfe()` | ~1896 | Serializes MDF-e events (cancel, close, add driver, add DF-e, payment) | ### Internal Serializers (NF-e) | Method | Lines | Purpose | @@ -36,98 +36,98 @@ Abstract base for all serializers. Stores `_fonte_dados`, `_ambiente` (1=prod, 2 | `_serializar_transportadora()` | 202-228 | Carrier data | | `_serializar_entrega_retirada()` | 230-255 | Delivery/withdrawal address | | `_serializar_autorizados_baixar_xml()` | 257-270 | Authorized XML downloaders | -| `_serializar_produto_servico()` | 272-459 | **Product/service item** (prod data, fuel, taxes dispatch) | -| `_serializar_responsavel_tecnico()` | 1347-1359 | Technical responsible (NT2018/003) | -| `_serializar_nota_fiscal()` | 1423-1829 | **Main NF-e assembly** (ide, emit, dest, items, totals, transport, billing, payment, additional info) | +| `_serializar_produto_servico()` | 272-467 | **Product/service item** (prod data, fuel, taxes dispatch incl. IBSCBS) | +| `_serializar_responsavel_tecnico()` | 1440-1452 | Technical responsible (NT2018/003) | +| `_serializar_nota_fiscal()` | 1516-1860 | **Main NF-e assembly** (ide w/ cMunFGIBS, emit, dest, items, totals incl. IBSCBSTot, transport, billing, payment, additional info) | ### Tax Serializers (within `SerializacaoXML`) | Method | Lines | Purpose | |--------|-------|---------| -| `_serializar_imposto_icms()` | 461-1078 | **ICMS tax** — all CST/CSOSN modalities | -| `_serializar_imposto_ipi()` | 1080-1118 | IPI tax | -| `_serializar_imposto_pis()` | 1120-1192 | PIS tax | -| `_serializar_imposto_cofins()` | 1194-1269 | COFINS tax | -| `_serializar_imposto_importacao()` | 1271-1293 | Import tax (II) | -| `_serializar_imposto_ibscbs()` | 1295-1322 | Reforma Tributaria — impostoMisto wrapper (CBS + IBS + IS) | -| `_serializar_cbs()` | 1324-1339 | CBS (Contribuicao sobre Bens e Servicos) | -| `_serializar_ibs()` | 1341-1361 | IBS (Imposto sobre Bens e Servicos) | -| `_serializar_is()` | 1363-1378 | IS (Imposto Seletivo) | -| `_serializar_declaracao_importacao()` | 1380-1430 | Import declaration (DI) | +| `_serializar_imposto_icms()` | 469-1086 | **ICMS tax** — all CST/CSOSN modalities | +| `_serializar_imposto_ipi()` | 1088-1126 | IPI tax | +| `_serializar_imposto_pis()` | 1128-1200 | PIS tax | +| `_serializar_imposto_cofins()` | 1202-1277 | COFINS tax | +| `_serializar_imposto_importacao()` | 1279-1301 | Import tax (II) | +| `_serializar_imposto_ibscbs()` | 1310-1328 | **Reforma Tributaria** — dispatches IBSCBS + IS serialization | +| `_serializar_ibscbs()` | 1330-1369 | IBSCBS (IBS + CBS) — `` as direct child of `` | +| `_serializar_is()` | 1371-1386 | IS (Imposto Seletivo) — ready but not called until 2027 | +| `_serializar_declaracao_importacao()` | 1388-1438 | Import declaration (DI) | ### ICMS Modalities Detail (within `_serializar_imposto_icms`) | CST/CSOSN | Lines | Description | |-----------|-------|-------------| -| 00 | 465-489 | Fully taxed | -| 02 | 491-504 | Monophasic on fuels | -| 10 | 507-568 | Taxed + ICMS ST | -| 15 | 571-600 | Monophasic + retention on fuels | -| 20 | 603-642 | Reduced base | -| 30 | 645-685 | Exempt + ICMS ST | -| 40/41/50 | 688-699 | Exempt / Not taxed / Suspended | -| 51 | 702-719 | Deferral | -| 53 | 722-744 | Monophasic deferred on fuels | -| 60/ST | 747-770 | Previously charged by ST | -| 61 | 773-785 | Monophasic previously charged on fuels | -| 70 | 788-855 | Reduced base + ICMS ST | -| 90 | 858-930 | Other | -| 101 | 935-944 | Simples Nacional with credit | -| 102/103/300/400 | 951-954 | Simples Nacional without credit | -| 201/202/203 | 959-1004 | Simples Nacional + ICMS ST | -| 500 | 1007-1010 | SN previously charged by ST | -| 900 | 1013-1075 | SN Other | +| 00 | 473-497 | Fully taxed | +| 02 | 499-512 | Monophasic on fuels | +| 10 | 515-576 | Taxed + ICMS ST | +| 15 | 579-608 | Monophasic + retention on fuels | +| 20 | 611-650 | Reduced base | +| 30 | 653-693 | Exempt + ICMS ST | +| 40/41/50 | 696-707 | Exempt / Not taxed / Suspended | +| 51 | 710-727 | Deferral | +| 53 | 730-752 | Monophasic deferred on fuels | +| 60/ST | 755-778 | Previously charged by ST | +| 61 | 781-793 | Monophasic previously charged on fuels | +| 70 | 796-863 | Reduced base + ICMS ST | +| 90 | 866-938 | Other | +| 101 | 943-952 | Simples Nacional with credit | +| 102/103/300/400 | 959-962 | Simples Nacional without credit | +| 201/202/203 | 967-1012 | Simples Nacional + ICMS ST | +| 500 | 1015-1018 | SN previously charged by ST | +| 900 | 1021-1083 | SN Other | ### Payment Serializers | Method | Lines | Purpose | |--------|-------|---------| -| `_serializar_pagamentos_antigo_deprecado()` | 1361-1390 | Legacy payment (deprecated) | -| `_serializar_pagamentos()` | 1392-1421 | Current payment serialization | +| `_serializar_pagamentos_antigo_deprecado()` | 1454-1483 | Legacy payment (deprecated) | +| `_serializar_pagamentos()` | 1485-1514 | Current payment serialization | ### NF-e Assembly Detail (`_serializar_nota_fiscal`) | Section | Lines | XML tags | |---------|-------|----------| -| IDE (identification) | 1436-1499 | `` cUF, cNF, natOp, mod, serie, nNF, dhEmi, tpNF, etc. | -| Referenced NF-e | 1501-1537 | `` refNFe, refNF, refNFP, refCTe | -| Contingency | 1539-1547 | dhCont, xJust | -| Emitter | 1549-1550 | `` | -| Customer | 1552-1564 | `` | -| Withdrawal/Delivery | 1566-1583 | ``, `` | -| Authorized XML | 1586-1587 | `` | -| Items | 1589-1596 | `` with nItem | -| Totals | 1598-1691 | `` all tax totals | -| Transport | 1693-1747 | `` modFrete, carrier, vehicle, volumes | -| Billing | 1748-1774 | `` fat, dup | -| Payment | 1776-1801 | `` detPag | -| Additional info | 1803-1816 | `` | -| Technical responsible | 1818-1824 | `` | +| IDE (identification) | 1529-1594 | `` cUF, cNF, natOp, mod, serie, nNF, dhEmi, tpNF, cMunFG, **cMunFGIBS**, etc. | +| Referenced NF-e | 1596-1632 | `` refNFe, refNF, refNFP, refCTe | +| Contingency | 1634-1643 | dhCont, xJust | +| Emitter | 1645 | `` | +| Customer | 1648-1659 | `` | +| Withdrawal/Delivery | 1661-1678 | ``, `` | +| Authorized XML | 1681-1682 | `` | +| Items | 1684-1691 | `` with nItem | +| Totals (ICMSTot) | 1693-1786 | `` all legacy tax totals | +| **Totals (IBSCBSTot)** | 1788-1833 | `` Reforma Tributaria totals (vBCIBSCBS, gIBS, gCBS) | +| Transport | 1835-1878 | `` modFrete, carrier, vehicle, volumes | +| Billing | ~1880-1900 | `` fat, dup | +| Payment | ~1902-1930 | `` detPag | +| Additional info | ~1932-1945 | `` | +| Technical responsible | ~1947-1953 | `` | --- -## `SerializacaoQrcode` — Lines 1960-2064 +## `SerializacaoQrcode` — Lines 2102-2206 Generates NFC-e QR Code URL. Handles online/offline modes and state-specific URL patterns (SP, BA, MG, etc.). -## `SerializacaoNfse` — Lines 2067-2133 +## `SerializacaoNfse` — Lines 2209-2275 Delegates to Betha or Ginfes serializers. Methods: `gerar`, `gerar_lote`, `consultar_nfse`, `consultar_lote`, `consultar_rps`, `consultar_situacao_lote`, `cancelar`. -## `SerializacaoQrcodeMDFe` — Lines 2136-2159 +## `SerializacaoQrcodeMDFe` — Lines 2278-2301 Generates MDF-e QR Code URL using SVRS endpoint. -## `SerializacaoMDFe` — Lines 2162-2630 +## `SerializacaoMDFe` — Lines 2304-2771 ### Methods | Method | Lines | Purpose | |--------|-------|---------| -| `exportar()` | 2167-2193 | Main entry: exports MDF-e XML | -| `_serializar_emitente()` | 2201-2233 | MDF-e issuer (CPF/CNPJ, IE, address) | -| `_serializar_municipio_carrega()` | 2235-2245 | Loading municipality | -| `_serializar_percurso()` | 2247-2254 | Route (UF waypoints) | -| `_serializar_modal_rodoviario()` | 2256-2415 | **Road modal** (ANTT, CIOT, tolls, contractors, traction vehicle, trailer) | -| `_serializar_documentos()` | 2417-2441 | Linked documents (NF-e or CT-e) | -| `_serializar_seguradora()` | 2443-2463 | Insurance info | -| `_serializar_produto()` | 2465-2475 | Predominant product | -| `_serializar_totais()` | 2477-2497 | Totals (weight, qty) | -| `_serializar_lacres()` | 2499-2506 | Seals | -| `_serializar_responsavel_tecnico()` | 2508-2520 | Technical responsible | -| `_serializar_manifesto()` | 2522-2629 | **Main MDF-e assembly** (ide, emit, modal, doc, seg, prod, tot, lacres, infAdic, infRespTec) | +| `exportar()` | 2309-2335 | Main entry: exports MDF-e XML | +| `_serializar_emitente()` | 2343-2375 | MDF-e issuer (CPF/CNPJ, IE, address) | +| `_serializar_municipio_carrega()` | 2377-2387 | Loading municipality | +| `_serializar_percurso()` | 2389-2396 | Route (UF waypoints) | +| `_serializar_modal_rodoviario()` | 2398-2557 | **Road modal** (ANTT, CIOT, tolls, contractors, traction vehicle, trailer) | +| `_serializar_documentos()` | 2559-2583 | Linked documents (NF-e or CT-e) | +| `_serializar_seguradora()` | 2585-2605 | Insurance info | +| `_serializar_produto()` | 2607-2617 | Predominant product | +| `_serializar_totais()` | 2619-2639 | Totals (weight, qty) | +| `_serializar_lacres()` | 2641-2648 | Seals | +| `_serializar_responsavel_tecnico()` | 2650-2662 | Technical responsible | +| `_serializar_manifesto()` | 2664-2771 | **Main MDF-e assembly** (ide, emit, modal, doc, seg, prod, tot, lacres, infAdic, infRespTec) | diff --git a/pynfe/entidades/notafiscal.py b/pynfe/entidades/notafiscal.py index c16cbfed..7047138e 100644 --- a/pynfe/entidades/notafiscal.py +++ b/pynfe/entidades/notafiscal.py @@ -302,11 +302,17 @@ class NotaFiscal(Entidade): # - Valor total do ICMS monofásico sujeito a retenção totais_icms_v_icms_mono_reten = Decimal() - # Reforma Tributaria - Totais IVA Dual - totais_cbs = Decimal() + # Reforma Tributaria - Totais IVA Dual (Group W03 - IBSCBSTot) + totais_vbc_ibscbs = Decimal() # vBCIBSCBS - Total Base de Calculo + totais_ibs_uf = Decimal() + totais_ibs_mun = Decimal() totais_ibs = Decimal() + totais_cbs = Decimal() totais_is = Decimal() + # Reforma Tributaria - cMunFGIBS (Group B) + municipio_fato_gerador_ibs = str() + # Transporte # - Modalidade do Frete (obrigatorio - seleciona de lista) - MODALIDADES_FRETE # 0=Contratação do Frete por conta do Remetente (CIF); @@ -469,14 +475,18 @@ def adicionar_produto_servico(self, **kwargs): self.totais_icms_q_bc_mono_ret += obj.icms_q_bc_mono_ret self.totais_icms_v_icms_mono_ret += obj.icms_v_icms_mono_ret - # Reforma Tributaria - IVA Dual - self.totais_cbs += obj.cbs_valor - self.totais_ibs += obj.ibs_valor + # Reforma Tributaria - IVA Dual (NT 2025.002-RTC) + self.totais_vbc_ibscbs += obj.ibscbs_vbc + self.totais_ibs_uf += obj.ibscbs_v_ibs_uf + self.totais_ibs_mun += obj.ibscbs_v_ibs_mun + self.totais_ibs += obj.ibscbs_v_ibs + self.totais_cbs += obj.ibscbs_v_cbs self.totais_is += obj.is_valor # TODO calcular impostos aproximados # self.totais_tributos_aproximado += obj.tributos + # vNF does NOT include IBS/CBS/IS (prohibited in 2025-2026 per NT 2025.002-RTC) self.totais_icms_total_nota += ( obj.valor_total_bruto + obj.icms_st_valor @@ -487,9 +497,6 @@ def adicionar_produto_servico(self, **kwargs): + obj.imposto_importacao_valor + obj.ipi_valor_ipi + obj.ipi_valor_ipi_dev - + obj.ibs_valor - + obj.cbs_valor - + obj.is_valor - obj.desconto - obj.icms_desonerado ) @@ -1016,27 +1023,35 @@ class NotaFiscalProduto(Entidade): imposto_importacao_valor = Decimal() # ============================================= - # Reforma Tributaria - IVA Dual (EC 132/2023) + # Reforma Tributaria - IVA Dual (NT 2025.002-RTC) # ============================================= - # CBS (Contribuicao sobre Bens e Servicos) - Federal - cbs_situacao_tributaria = str() - cbs_valor_base_calculo = Decimal() - cbs_aliquota = Decimal() - cbs_valor = Decimal() - - # IBS (Imposto sobre Bens e Servicos) - Estadual/Municipal - ibs_situacao_tributaria = str() - ibs_valor_base_calculo = Decimal() - ibs_aliquota = Decimal() - ibs_valor = Decimal() - ibs_codigo_municipio_destino = str() # cMunDest - IBGE code - - # IS (Imposto Seletivo) - Federal - is_situacao_tributaria = str() - is_valor_base_calculo = Decimal() - is_aliquota = Decimal() - is_valor = Decimal() + # IBSCBS group (Group UB) + ibscbs_cst = str() # CST 3-digit (e.g. "000", "222") + ibscbs_c_class_trib = str() # cClassTrib 6-digit classification code + ibscbs_vbc = Decimal() # vBC - shared base de calculo for IBS + CBS + + # gIBSUF - IBS estadual (UF) + ibscbs_p_ibs_uf = Decimal() # pIBSUF + ibscbs_v_ibs_uf = Decimal() # vIBSUF + + # gIBSMun - IBS municipal + ibscbs_p_ibs_mun = Decimal() # pIBSMun + ibscbs_v_ibs_mun = Decimal() # vIBSMun + + # vIBS total (UF + Mun) + ibscbs_v_ibs = Decimal() + + # gCBS - CBS federal + ibscbs_p_cbs = Decimal() # pCBS + ibscbs_v_cbs = Decimal() # vCBS + + # IS (Imposto Seletivo) - Group UB-IS + is_cst_selec = str() # CSTSelec (2-digit) + is_c_class_trib = str() # cClassTribIS 6-digit + is_vbc = Decimal() # vBC + is_aliquota = Decimal() # pIS + is_valor = Decimal() # vIS # - Informacoes Adicionais # - Texto livre de informacoes adicionais diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index 23419592..b0569987 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -1301,67 +1301,87 @@ def _serializar_imposto_importacao( ) # ============================================= - # Reforma Tributaria - IVA Dual (EC 132/2023) + # 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") + def _serializar_imposto_ibscbs( self, produto_servico, modelo, tag_raiz="imposto", retorna_string=True ): - """Serializa grupo impostoMisto contendo CBS, IBS e IS.""" - has_cbs = produto_servico.cbs_situacao_tributaria - has_ibs = produto_servico.ibs_situacao_tributaria - has_is = produto_servico.is_situacao_tributaria + """Serializa grupo IBSCBS (Group UB) como filho direto de . + + Nota: (Imposto Seletivo) so entra no schema a partir de 2027. + O metodo _serializar_is() esta pronto mas nao e chamado ate que o + schema PL 010b inclua o elemento IS dentro de . + """ + has_ibscbs = produto_servico.ibscbs_cst - if not has_cbs and not has_ibs and not has_is: + if not has_ibscbs: return - imposto_misto = etree.SubElement(tag_raiz, "impostoMisto") + self._serializar_ibscbs(produto_servico, tag_raiz) - if has_cbs: - self._serializar_cbs(produto_servico, imposto_misto) + # IS: descomentar quando schema suportar (previsto para 2027) + # if produto_servico.is_cst_selec: + # self._serializar_is(produto_servico, tag_raiz) - if has_ibs: - self._serializar_ibs(produto_servico, imposto_misto) + def _serializar_ibscbs(self, produto_servico, tag_raiz): + """Serializa com gIBSCBS contendo gIBSUF, gIBSMun e gCBS.""" + ibscbs = etree.SubElement(tag_raiz, "IBSCBS") + etree.SubElement(ibscbs, "CST").text = produto_servico.ibscbs_cst - if has_is: - self._serializar_is(produto_servico, imposto_misto) + if produto_servico.ibscbs_c_class_trib: + etree.SubElement(ibscbs, "cClassTrib").text = produto_servico.ibscbs_c_class_trib - def _serializar_cbs(self, produto_servico, tag_raiz): - """Serializa CBS (Contribuicao sobre Bens e Servicos).""" - cbs = etree.SubElement(tag_raiz, "CBS") - etree.SubElement(cbs, "CST").text = produto_servico.cbs_situacao_tributaria + if produto_servico.ibscbs_cst in self._IBSCBS_CST_TRIBUTADOS: + gibscbs = etree.SubElement(ibscbs, "gIBSCBS") - if produto_servico.cbs_situacao_tributaria in ("01", "02", "51"): - etree.SubElement(cbs, "vBC").text = "{:.2f}".format( - produto_servico.cbs_valor_base_calculo or 0 + etree.SubElement(gibscbs, "vBC").text = "{:.2f}".format(produto_servico.ibscbs_vbc 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 ) - etree.SubElement(cbs, "pCBS").text = "{:.4f}".format(produto_servico.cbs_aliquota or 0) - etree.SubElement(cbs, "vCBS").text = "{:.2f}".format(produto_servico.cbs_valor or 0) - def _serializar_ibs(self, produto_servico, tag_raiz): - """Serializa IBS (Imposto sobre Bens e Servicos).""" - ibs = etree.SubElement(tag_raiz, "IBS") - etree.SubElement(ibs, "CST").text = produto_servico.ibs_situacao_tributaria + # 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 + ) - if produto_servico.ibs_situacao_tributaria in ("01", "02", "51"): - etree.SubElement(ibs, "vBC").text = "{:.2f}".format( - produto_servico.ibs_valor_base_calculo or 0 + # vIBS total + etree.SubElement(gibscbs, "vIBS").text = "{:.2f}".format( + produto_servico.ibscbs_v_ibs or 0 ) - etree.SubElement(ibs, "pIBS").text = "{:.4f}".format(produto_servico.ibs_aliquota or 0) - etree.SubElement(ibs, "vIBS").text = "{:.2f}".format(produto_servico.ibs_valor or 0) - if produto_servico.ibs_codigo_municipio_destino: - etree.SubElement(ibs, "cMunDest").text = produto_servico.ibs_codigo_municipio_destino + # 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_is(self, produto_servico, tag_raiz): - """Serializa IS (Imposto Seletivo).""" + """Serializa (Imposto Seletivo) como filho direto de . + + Type: TIS (PL 010b DFeTiposBasicos_v1.00.xsd) + Schema field names: CSTIS, cClassTribIS, vBCIS, pIS, vIS + """ is_tag = etree.SubElement(tag_raiz, "IS") - etree.SubElement(is_tag, "CST").text = produto_servico.is_situacao_tributaria + etree.SubElement(is_tag, "CSTIS").text = produto_servico.is_cst_selec - if produto_servico.is_situacao_tributaria in ("01", "02"): - etree.SubElement(is_tag, "vBC").text = "{:.2f}".format( - produto_servico.is_valor_base_calculo or 0 - ) + if produto_servico.is_c_class_trib: + etree.SubElement(is_tag, "cClassTribIS").text = produto_servico.is_c_class_trib + + if produto_servico.is_cst_selec in ("01", "02"): + etree.SubElement(is_tag, "vBCIS").text = "{:.2f}".format(produto_servico.is_vbc or 0) etree.SubElement(is_tag, "pIS").text = "{:.4f}".format(produto_servico.is_aliquota or 0) etree.SubElement(is_tag, "vIS").text = "{:.2f}".format(produto_servico.is_valor or 0) @@ -1535,6 +1555,8 @@ def _serializar_nota_fiscal(self, nota_fiscal, tag_raiz="infNFe", retorna_string else: etree.SubElement(ide, "idDest").text = str(nota_fiscal.indicador_destino) etree.SubElement(ide, "cMunFG").text = nota_fiscal.municipio + if nota_fiscal.municipio_fato_gerador_ibs: + etree.SubElement(ide, "cMunFGIBS").text = nota_fiscal.municipio_fato_gerador_ibs etree.SubElement(ide, "tpImp").text = str(nota_fiscal.tipo_impressao_danfe) """ # CONTINGENCIA # 1=Emissão normal (não em contingência); @@ -1763,13 +1785,52 @@ def _serializar_nota_fiscal(self, nota_fiscal, tag_raiz="infNFe", retorna_string nota_fiscal.totais_tributos_aproximado ) - # Reforma Tributaria - Totais IVA Dual - if nota_fiscal.totais_cbs: - etree.SubElement(icms_total, "vCBS").text = "{:.2f}".format(nota_fiscal.totais_cbs) - if nota_fiscal.totais_ibs: - etree.SubElement(icms_total, "vIBS").text = "{:.2f}".format(nota_fiscal.totais_ibs) - if nota_fiscal.totais_is: - etree.SubElement(icms_total, "vIS").text = "{:.2f}".format(nota_fiscal.totais_is) + # Reforma Tributaria - Totais IVA Dual (Group W03 - IBSCBSTot) + # Type: TIBSCBSMonoTot (PL 010b DFeTiposBasicos_v1.00.xsd) + has_reforma = ( + nota_fiscal.totais_vbc_ibscbs + or nota_fiscal.totais_ibs + or nota_fiscal.totais_cbs + ) + if has_reforma: + ibscbs_tot = etree.SubElement(total, "IBSCBSTot") + etree.SubElement(ibscbs_tot, "vBCIBSCBS").text = "{:.2f}".format( + nota_fiscal.totais_vbc_ibscbs + ) + + # gIBS (optional — emit if any IBS value exists) + if nota_fiscal.totais_ibs_uf or nota_fiscal.totais_ibs_mun or nota_fiscal.totais_ibs: + g_ibs = etree.SubElement(ibscbs_tot, "gIBS") + + g_ibs_uf = etree.SubElement(g_ibs, "gIBSUF") + etree.SubElement(g_ibs_uf, "vDif").text = "0.00" + etree.SubElement(g_ibs_uf, "vDevTrib").text = "0.00" + etree.SubElement(g_ibs_uf, "vIBSUF").text = "{:.2f}".format( + nota_fiscal.totais_ibs_uf + ) + + g_ibs_mun = etree.SubElement(g_ibs, "gIBSMun") + etree.SubElement(g_ibs_mun, "vDif").text = "0.00" + etree.SubElement(g_ibs_mun, "vDevTrib").text = "0.00" + etree.SubElement(g_ibs_mun, "vIBSMun").text = "{:.2f}".format( + nota_fiscal.totais_ibs_mun + ) + + etree.SubElement(g_ibs, "vIBS").text = "{:.2f}".format(nota_fiscal.totais_ibs) + etree.SubElement(g_ibs, "vCredPres").text = "0.00" + etree.SubElement(g_ibs, "vCredPresCondSus").text = "0.00" + + # gCBS (optional — emit if any CBS value exists) + if nota_fiscal.totais_cbs: + g_cbs = etree.SubElement(ibscbs_tot, "gCBS") + etree.SubElement(g_cbs, "vDif").text = "0.00" + etree.SubElement(g_cbs, "vDevTrib").text = "0.00" + etree.SubElement(g_cbs, "vCBS").text = "{:.2f}".format(nota_fiscal.totais_cbs) + etree.SubElement(g_cbs, "vCredPres").text = "0.00" + etree.SubElement(g_cbs, "vCredPresCondSus").text = "0.00" + + # gMono: not implemented yet (monofasia totals) + # gEstornoCred: not implemented yet (estorno de credito totals) # Transporte transp = etree.SubElement(raiz, "transp") diff --git a/pynfe/utils/flags.py b/pynfe/utils/flags.py index 36ede00f..0cdcc5e0 100644 --- a/pynfe/utils/flags.py +++ b/pynfe/utils/flags.py @@ -223,6 +223,8 @@ (2, "NF-e complementar"), (3, "NF-e de ajuste"), (4, "NF-e de Devolução"), + (5, "NF-e de Débito"), + (6, "NF-e de Crédito"), ) NF_REFERENCIADA_TIPOS = ( @@ -549,25 +551,34 @@ # Reforma Tributaria - IVA Dual (EC 132/2023) # ============================================= -# CST para IBS (Imposto sobre Bens e Servicos) e CBS (Contribuicao sobre Bens e Servicos) -# Tabela preliminar - sujeita a ajuste na regulamentacao da LC +# CST para IBSCBS (IBS + CBS) — NT 2025.002-RTC (3-digit codes) IBS_CBS_TIPOS_TRIBUTACAO = ( - ("01", "Tributada Integralmente"), - ("02", "Tributada com Reducao (Cesta Basica/Saude)"), - ("03", "Isencao"), - ("04", "Imunidade"), - ("05", "Suspensao"), - ("51", "Diferimento"), - ("70", "Monofasica (Combustiveis)"), + ("000", "Tributação integral"), + ("010", "Tributação com alíquota reduzida"), + ("100", "Tributação com suspensão"), + ("110", "Imunidade — exportação"), + ("200", "Tributação com diferimento"), + ("222", "Isenção"), + ("300", "Não incidência"), + ("400", "Tributação por substituição"), + ("410", "Tributação com alíquota zero"), + ("510", "Tributação integral — regime específico"), + ("600", "Tributação monofásica — incidência padrão"), + ("620", "Tributação monofásica — demais operações"), + ("800", "Crédito presumido"), + ("810", "Crédito presumido — ZFM"), + ("900", "Outros"), ) -# CST para IS (Imposto Seletivo) +# CST para IS (Imposto Seletivo) — CSTSelec (NT 2025.002-RTC) IS_TIPOS_TRIBUTACAO = ( - ("01", "Tributada Integralmente"), - ("02", "Tributada com Reducao"), - ("03", "Isencao"), + ("01", "Tributada integralmente"), + ("02", "Tributada com redução"), + ("03", "Isenção"), ("04", "Imunidade"), - ("05", "Suspensao"), + ("05", "Suspensão"), + ("06", "Diferimento"), + ("90", "Outros"), ) MODALIDADES_FRETE = ( diff --git a/tests/test_nfe_serializacao_reforma_tributaria.py b/tests/test_nfe_serializacao_reforma_tributaria.py index cf866c42..aba36cc7 100644 --- a/tests/test_nfe_serializacao_reforma_tributaria.py +++ b/tests/test_nfe_serializacao_reforma_tributaria.py @@ -1,7 +1,11 @@ #!/usr/bin/env python # *-* encoding: utf8 *-* -"""Tests for Reforma Tributaria IBS/CBS/IS serialization (EC 132/2023).""" +"""Tests for Reforma Tributaria IBS/CBS serialization (NT 2025.002-RTC). + +Note: IS (Imposto Seletivo) is not yet in the SEFAZ schema (starts 2027). +IS entity fields are preserved for future use but not serialized to XML. +""" import datetime import unittest @@ -17,7 +21,7 @@ class ReformaTributariaSerializacaoTestCase(unittest.TestCase): - """Tests for IBS/CBS/IS (Reforma Tributaria) XML serialization.""" + """Tests for IBSCBS (Reforma Tributaria) XML serialization per NT 2025.002-RTC.""" def setUp(self): self.certificado = "./tests/certificado.pfx" @@ -87,21 +91,9 @@ def _nota_fiscal(self, emitente, cliente): totais_tributos_aproximado=Decimal("0.00"), ) - def _serializar_e_assinar(self): - serializador = SerializacaoXML(_fonte_dados, homologacao=self.homologacao) - xml = serializador.exportar() - a1 = AssinaturaA1(self.certificado, self.senha) - return a1.assinar(xml) - - # ------------------------------------------------------------------ - # Test 1: CST 01 - Tributada Integralmente (CBS + IBS + IS) - # ------------------------------------------------------------------ - def test_cst01_cbs_ibs_is_tributada_integralmente(self): - emitente = self._emitente() - cliente = self._cliente() - nf = self._nota_fiscal(emitente, cliente) - - nf.adicionar_produto_servico( + def _base_product_kwargs(self): + """Common product kwargs without reforma tributaria fields.""" + return dict( codigo="001", descricao="Produto teste reforma tributaria", ncm="99999999", @@ -128,194 +120,143 @@ def test_cst01_cbs_ibs_is_tributada_integralmente(self): cofins_aliquota_percentual=Decimal("0.00"), cofins_valor=Decimal("0.00"), valor_tributos_aprox="0", - # CBS - cbs_situacao_tributaria="01", - cbs_valor_base_calculo=Decimal("1000.00"), - cbs_aliquota=Decimal("8.8000"), - cbs_valor=Decimal("88.00"), - # IBS - ibs_situacao_tributaria="01", - ibs_valor_base_calculo=Decimal("1000.00"), - ibs_aliquota=Decimal("17.7000"), - ibs_valor=Decimal("177.00"), - ibs_codigo_municipio_destino="4118402", - # IS - is_situacao_tributaria="01", - is_valor_base_calculo=Decimal("1000.00"), - is_aliquota=Decimal("1.0000"), - is_valor=Decimal("10.00"), ) - nf.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=1275.00, ind_pag=0) + def _serializar_e_assinar(self): + serializador = SerializacaoXML(_fonte_dados, homologacao=self.homologacao) + xml = serializador.exportar() + a1 = AssinaturaA1(self.certificado, self.senha) + return a1.assinar(xml) + + # ------------------------------------------------------------------ + # Test 1: CST 000 — full regular taxation with IBS UF/Mun split + # ------------------------------------------------------------------ + def test_cst000_ibscbs_tributacao_integral(self): + 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() - # impostoMisto group exists - imposto_misto = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto", namespaces=self.ns) - self.assertEqual(len(imposto_misto), 1) + # is direct child of (no impostoMisto wrapper) + ibscbs = xml.xpath("//ns:det/ns:imposto/ns:IBSCBS", namespaces=self.ns) + self.assertEqual(len(ibscbs), 1) - # CBS - cst_cbs = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto/ns:CBS/ns:CST", namespaces=self.ns - )[0].text - self.assertEqual(cst_cbs, "01") + # No impostoMisto wrapper + imposto_misto = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto", namespaces=self.ns) + self.assertEqual(len(imposto_misto), 0) - vbc_cbs = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto/ns:CBS/ns:vBC", namespaces=self.ns - )[0].text - self.assertEqual(vbc_cbs, "1000.00") + # CST is 3-digit + cst = xml.xpath("//ns:IBSCBS/ns:CST", namespaces=self.ns)[0].text + self.assertEqual(cst, "000") - pcbs = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:CBS/ns:pCBS", namespaces=self.ns)[ - 0 - ].text - self.assertEqual(pcbs, "8.8000") + # cClassTrib present + cclass = xml.xpath("//ns:IBSCBS/ns:cClassTrib", namespaces=self.ns)[0].text + self.assertEqual(cclass, "000001") - vcbs = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:CBS/ns:vCBS", namespaces=self.ns)[ - 0 - ].text - self.assertEqual(vcbs, "88.00") + # Shared vBC at gIBSCBS level + vbc = xml.xpath("//ns:IBSCBS/ns:gIBSCBS/ns:vBC", namespaces=self.ns)[0].text + self.assertEqual(vbc, "1000.00") - # IBS - cst_ibs = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto/ns:IBS/ns:CST", namespaces=self.ns + # gIBSUF + p_ibs_uf = xml.xpath( + "//ns:IBSCBS/ns:gIBSCBS/ns:gIBSUF/ns:pIBSUF", namespaces=self.ns )[0].text - self.assertEqual(cst_ibs, "01") - - vbc_ibs = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto/ns:IBS/ns:vBC", namespaces=self.ns + self.assertEqual(p_ibs_uf, "0.1000") + v_ibs_uf = xml.xpath( + "//ns:IBSCBS/ns:gIBSCBS/ns:gIBSUF/ns:vIBSUF", namespaces=self.ns )[0].text - self.assertEqual(vbc_ibs, "1000.00") - - pibs = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:IBS/ns:pIBS", namespaces=self.ns)[ - 0 - ].text - self.assertEqual(pibs, "17.7000") + self.assertEqual(v_ibs_uf, "1.00") - vibs = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:IBS/ns:vIBS", namespaces=self.ns)[ - 0 - ].text - self.assertEqual(vibs, "177.00") - - cmun = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto/ns:IBS/ns:cMunDest", namespaces=self.ns + # gIBSMun + p_ibs_mun = xml.xpath( + "//ns:IBSCBS/ns:gIBSCBS/ns:gIBSMun/ns:pIBSMun", namespaces=self.ns )[0].text - self.assertEqual(cmun, "4118402") - - # IS - cst_is = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:IS/ns:CST", namespaces=self.ns)[ - 0 - ].text - self.assertEqual(cst_is, "01") - - vbc_is = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:IS/ns:vBC", namespaces=self.ns)[ - 0 - ].text - self.assertEqual(vbc_is, "1000.00") - - pis_val = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:IS/ns:pIS", namespaces=self.ns)[ - 0 - ].text - self.assertEqual(pis_val, "1.0000") - - vis = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:IS/ns:vIS", namespaces=self.ns)[ - 0 - ].text - self.assertEqual(vis, "10.00") - - # Totals - vcbs_total = xml.xpath("//ns:total/ns:ICMSTot/ns:vCBS", namespaces=self.ns)[0].text - self.assertEqual(vcbs_total, "88.00") + self.assertEqual(p_ibs_mun, "0.0000") + v_ibs_mun = xml.xpath( + "//ns:IBSCBS/ns:gIBSCBS/ns:gIBSMun/ns:vIBSMun", namespaces=self.ns + )[0].text + self.assertEqual(v_ibs_mun, "0.00") - vibs_total = xml.xpath("//ns:total/ns:ICMSTot/ns:vIBS", namespaces=self.ns)[0].text - self.assertEqual(vibs_total, "177.00") + # vIBS total + v_ibs = xml.xpath("//ns:IBSCBS/ns:gIBSCBS/ns:vIBS", namespaces=self.ns)[0].text + self.assertEqual(v_ibs, "1.00") - vis_total = xml.xpath("//ns:total/ns:ICMSTot/ns:vIS", namespaces=self.ns)[0].text - self.assertEqual(vis_total, "10.00") + # gCBS + p_cbs = xml.xpath("//ns:IBSCBS/ns:gIBSCBS/ns:gCBS/ns:pCBS", namespaces=self.ns)[0].text + self.assertEqual(p_cbs, "0.9000") + v_cbs = xml.xpath("//ns:IBSCBS/ns:gIBSCBS/ns:gCBS/ns:vCBS", namespaces=self.ns)[0].text + self.assertEqual(v_cbs, "9.00") - # vNF includes IBS/CBS/IS - vnf = xml.xpath("//ns:total/ns:ICMSTot/ns:vNF", namespaces=self.ns)[0].text - self.assertEqual(vnf, "1275.00") + # IS NOT emitted in XML (schema not ready until 2027) + is_tag = xml.xpath("//ns:det/ns:imposto/ns:IS", namespaces=self.ns) + self.assertEqual(len(is_tag), 0) # ------------------------------------------------------------------ - # Test 2: CST 03 - Isencao (CBS + IBS isentas, no IS) + # Test 2: CST 222 — isenção (no vBC, values zero) # ------------------------------------------------------------------ - def test_cst03_isencao_sem_valores(self): + def test_cst222_isencao_sem_valores(self): emitente = self._emitente() cliente = self._cliente() nf = self._nota_fiscal(emitente, cliente) - nf.adicionar_produto_servico( + kwargs = self._base_product_kwargs() + kwargs.update( codigo="002", descricao="Produto isento reforma tributaria", - ncm="99999999", - ean="SEM GTIN", - cfop="5102", - unidade_comercial="UN", quantidade_comercial=Decimal("1"), valor_unitario_comercial=Decimal("50.00"), valor_total_bruto=Decimal("50.00"), - unidade_tributavel="UN", quantidade_tributavel=Decimal("1"), valor_unitario_tributavel=Decimal("50.00"), - ean_tributavel="SEM GTIN", - ind_total=1, - icms_modalidade="00", - icms_origem=0, - icms_csosn="", - pis_modalidade="99", - cofins_modalidade="99", - pis_valor_base_calculo=Decimal("0.00"), - pis_aliquota_percentual=Decimal("0.00"), - pis_valor=Decimal("0.00"), - cofins_valor_base_calculo=Decimal("0.00"), - cofins_aliquota_percentual=Decimal("0.00"), - cofins_valor=Decimal("0.00"), - valor_tributos_aprox="0", - # CBS isenta - cbs_situacao_tributaria="03", - # IBS isenta - ibs_situacao_tributaria="03", - # No IS + ibscbs_cst="222", + ibscbs_c_class_trib="000002", ) - + nf.adicionar_produto_servico(**kwargs) nf.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=50.00, ind_pag=0) xml = self._serializar_e_assinar() - # impostoMisto group exists (CBS + IBS present even if exempt) - imposto_misto = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto", namespaces=self.ns) - self.assertEqual(len(imposto_misto), 1) + # IBSCBS present even if exempt + ibscbs = xml.xpath("//ns:det/ns:imposto/ns:IBSCBS", namespaces=self.ns) + self.assertEqual(len(ibscbs), 1) - # CBS CST present, no value tags - cst_cbs = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto/ns:CBS/ns:CST", namespaces=self.ns - )[0].text - self.assertEqual(cst_cbs, "03") - - vbc_cbs = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:CBS/ns:vBC", namespaces=self.ns) - self.assertEqual(len(vbc_cbs), 0) # No vBC for CST 03 + # CST present + cst = xml.xpath("//ns:IBSCBS/ns:CST", namespaces=self.ns)[0].text + self.assertEqual(cst, "222") - # IBS CST present, no value tags - cst_ibs = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto/ns:IBS/ns:CST", namespaces=self.ns - )[0].text - self.assertEqual(cst_ibs, "03") + # cClassTrib present + cclass = xml.xpath("//ns:IBSCBS/ns:cClassTrib", namespaces=self.ns)[0].text + self.assertEqual(cclass, "000002") - vbc_ibs = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:IBS/ns:vBC", namespaces=self.ns) - self.assertEqual(len(vbc_ibs), 0) + # No gIBSCBS group (CST 222 is not in taxable CSTs) + gibscbs = xml.xpath("//ns:IBSCBS/ns:gIBSCBS", namespaces=self.ns) + self.assertEqual(len(gibscbs), 0) # No IS group - is_group = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:IS", namespaces=self.ns) + is_group = xml.xpath("//ns:det/ns:imposto/ns:IS", namespaces=self.ns) self.assertEqual(len(is_group), 0) - # No totals for CBS/IBS/IS (all zero) - vcbs_total = xml.xpath("//ns:total/ns:ICMSTot/ns:vCBS", namespaces=self.ns) - self.assertEqual(len(vcbs_total), 0) - # ------------------------------------------------------------------ - # Test 3: No reforma data - impostoMisto NOT emitted + # Test 3: No reforma data — IBSCBS not emitted # ------------------------------------------------------------------ - def test_sem_reforma_tributaria_sem_imposto_misto(self): + def test_sem_reforma_tributaria_sem_ibscbs(self): emitente = self._emitente() cliente = self._cliente() nf = self._nota_fiscal(emitente, cliente) @@ -353,127 +294,102 @@ def test_sem_reforma_tributaria_sem_imposto_misto(self): xml = self._serializar_e_assinar() - # impostoMisto should NOT exist + # IBSCBS should NOT exist + ibscbs = xml.xpath("//ns:det/ns:imposto/ns:IBSCBS", namespaces=self.ns) + self.assertEqual(len(ibscbs), 0) + + # No impostoMisto either imposto_misto = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto", namespaces=self.ns) self.assertEqual(len(imposto_misto), 0) - # No reform totals - vcbs_total = xml.xpath("//ns:total/ns:ICMSTot/ns:vCBS", namespaces=self.ns) - self.assertEqual(len(vcbs_total), 0) - - vibs_total = xml.xpath("//ns:total/ns:ICMSTot/ns:vIBS", namespaces=self.ns) - self.assertEqual(len(vibs_total), 0) + # No IBSCBSTot + ibscbs_tot = xml.xpath("//ns:total/ns:IBSCBSTot", namespaces=self.ns) + self.assertEqual(len(ibscbs_tot), 0) # ------------------------------------------------------------------ - # Test 4: Totals accumulation - multiple products + # Test 4: Multiple products — totals accumulation with split IBS # ------------------------------------------------------------------ def test_totais_acumulacao_multiplos_produtos(self): emitente = self._emitente() cliente = self._cliente() nf = self._nota_fiscal(emitente, cliente) - # Product 1: CBS=88, IBS=177, IS=10 - nf.adicionar_produto_servico( + base = self._base_product_kwargs() + + # Product 1: vBC=1000, IBS UF=1.00, IBS Mun=0.50, IBS=1.50, CBS=9.00 + p1 = dict(base) + p1.update( codigo="004", descricao="Produto 1 reforma", - ncm="99999999", - ean="SEM GTIN", - cfop="5102", - unidade_comercial="UN", - quantidade_comercial=Decimal("10"), - valor_unitario_comercial=Decimal("100.00"), - valor_total_bruto=Decimal("1000.00"), - unidade_tributavel="UN", - quantidade_tributavel=Decimal("10"), - valor_unitario_tributavel=Decimal("100.00"), - ean_tributavel="SEM GTIN", - ind_total=1, - icms_modalidade="00", - icms_origem=0, - icms_csosn="", - pis_modalidade="99", - cofins_modalidade="99", - pis_valor=Decimal("0.00"), - cofins_valor=Decimal("0.00"), - valor_tributos_aprox="0", - cbs_situacao_tributaria="01", - cbs_valor_base_calculo=Decimal("1000.00"), - cbs_aliquota=Decimal("8.8000"), - cbs_valor=Decimal("88.00"), - ibs_situacao_tributaria="01", - ibs_valor_base_calculo=Decimal("1000.00"), - ibs_aliquota=Decimal("17.7000"), - ibs_valor=Decimal("177.00"), - is_situacao_tributaria="01", - is_valor_base_calculo=Decimal("1000.00"), - is_aliquota=Decimal("1.0000"), - is_valor=Decimal("10.00"), + 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.0500"), + ibscbs_v_ibs_mun=Decimal("0.50"), + ibscbs_v_ibs=Decimal("1.50"), + ibscbs_p_cbs=Decimal("0.9000"), + ibscbs_v_cbs=Decimal("9.00"), ) + nf.adicionar_produto_servico(**p1) - # Product 2: CBS=44, IBS=88.50, IS=5 - nf.adicionar_produto_servico( + # Product 2: vBC=500, IBS UF=0.50, IBS Mun=0.25, IBS=0.75, CBS=4.50 + p2 = dict(base) + p2.update( codigo="005", descricao="Produto 2 reforma", - ncm="99999999", - ean="SEM GTIN", - cfop="5102", - unidade_comercial="UN", quantidade_comercial=Decimal("5"), valor_unitario_comercial=Decimal("100.00"), valor_total_bruto=Decimal("500.00"), - unidade_tributavel="UN", quantidade_tributavel=Decimal("5"), valor_unitario_tributavel=Decimal("100.00"), - ean_tributavel="SEM GTIN", - ind_total=1, - icms_modalidade="00", - icms_origem=0, - icms_csosn="", - pis_modalidade="99", - cofins_modalidade="99", - pis_valor=Decimal("0.00"), - cofins_valor=Decimal("0.00"), - valor_tributos_aprox="0", - cbs_situacao_tributaria="01", - cbs_valor_base_calculo=Decimal("500.00"), - cbs_aliquota=Decimal("8.8000"), - cbs_valor=Decimal("44.00"), - ibs_situacao_tributaria="01", - ibs_valor_base_calculo=Decimal("500.00"), - ibs_aliquota=Decimal("17.7000"), - ibs_valor=Decimal("88.50"), - is_situacao_tributaria="01", - is_valor_base_calculo=Decimal("500.00"), - is_aliquota=Decimal("1.0000"), - is_valor=Decimal("5.00"), + ibscbs_cst="000", + ibscbs_c_class_trib="000001", + ibscbs_vbc=Decimal("500.00"), + ibscbs_p_ibs_uf=Decimal("0.1000"), + ibscbs_v_ibs_uf=Decimal("0.50"), + ibscbs_p_ibs_mun=Decimal("0.0500"), + ibscbs_v_ibs_mun=Decimal("0.25"), + ibscbs_v_ibs=Decimal("0.75"), + ibscbs_p_cbs=Decimal("0.9000"), + ibscbs_v_cbs=Decimal("4.50"), ) + nf.adicionar_produto_servico(**p2) - nf.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=1907.50, ind_pag=0) + nf.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=1500.00, ind_pag=0) xml = self._serializar_e_assinar() - # Accumulated totals: CBS=132.00, IBS=265.50, IS=15.00 - vcbs_total = xml.xpath("//ns:total/ns:ICMSTot/ns:vCBS", namespaces=self.ns)[0].text - self.assertEqual(vcbs_total, "132.00") + # Totals in IBSCBSTot (NOT ICMSTot) + ibscbs_tot = xml.xpath("//ns:total/ns:IBSCBSTot", namespaces=self.ns) + self.assertEqual(len(ibscbs_tot), 1) - vibs_total = xml.xpath("//ns:total/ns:ICMSTot/ns:vIBS", namespaces=self.ns)[0].text - self.assertEqual(vibs_total, "265.50") + # vBCIBSCBS = sum of all ibscbs_vbc = 1000 + 500 = 1500 + v_bc = xml.xpath("//ns:IBSCBSTot/ns:vBCIBSCBS", namespaces=self.ns)[0].text + self.assertEqual(v_bc, "1500.00") - vis_total = xml.xpath("//ns:total/ns:ICMSTot/ns:vIS", namespaces=self.ns)[0].text - self.assertEqual(vis_total, "15.00") + # Accumulated: IBS UF=1.50, IBS Mun=0.75, IBS=2.25, CBS=13.50 + v_ibs_uf = xml.xpath( + "//ns:IBSCBSTot/ns:gIBS/ns:gIBSUF/ns:vIBSUF", namespaces=self.ns + )[0].text + self.assertEqual(v_ibs_uf, "1.50") - # vNF = 1000 + 500 + 132 + 265.50 + 15 = 1912.50 - # Wait: vNF = sum of (vProd + IBS + CBS + IS) for each product - # Prod1: 1000 + 177 + 88 + 10 = 1275 - # Prod2: 500 + 88.50 + 44 + 5 = 637.50 - # Total: 1275 + 637.50 = 1912.50 - vnf = xml.xpath("//ns:total/ns:ICMSTot/ns:vNF", namespaces=self.ns)[0].text - self.assertEqual(vnf, "1912.50") + v_ibs_mun = xml.xpath( + "//ns:IBSCBSTot/ns:gIBS/ns:gIBSMun/ns:vIBSMun", namespaces=self.ns + )[0].text + self.assertEqual(v_ibs_mun, "0.75") + + v_ibs = xml.xpath("//ns:IBSCBSTot/ns:gIBS/ns:vIBS", namespaces=self.ns)[0].text + self.assertEqual(v_ibs, "2.25") + + v_cbs = xml.xpath("//ns:IBSCBSTot/ns:gCBS/ns:vCBS", namespaces=self.ns)[0].text + self.assertEqual(v_cbs, "13.50") # ------------------------------------------------------------------ - # Test 5: Mixed legacy + reform (ICMS/PIS/COFINS + IBS/CBS) + # Test 5: Mixed legacy (ICMS/PIS/COFINS) + reform (IBSCBS) coexistence # ------------------------------------------------------------------ - def test_misto_legacy_icms_pis_cofins_com_ibs_cbs(self): + def test_misto_legacy_icms_pis_cofins_com_ibscbs(self): emitente = self._emitente() cliente = self._cliente() nf = self._nota_fiscal(emitente, cliente) @@ -511,32 +427,33 @@ def test_misto_legacy_icms_pis_cofins_com_ibs_cbs(self): cofins_aliquota_percentual=Decimal("7.60"), cofins_valor=Decimal("15.20"), valor_tributos_aprox="0", - # Reform CBS (coexisting during transition) - cbs_situacao_tributaria="02", - cbs_valor_base_calculo=Decimal("200.00"), - cbs_aliquota=Decimal("4.4000"), - cbs_valor=Decimal("8.80"), - # Reform IBS (coexisting during transition) - ibs_situacao_tributaria="02", - ibs_valor_base_calculo=Decimal("200.00"), - ibs_aliquota=Decimal("8.8500"), - ibs_valor=Decimal("17.70"), + # Reform IBSCBS (coexisting during transition) + ibscbs_cst="010", + ibscbs_c_class_trib="000003", + ibscbs_vbc=Decimal("200.00"), + ibscbs_p_ibs_uf=Decimal("4.4250"), + ibscbs_v_ibs_uf=Decimal("8.85"), + ibscbs_p_ibs_mun=Decimal("4.4250"), + ibscbs_v_ibs_mun=Decimal("8.85"), + ibscbs_v_ibs=Decimal("17.70"), + ibscbs_p_cbs=Decimal("4.4000"), + ibscbs_v_cbs=Decimal("8.80"), ) - nf.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=226.50, ind_pag=0) + nf.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=200.00, ind_pag=0) xml = self._serializar_e_assinar() # Legacy ICMS still present - icms_cst = xml.xpath("//ns:det/ns:imposto/ns:ICMS/ns:ICMS00/ns:CST", namespaces=self.ns)[ - 0 - ].text + icms_cst = xml.xpath( + "//ns:det/ns:imposto/ns:ICMS/ns:ICMS00/ns:CST", namespaces=self.ns + )[0].text self.assertEqual(icms_cst, "00") # Legacy PIS still present - pis_cst = xml.xpath("//ns:det/ns:imposto/ns:PIS/ns:PISAliq/ns:CST", namespaces=self.ns)[ - 0 - ].text + pis_cst = xml.xpath( + "//ns:det/ns:imposto/ns:PIS/ns:PISAliq/ns:CST", namespaces=self.ns + )[0].text self.assertEqual(pis_cst, "01") # Legacy COFINS still present @@ -545,22 +462,172 @@ def test_misto_legacy_icms_pis_cofins_com_ibs_cbs(self): )[0].text self.assertEqual(cofins_cst, "01") - # Reform CBS also present - cbs_cst = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto/ns:CBS/ns:CST", namespaces=self.ns - )[0].text - self.assertEqual(cbs_cst, "02") + # Reform IBSCBS also present (as direct child of imposto) + ibscbs = xml.xpath("//ns:det/ns:imposto/ns:IBSCBS", namespaces=self.ns) + self.assertEqual(len(ibscbs), 1) - # Reform IBS also present - ibs_cst = xml.xpath( - "//ns:det/ns:imposto/ns:impostoMisto/ns:IBS/ns:CST", namespaces=self.ns - )[0].text - self.assertEqual(ibs_cst, "02") + cst = xml.xpath("//ns:IBSCBS/ns:CST", namespaces=self.ns)[0].text + self.assertEqual(cst, "010") - # No IS in this test - is_group = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto/ns:IS", namespaces=self.ns) + # No IS in XML + is_group = xml.xpath("//ns:det/ns:imposto/ns:IS", namespaces=self.ns) self.assertEqual(len(is_group), 0) + # ------------------------------------------------------------------ + # Test 6: vNF does NOT include IBS/CBS (prohibited in 2025-2026) + # ------------------------------------------------------------------ + def test_vnf_nao_inclui_ibs_cbs(self): + 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() + + # vNF should be 1000.00 (product value only, NO IBS/CBS) + vnf = xml.xpath("//ns:total/ns:ICMSTot/ns:vNF", namespaces=self.ns)[0].text + self.assertEqual(vnf, "1000.00") + + # IBS/CBS totals should NOT be inside ICMSTot + vcbs_in_icmstot = xml.xpath("//ns:ICMSTot/ns:vCBS", namespaces=self.ns) + self.assertEqual(len(vcbs_in_icmstot), 0) + vibs_in_icmstot = xml.xpath("//ns:ICMSTot/ns:vIBS", namespaces=self.ns) + self.assertEqual(len(vibs_in_icmstot), 0) + + # ------------------------------------------------------------------ + # Test 7: Totals in IBSCBSTot, NOT in ICMSTot + # ------------------------------------------------------------------ + def test_totais_em_ibscbstot_nao_icmstot(self): + 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("5.0000"), + ibscbs_v_ibs_uf=Decimal("50.00"), + ibscbs_p_ibs_mun=Decimal("3.0000"), + ibscbs_v_ibs_mun=Decimal("30.00"), + ibscbs_v_ibs=Decimal("80.00"), + ibscbs_p_cbs=Decimal("8.8000"), + ibscbs_v_cbs=Decimal("88.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 a sibling of ICMSTot under + ibscbs_tot = xml.xpath("//ns:total/ns:IBSCBSTot", namespaces=self.ns) + self.assertEqual(len(ibscbs_tot), 1) + + # Verify vBCIBSCBS (required first child) + self.assertEqual( + xml.xpath("//ns:IBSCBSTot/ns:vBCIBSCBS", namespaces=self.ns)[0].text, + "1000.00", + ) + + # Verify nested gIBS structure + self.assertEqual( + xml.xpath("//ns:IBSCBSTot/ns:gIBS/ns:gIBSUF/ns:vIBSUF", namespaces=self.ns)[0].text, + "50.00", + ) + self.assertEqual( + xml.xpath("//ns:IBSCBSTot/ns:gIBS/ns:gIBSMun/ns:vIBSMun", namespaces=self.ns)[0].text, + "30.00", + ) + self.assertEqual( + xml.xpath("//ns:IBSCBSTot/ns:gIBS/ns:vIBS", namespaces=self.ns)[0].text, + "80.00", + ) + + # Verify nested gCBS structure + self.assertEqual( + xml.xpath("//ns:IBSCBSTot/ns:gCBS/ns:vCBS", namespaces=self.ns)[0].text, + "88.00", + ) + + # Verify vDif and vDevTrib are present (required, zero for now) + self.assertEqual( + xml.xpath("//ns:IBSCBSTot/ns:gIBS/ns:gIBSUF/ns:vDif", namespaces=self.ns)[0].text, + "0.00", + ) + self.assertEqual( + xml.xpath("//ns:IBSCBSTot/ns:gCBS/ns:vDif", namespaces=self.ns)[0].text, + "0.00", + ) + + # ------------------------------------------------------------------ + # Test 8: IS entity fields stored but NOT serialized to XML + # ------------------------------------------------------------------ + def test_is_entity_stored_but_not_in_xml(self): + """IS (Imposto Seletivo) starts in 2027. Entity fields are stored + but _serializar_is is not called until schema supports it.""" + 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"), + # IS fields stored in entity + is_cst_selec="01", + is_c_class_trib="010001", + is_vbc=Decimal("1000.00"), + is_aliquota=Decimal("1.0000"), + is_valor=Decimal("10.00"), + ) + nf.adicionar_produto_servico(**kwargs) + nf.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=1000.00, ind_pag=0) + + # Verify entity fields are stored + produto = nf.produtos_e_servicos[0] + self.assertEqual(produto.is_cst_selec, "01") + self.assertEqual(produto.is_c_class_trib, "010001") + self.assertEqual(produto.is_vbc, Decimal("1000.00")) + self.assertEqual(produto.is_aliquota, Decimal("1.0000")) + self.assertEqual(produto.is_valor, Decimal("10.00")) + + # IS totals accumulated internally + self.assertEqual(nf.totais_is, Decimal("10.00")) + + xml = self._serializar_e_assinar() + + # But IS is NOT in the XML output + is_tag = xml.xpath("//ns:det/ns:imposto/ns:IS", namespaces=self.ns) + self.assertEqual(len(is_tag), 0) + + # ISTot not emitted (IS not in schema yet) + is_tot = xml.xpath("//ns:total/ns:ISTot", namespaces=self.ns) + self.assertEqual(len(is_tot), 0) + if __name__ == "__main__": unittest.main() From d2c5971b580ee868cc59132f798ced1de1019a14 Mon Sep 17 00:00:00 2001 From: Felipe Correa Date: Sat, 14 Feb 2026 22:50:14 -0300 Subject: [PATCH 6/7] test: Add cMunFGIBS header serialization tests Co-Authored-By: Claude Opus 4.6 --- ...est_nfe_serializacao_reforma_tributaria.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/test_nfe_serializacao_reforma_tributaria.py b/tests/test_nfe_serializacao_reforma_tributaria.py index aba36cc7..679f496c 100644 --- a/tests/test_nfe_serializacao_reforma_tributaria.py +++ b/tests/test_nfe_serializacao_reforma_tributaria.py @@ -629,5 +629,64 @@ def test_is_entity_stored_but_not_in_xml(self): self.assertEqual(len(is_tot), 0) + # ------------------------------------------------------------------ + # Test 9: cMunFGIBS emitted in header + # ------------------------------------------------------------------ + def test_cmunfgibs_emitido_no_ide(self): + emitente = self._emitente() + cliente = self._cliente() + nf = self._nota_fiscal(emitente, cliente) + nf.municipio_fato_gerador_ibs = "4118402" + + 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() + + # cMunFGIBS should be present in + cmunfgibs = xml.xpath("//ns:ide/ns:cMunFGIBS", namespaces=self.ns) + self.assertEqual(len(cmunfgibs), 1) + self.assertEqual(cmunfgibs[0].text, "4118402") + + # cMunFGIBS should come after cMunFG + ide = xml.xpath("//ns:ide", namespaces=self.ns)[0] + tags = [child.tag.split("}")[-1] for child in ide] + cmunfg_idx = tags.index("cMunFG") + cmunfgibs_idx = tags.index("cMunFGIBS") + self.assertGreater(cmunfgibs_idx, cmunfg_idx) + + # ------------------------------------------------------------------ + # Test 10: cMunFGIBS NOT emitted when not set + # ------------------------------------------------------------------ + def test_cmunfgibs_nao_emitido_quando_vazio(self): + emitente = self._emitente() + cliente = self._cliente() + nf = self._nota_fiscal(emitente, cliente) + # municipio_fato_gerador_ibs not set (empty string default) + + kwargs = self._base_product_kwargs() + 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() + + # cMunFGIBS should NOT be present + cmunfgibs = xml.xpath("//ns:ide/ns:cMunFGIBS", namespaces=self.ns) + self.assertEqual(len(cmunfgibs), 0) + + if __name__ == "__main__": unittest.main() From 62e72eac2430c90089eef52c688af16c30a88409 Mon Sep 17 00:00:00 2001 From: Felipe Correa Date: Sat, 14 Feb 2026 22:53:38 -0300 Subject: [PATCH 7/7] style: Fix ruff format in serializacao.py and reforma tributaria tests Co-Authored-By: Claude Opus 4.6 --- pynfe/processamento/serializacao.py | 4 +- ...est_nfe_serializacao_reforma_tributaria.py | 49 +++++++++---------- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index b0569987..5cf123cc 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -1788,9 +1788,7 @@ 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_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 ) if has_reforma: ibscbs_tot = etree.SubElement(total, "IBSCBSTot") diff --git a/tests/test_nfe_serializacao_reforma_tributaria.py b/tests/test_nfe_serializacao_reforma_tributaria.py index 679f496c..ee042b8a 100644 --- a/tests/test_nfe_serializacao_reforma_tributaria.py +++ b/tests/test_nfe_serializacao_reforma_tributaria.py @@ -175,23 +175,23 @@ def test_cst000_ibscbs_tributacao_integral(self): self.assertEqual(vbc, "1000.00") # gIBSUF - p_ibs_uf = xml.xpath( - "//ns:IBSCBS/ns:gIBSCBS/ns:gIBSUF/ns:pIBSUF", namespaces=self.ns - )[0].text + p_ibs_uf = xml.xpath("//ns:IBSCBS/ns:gIBSCBS/ns:gIBSUF/ns:pIBSUF", namespaces=self.ns)[ + 0 + ].text self.assertEqual(p_ibs_uf, "0.1000") - v_ibs_uf = xml.xpath( - "//ns:IBSCBS/ns:gIBSCBS/ns:gIBSUF/ns:vIBSUF", namespaces=self.ns - )[0].text + v_ibs_uf = xml.xpath("//ns:IBSCBS/ns:gIBSCBS/ns:gIBSUF/ns:vIBSUF", namespaces=self.ns)[ + 0 + ].text self.assertEqual(v_ibs_uf, "1.00") # gIBSMun - p_ibs_mun = xml.xpath( - "//ns:IBSCBS/ns:gIBSCBS/ns:gIBSMun/ns:pIBSMun", namespaces=self.ns - )[0].text + p_ibs_mun = xml.xpath("//ns:IBSCBS/ns:gIBSCBS/ns:gIBSMun/ns:pIBSMun", namespaces=self.ns)[ + 0 + ].text self.assertEqual(p_ibs_mun, "0.0000") - v_ibs_mun = xml.xpath( - "//ns:IBSCBS/ns:gIBSCBS/ns:gIBSMun/ns:vIBSMun", namespaces=self.ns - )[0].text + v_ibs_mun = xml.xpath("//ns:IBSCBS/ns:gIBSCBS/ns:gIBSMun/ns:vIBSMun", namespaces=self.ns)[ + 0 + ].text self.assertEqual(v_ibs_mun, "0.00") # vIBS total @@ -370,14 +370,14 @@ def test_totais_acumulacao_multiplos_produtos(self): self.assertEqual(v_bc, "1500.00") # Accumulated: IBS UF=1.50, IBS Mun=0.75, IBS=2.25, CBS=13.50 - v_ibs_uf = xml.xpath( - "//ns:IBSCBSTot/ns:gIBS/ns:gIBSUF/ns:vIBSUF", namespaces=self.ns - )[0].text + v_ibs_uf = xml.xpath("//ns:IBSCBSTot/ns:gIBS/ns:gIBSUF/ns:vIBSUF", namespaces=self.ns)[ + 0 + ].text self.assertEqual(v_ibs_uf, "1.50") - v_ibs_mun = xml.xpath( - "//ns:IBSCBSTot/ns:gIBS/ns:gIBSMun/ns:vIBSMun", namespaces=self.ns - )[0].text + v_ibs_mun = xml.xpath("//ns:IBSCBSTot/ns:gIBS/ns:gIBSMun/ns:vIBSMun", namespaces=self.ns)[ + 0 + ].text self.assertEqual(v_ibs_mun, "0.75") v_ibs = xml.xpath("//ns:IBSCBSTot/ns:gIBS/ns:vIBS", namespaces=self.ns)[0].text @@ -445,15 +445,15 @@ def test_misto_legacy_icms_pis_cofins_com_ibscbs(self): xml = self._serializar_e_assinar() # Legacy ICMS still present - icms_cst = xml.xpath( - "//ns:det/ns:imposto/ns:ICMS/ns:ICMS00/ns:CST", namespaces=self.ns - )[0].text + icms_cst = xml.xpath("//ns:det/ns:imposto/ns:ICMS/ns:ICMS00/ns:CST", namespaces=self.ns)[ + 0 + ].text self.assertEqual(icms_cst, "00") # Legacy PIS still present - pis_cst = xml.xpath( - "//ns:det/ns:imposto/ns:PIS/ns:PISAliq/ns:CST", namespaces=self.ns - )[0].text + pis_cst = xml.xpath("//ns:det/ns:imposto/ns:PIS/ns:PISAliq/ns:CST", namespaces=self.ns)[ + 0 + ].text self.assertEqual(pis_cst, "01") # Legacy COFINS still present @@ -628,7 +628,6 @@ def test_is_entity_stored_but_not_in_xml(self): is_tot = xml.xpath("//ns:total/ns:ISTot", namespaces=self.ns) self.assertEqual(len(is_tot), 0) - # ------------------------------------------------------------------ # Test 9: cMunFGIBS emitted in header # ------------------------------------------------------------------