diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..63cc9e21 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,115 @@ +# 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 `docs/` 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 | +|------------|------|-------|-------------| +| `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 `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 +│ ├── 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/docs/autorizador_nfse_map.md b/docs/autorizador_nfse_map.md new file mode 100644 index 00000000..3144779e --- /dev/null +++ b/docs/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/docs/comunicacao_map.md b/docs/comunicacao_map.md new file mode 100644 index 00000000..81288a73 --- /dev/null +++ b/docs/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/docs/evento_map.md b/docs/evento_map.md new file mode 100644 index 00000000..844e8bb4 --- /dev/null +++ b/docs/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/docs/flags_map.md b/docs/flags_map.md new file mode 100644 index 00000000..c76fc581 --- /dev/null +++ b/docs/flags_map.md @@ -0,0 +1,38 @@ +# Source Map: `flags.py` (681 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-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 + +| 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/docs/manifesto_map.md b/docs/manifesto_map.md new file mode 100644 index 00000000..0c07f459 --- /dev/null +++ b/docs/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/docs/notafiscal_map.md b/docs/notafiscal_map.md new file mode 100644 index 00000000..fcf8c0a9 --- /dev/null +++ b/docs/notafiscal_map.md @@ -0,0 +1,81 @@ +# Source Map: `notafiscal.py` (1303 lines) + +Entity models for NF-e/NFC-e invoices and related objects (products, taxes, payments, transport, etc.). + +## Classes Overview + +| Class | Lines | Purpose | +|-------|-------|---------| +| `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-593 + +### 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_* | +| **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__()` | 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 626-1071 + +### Field Groups +| Section | Lines | Fields | +|---------|-------|--------| +| 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 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 new file mode 100644 index 00000000..cae67eb2 --- /dev/null +++ b/docs/reforma_tributaria.md @@ -0,0 +1,393 @@ +# Reforma Tributaria - IBS/CBS (NT 2025.002-RTC) + +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 substitui gradualmente cinco tributos por um modelo de IVA Dual: + +| 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 — 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) na mesma NF-e. + +## Escopo da implementacao + +A implementacao cobre: + +- 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, 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 — CST de 3 digitos (compartilham a mesma tabela) + +```python +from pynfe.utils.flags import IBS_CBS_TIPOS_TRIBUTACAO +``` + +| CST | Descricao | +|-----|-----------| +| 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 +``` + +| 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: produto com IBSCBS (tributacao integral CST 000) + +```python +from decimal import Decimal + +nota_fiscal.adicionar_produto_servico( + # ... campos obrigatorios do produto ... + 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"), + + # 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 (CST 222) + +```python +nota_fiscal.adicionar_produto_servico( + # ... campos do produto ... + + # 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 `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 = 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 | +|-------|------|-----------| +| `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 | 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()`. + +> **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 + +### Item — dentro de `` + +O grupo `` e adicionado como filho direto de ``, apos ``: + +```xml + + ... + + ... + ... + ... + + 000 + 000001 + + 1000.00 + + 0.1000 + 1.00 + + + 0.0000 + 0.00 + + 1.00 + + 0.9000 + 9.00 + + + + + +``` + +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 + + + + 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 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 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 new file mode 100644 index 00000000..f4594710 --- /dev/null +++ b/docs/serializacao_map.md @@ -0,0 +1,133 @@ +# Source Map: `serializacao.py` (2771 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-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 | + +--- + +## `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-1860 + +### Exported Methods +| Method | Lines | Purpose | +|--------|-------|---------| +| `exportar()` | 71-97 | Main entry: exports NF-e XML from data source | +| `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 | +|--------|-------|---------| +| `_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-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()` | 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 | 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()` | 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) | 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 2102-2206 + +Generates NFC-e QR Code URL. Handles online/offline modes and state-specific URL patterns (SP, BA, MG, etc.). + +## `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 2278-2301 + +Generates MDF-e QR Code URL using SVRS endpoint. + +## `SerializacaoMDFe` — Lines 2304-2771 + +### Methods +| Method | Lines | Purpose | +|--------|-------|---------| +| `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/docs/utils_map.md b/docs/utils_map.md new file mode 100644 index 00000000..a4ca2e61 --- /dev/null +++ b/docs/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/docs/webservices_map.md b/docs/webservices_map.md new file mode 100644 index 00000000..5ab7bc9e --- /dev/null +++ b/docs/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) | diff --git a/pynfe/entidades/notafiscal.py b/pynfe/entidades/notafiscal.py index e458d6d5..7047138e 100644 --- a/pynfe/entidades/notafiscal.py +++ b/pynfe/entidades/notafiscal.py @@ -302,6 +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 (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); @@ -464,9 +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 (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 @@ -1002,6 +1022,37 @@ class NotaFiscalProduto(Entidade): # - Valor imposto de importacao imposto_importacao_valor = Decimal() + # ============================================= + # Reforma Tributaria - IVA Dual (NT 2025.002-RTC) + # ============================================= + + # 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 informacoes_adicionais = str() diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index 7ab4042e..5cf123cc 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,91 @@ def _serializar_imposto_importacao( produto_servico.imposto_importacao_valor_iof ) + # ============================================= + # 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 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_ibscbs: + return + + self._serializar_ibscbs(produto_servico, tag_raiz) + + # IS: descomentar quando schema suportar (previsto para 2027) + # if produto_servico.is_cst_selec: + # self._serializar_is(produto_servico, tag_raiz) + + 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 produto_servico.ibscbs_c_class_trib: + etree.SubElement(ibscbs, "cClassTrib").text = produto_servico.ibscbs_c_class_trib + + if produto_servico.ibscbs_cst in self._IBSCBS_CST_TRIBUTADOS: + gibscbs = etree.SubElement(ibscbs, "gIBSCBS") + + 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 + ) + + # 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 + ) + + # vIBS total + etree.SubElement(gibscbs, "vIBS").text = "{:.2f}".format( + produto_servico.ibscbs_v_ibs or 0 + ) + + # gCBS + gcbs = etree.SubElement(gibscbs, "gCBS") + etree.SubElement(gcbs, "pCBS").text = "{:.4f}".format(produto_servico.ibscbs_p_cbs or 0) + etree.SubElement(gcbs, "vCBS").text = "{:.2f}".format(produto_servico.ibscbs_v_cbs or 0) + + def _serializar_is(self, produto_servico, tag_raiz): + """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, "CSTIS").text = produto_servico.is_cst_selec + + 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) + def _serializar_declaracao_importacao( self, produto_servico, tag_raiz="prod", retorna_string=True ): @@ -1462,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); @@ -1690,6 +1785,51 @@ def _serializar_nota_fiscal(self, nota_fiscal, tag_raiz="infNFe", retorna_string nota_fiscal.totais_tributos_aproximado ) + # 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") etree.SubElement(transp, "modFrete").text = str(nota_fiscal.transporte_modalidade_frete) diff --git a/pynfe/utils/flags.py b/pynfe/utils/flags.py index 58519687..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 = ( @@ -545,6 +547,40 @@ COFINS_TIPOS_CALCULO = IPI_TIPOS_CALCULO +# ============================================= +# Reforma Tributaria - IVA Dual (EC 132/2023) +# ============================================= + +# CST para IBSCBS (IBS + CBS) — NT 2025.002-RTC (3-digit codes) +IBS_CBS_TIPOS_TRIBUTACAO = ( + ("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) — CSTSelec (NT 2025.002-RTC) +IS_TIPOS_TRIBUTACAO = ( + ("01", "Tributada integralmente"), + ("02", "Tributada com redução"), + ("03", "Isenção"), + ("04", "Imunidade"), + ("05", "Suspensão"), + ("06", "Diferimento"), + ("90", "Outros"), +) + MODALIDADES_FRETE = ( (0, "0 - Contratação por conta do Remetente (CIF)"), (1, "1 - Por conta do destinatário"), diff --git a/tests/test_nfe_serializacao_reforma_tributaria.py b/tests/test_nfe_serializacao_reforma_tributaria.py new file mode 100644 index 00000000..ee042b8a --- /dev/null +++ b/tests/test_nfe_serializacao_reforma_tributaria.py @@ -0,0 +1,691 @@ +#!/usr/bin/env python +# *-* encoding: utf8 *-* + +"""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 +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 IBSCBS (Reforma Tributaria) XML serialization per NT 2025.002-RTC.""" + + 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 _base_product_kwargs(self): + """Common product kwargs without reforma tributaria fields.""" + return dict( + 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", + ) + + 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() + + # is direct child of (no impostoMisto wrapper) + ibscbs = xml.xpath("//ns:det/ns:imposto/ns:IBSCBS", namespaces=self.ns) + self.assertEqual(len(ibscbs), 1) + + # No impostoMisto wrapper + imposto_misto = xml.xpath("//ns:det/ns:imposto/ns:impostoMisto", namespaces=self.ns) + self.assertEqual(len(imposto_misto), 0) + + # CST is 3-digit + cst = xml.xpath("//ns:IBSCBS/ns:CST", namespaces=self.ns)[0].text + self.assertEqual(cst, "000") + + # cClassTrib present + cclass = xml.xpath("//ns:IBSCBS/ns:cClassTrib", namespaces=self.ns)[0].text + self.assertEqual(cclass, "000001") + + # Shared vBC at gIBSCBS level + vbc = xml.xpath("//ns:IBSCBS/ns:gIBSCBS/ns:vBC", namespaces=self.ns)[0].text + self.assertEqual(vbc, "1000.00") + + # gIBSUF + 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 + 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 + 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 + v_ibs = xml.xpath("//ns:IBSCBS/ns:gIBSCBS/ns:vIBS", namespaces=self.ns)[0].text + self.assertEqual(v_ibs, "1.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") + + # 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 222 — isenção (no vBC, values zero) + # ------------------------------------------------------------------ + def test_cst222_isencao_sem_valores(self): + emitente = self._emitente() + cliente = self._cliente() + nf = self._nota_fiscal(emitente, cliente) + + kwargs = self._base_product_kwargs() + kwargs.update( + codigo="002", + descricao="Produto isento reforma tributaria", + quantidade_comercial=Decimal("1"), + valor_unitario_comercial=Decimal("50.00"), + valor_total_bruto=Decimal("50.00"), + quantidade_tributavel=Decimal("1"), + valor_unitario_tributavel=Decimal("50.00"), + 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() + + # IBSCBS present even if exempt + ibscbs = xml.xpath("//ns:det/ns:imposto/ns:IBSCBS", namespaces=self.ns) + self.assertEqual(len(ibscbs), 1) + + # CST present + cst = xml.xpath("//ns:IBSCBS/ns:CST", namespaces=self.ns)[0].text + self.assertEqual(cst, "222") + + # cClassTrib present + cclass = xml.xpath("//ns:IBSCBS/ns:cClassTrib", namespaces=self.ns)[0].text + self.assertEqual(cclass, "000002") + + # 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:IS", namespaces=self.ns) + self.assertEqual(len(is_group), 0) + + # ------------------------------------------------------------------ + # Test 3: No reforma data — IBSCBS not emitted + # ------------------------------------------------------------------ + def test_sem_reforma_tributaria_sem_ibscbs(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() + + # 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 IBSCBSTot + ibscbs_tot = xml.xpath("//ns:total/ns:IBSCBSTot", namespaces=self.ns) + self.assertEqual(len(ibscbs_tot), 0) + + # ------------------------------------------------------------------ + # 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) + + 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", + 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: 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", + quantidade_comercial=Decimal("5"), + valor_unitario_comercial=Decimal("100.00"), + valor_total_bruto=Decimal("500.00"), + quantidade_tributavel=Decimal("5"), + valor_unitario_tributavel=Decimal("100.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=1500.00, ind_pag=0) + + xml = self._serializar_e_assinar() + + # Totals in IBSCBSTot (NOT ICMSTot) + ibscbs_tot = xml.xpath("//ns:total/ns:IBSCBSTot", namespaces=self.ns) + self.assertEqual(len(ibscbs_tot), 1) + + # 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") + + # 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") + + 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 (ICMS/PIS/COFINS) + reform (IBSCBS) coexistence + # ------------------------------------------------------------------ + def test_misto_legacy_icms_pis_cofins_com_ibscbs(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 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=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 + 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 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) + + cst = xml.xpath("//ns:IBSCBS/ns:CST", namespaces=self.ns)[0].text + self.assertEqual(cst, "010") + + # 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) + + # ------------------------------------------------------------------ + # 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()