Skip to content

feat(danfe): ajustes Revenda Mais (CST/CSOSN + Forma de Pagamento + Marca Cancelada)#8

Merged
adrbarros merged 1 commit into
adrbarros:mainfrom
nfe:main
May 29, 2026
Merged

feat(danfe): ajustes Revenda Mais (CST/CSOSN + Forma de Pagamento + Marca Cancelada)#8
adrbarros merged 1 commit into
adrbarros:mainfrom
nfe:main

Conversation

@adrbarros

Copy link
Copy Markdown
Owner

ajustes Revenda Mais (CST/CSOSN + Forma de Pagamento + Marca Cancelada) (nfe#43)

Initialize OpenSpec (--tools=claude) and propose change covering the ICMS column rendering bug reported in nfe#38:

  • Concatenation without separator (e.g. "120" instead of "1/20") in DanfeViewModelCreator.cs:497
  • Static header label "O/CSOSN" derived from Emitente.CRT only, fragile when CRT is missing in the XML

Artifacts in openspec/changes/fix-danfe-csosn-rendering/:

  • proposal.md: motivation, capabilities, impact
  • design.md: 4 decisions anchored on real code (item-derived header, separate Origem/Cst/Csosn properties, OCst as computed, no feature flag)
  • specs/danfe-icms-column/spec.md: 3 requirements + scenarios
  • tasks.md: 9 task groups (investigation, model, producer, renderer, tests, visual validation, docs, PR, archive)

No code changes yet — implementation lands via /opsx:apply on a follow-up commit.

Corrige dois sintomas reportados pelo cliente Revenda Mais na coluna "O/CSOSN" do bloco produtos da DANFE:

  1. Origem e código (CST/CSOSN) concatenados sem separador ("120" em vez de "1/20"). Causa: produto.OCst = icms.orig + icms.CST + icms.CSOSN em DanfeViewModelCreator.cs:497.

  2. Cabeçalho "O/CSOSN" mesmo para emitentes do Regime Normal quando <emit><CRT> está ausente/inconsistente no XML. Causa: lógica dependia só de Emitente.CRT == "3".

Mudanças

  • ProdutoViewModel ganha Origem/Cst/Csosn separados. OCst vira propriedade calculada que aplica "/<código>" e omite componentes vazios (XMLs sem exibem só o código).
  • ProdutoViewModel.CalcularCabecalhoColunaIcms(produtos) — helper estático que decide o cabeçalho inspecionando os itens da nota (se algum tem CST → "O/CST"; senão CSOSN → "O/CSOSN"; fallback "O/CST"). Mais robusto que confiar em Emitente.CRT.
  • DanfeViewModelCreator popula Origem/Cst/Csosn normalizando strings vazias do XmlSerializer para null.
  • TabelaProdutosServicos consome o helper.

Testes

DanfeSharp.Test/ProdutoIcmsColumnTests.cs: 15 testes (8 de célula, 7 de cabeçalho), incluindo regressão do caso Revenda Mais (CRT ausente + CST nos itens → "O/CST"). 15/15 verdes via dotnet vstest.

Hotfix tangencial (pre-existente em origin/main, bloqueava build do projeto de teste)

  • FabricaFake.cs:51: cast (decimal?)v para vFCPUFDest.
  • DanfeTest.cs:63: DanfeCCC renomeado para DanfeCartaCorrecao (rename antigo não propagado).

Pendente (manual)

  • §1 pre-flight: confirmar com Carolina que separador é / e grep cross-repo por ProdutoViewModel.OCst.
  • §6 validação visual: gerar DANFE da invoice de referência do Revenda Mais e comparar PDF antes/depois.

OpenSpec change: openspec/changes/fix-danfe-csosn-rendering/
Spec validated: openspec validate OK.

Fixes nfe#38

Após leitura literal do MOC 7.0 Anexo II (PDF oficial do CONFAZ), constatei que:

  • §3.1.7 (página 11) lista a coluna "CST" como obrigatória no quadro "Dados dos Produtos/Serviços", mas NÃO prescreve literalmente o formato combinado "O/CST" nem o separador "/".
  • O campo faz parte do schema do XML (Anexo I) mas o Anexo II não menciona apresentação visual conjunta com CST/CSOSN.
  • O formato "O/CST" com separador "/" é convenção universal de mercado (TOTVS, SAP, SmartGo, eMissor, etc.) — não mandato literal.

Calibragem aplicada em:

  • proposal.md (Why): substituída "conformidade visual com MOC v7.0+" por "convenção universal de mercado", com referência §3.1.7 para a parte que é mandato (coluna CST).
  • design.md (Decision 3): nova seção "Base normativa" com URLs do CONFAZ para Anexo I + Anexo II. Open Question sobre separador marcada como resolvida.
  • specs/danfe-icms-column/spec.md: nota normativa no topo do bloco ADDED Requirements explicando o que é mandato literal vs convenção.
  • tasks.md §1.2: contexto expandido — separador "/" é convenção universal, não MOC literal.

Sem mudança de código nem de comportamento; só de citação/justificativa. Implementation alinhada com convenção do mercado, que é o que receptores fiscais esperam ver (raiz do problema com cliente Revenda Mais).

Fixes nfe#38 (continued)

  • chore(openspec): archive fix-danfe-csosn-rendering → specs/danfe-icms-column
  • Move openspec/changes/fix-danfe-csosn-rendering/ → openspec/changes/archive/2026-05-28-fix-danfe-csosn-rendering/
  • Promote ADDED requirements to openspec/specs/danfe-icms-column/spec.md
  • Preencher Purpose com base normativa (MOC 7.0 §3.1.7) — substituindo o boilerplate "TBD" gerado pelo CLI

A spec danfe-icms-column passa a ser fonte de verdade para qualquer mudança futura na coluna ICMS da DANFE. Mudanças subsequentes devem ser propostas como MODIFIED/ADDED via /opsx:new ou /opsx:propose.

Note: 11 tasks ficaram incompletas no archive (manuais/externas — §1.1 grep cross-repo, §1.2 confirmar com Carolina, §4.3 validar largura visual, §6 validação visual com XML real do Revenda Mais, §8 review + merge, §9 archive já feita aqui). O archive forçou via --yes flag por se tratar de tarefas extra-código.

Fixes nfe#38 (continued)

Initialize OpenSpec na branch (igual a nfe#38) e propor o change cobrindo a implementação do bloco "Forma de Pagamento" no DANFE de NF-e modelo 55:

PROBLEMA RAIZ

  • DanfeSharp não renderiza bloco visual de pagamento mesmo quando o XML traz preenchido (NT 2016.002 v1.50 tornou obrigatório desde 2018).
  • Cliente Revenda Mais reportou via Carolina Fagundes / Teams 2026-05-27; invoice 9175b02ac0cf4ed898025c4bad09e2fe.

ESTADO DO CÓDIGO (auditado)

  • Schema XML: pag/detPag/FormaPagamento enum já existem (ProcNFe.cs) com Description em cada tPag (01..19, 90, 99); FALTA xPag em detPag.
  • ViewModel: PagamentoViewModel + DetalheViewModel + CartaoViewModel já existem; FALTA Descricao em DetalheViewModel.
  • Producer (DanfeViewModelCreator) já popula model.Pagamento; falta apenas mapear xPag → Descricao.
  • FALTA renderer visual (BlocoFormaPagamento.cs).
  • FALTA inserção no fluxo do construtor de Danfe.cs.

BASE NORMATIVA CALIBRADA (lição aprendida do nfe#38)

  • MOC 7.0 Anexo II não tem seção dedicada a forma de pagamento para DANFE NF-e modelo 55 (verifiquei literalmente o PDF do CONFAZ; zero ocorrências de "pagamento"/"detPag"/"tPag" no texto).
  • NT 2016.002 obrigou o grupo no XML; o manual do DANFE NFCe (Anexo III) mandou exibir o bloco no cupom da NFCe.
  • Para NF-e modelo 55, o bloco visual é CONVENÇÃO UNIVERSAL DE MERCADO (TOTVS, SAP, SmartGo, eMissor), não mandato literal. Spec deixa isso explícito.

ARTEFATOS

  • proposal.md (5.9 KB): motivação + capability danfe-payment-block + impacto
  • design.md (14 KB): 6 decisões ancoradas no código + open questions
  • specs/danfe-payment-block/spec.md (8.2 KB): 8 requirements + 18 cenários BDD
  • tasks.md (8.3 KB): 13 grupos (32 itens) cobrindo schema, viewmodel, producer, helper, renderer, fluxo, testes, fixtures, validação visual, docs, PR, archive

openspec validate OK.

Nenhuma mudança de código ainda — implementação virá via /opsx:apply no próximo commit.

Fixes nfe#39

Adiciona o bloco "Forma de Pagamento" na DANFE de NF-e modelo 55, lendo o grupo do XML. Demanda do cliente Revenda Mais (invoice 9175b02ac0cf4ed898025c4bad09e2fe), umbrella nfe#37.

MUDANÇAS

Schema (DanfeSharp/Esquemas/ProcNFe.cs)

  • detPag ganha campo xPag (YA03 — descrição do pagamento).

ViewModel (DanfeSharp/Modelo/PagamentoViewModel.cs)

  • DetalheViewModel ganha propriedade Descricao (mapeada de xPag).

Producer (DanfeSharp/Modelo/DanfeViewModelCreator.cs)

  • Producer existente para em CreateFromProcNFCe ganha população de Descricao (xPag → Descricao com normalização "" → null).
  • BUG RAIZ ADICIONAL: a população do grupo estava SÓ em CreateFromProcNFCe (NFC-e modelo 65) — faltava em CreateFromProcNFe (NF-e modelo 55, escopo desta change). Adicionado o foreach no CreateFromProcNFe antes do return — sem essa correção o renderer nunca dispararia para NF-e.

Helper (DanfeSharp/Modelo/FormaPagamentoExtensions.cs — novo)

  • GetDescricao(this FormaPagamento) lê [DescriptionAttribute] via reflexão. Retorna string.Empty para enum value inválido (defensivo).

Renderer (DanfeSharp/Blocos/BlocoFormaPagamento.cs — novo)

  • Herda BlocoBase. Cabecalho "Forma de Pagamento". Itera sobre Pagamento[].DetalhePagamento via SelectMany e cria uma LinhaCampos por detalhe com FORMA PAGAMENTO (Descricao ?? GetDescricao) + VALOR (ComCampoNumerico, sem prefixo R$ — convenção do BlocoCalculoImposto).
  • Auto-omissão: early return quando detalhes == null/empty.

Sequência (DanfeSharp/Danfe.cs)

  • AdicionarBloco() inserido entre BlocoCalculoImposto e BlocoTransportador.
  • Condicionado a Pagamento ser não-vazio (defense in depth).

TESTES (DanfeSharp.Test/FormaPagamentoTests.cs — novo)

  • 14 testes em FormaPagamentoTests: helper para Dinheiro/CartaoCredito/ CartaoDebito/Outro/PIX/inválido, schema parsing com/sem , ViewModel propertes, regra Descricao-prevalece-sobre-enum.
  • 2 testes de integração em IntegracaoPagamentoNFe: NF-e v4 popula model.Pagamento (regressão do bug raiz §4.2); NF-e v3.10 sem não quebra e mantém Pagamento vazio.
  • Aliases FormaPagamentoVm/FormaPagamentoSchema para desambiguar os 2 enums coexistentes (Modelo/Enums.cs e Esquemas/ProcNFe.cs).
  • DanfeSharp.Test.csproj: para acessar XmlSerializer nos testes de schema.

dotnet vstest:

  • FormaPagamentoTests + IntegracaoPagamentoNFe: 16/16 verde (748 ms)
  • DanfeXmlTests: 5/5 verde (zero regressão)
  • PDF re-renderizado do v4_ComLocalEntrega.xml exibe o bloco no local esperado (texto extraído: "FORMA DE PAGAMENTO ... VALOR ... Outros ... 3.977,00").

HOTFIX TANGENCIAL (pré-existente em origin/main)

OpenSpec change: openspec/changes/add-danfe-payment-block/ Spec validated: openspec validate OK.
38 tasks; tasks externas/manuais (§1, §10, §12, §13) seguem pendentes do lado humano (cliente + reviewer + post-merge archive).

Fixes nfe#39

  • refactor(danfe): layout horizontal dos campos da Duplicata (FATURA / DUPLICATA)

Reportado pelo cliente Revenda Mais durante revisão do PR nfe#42: o quadro "FATURA / DUPLICATA" exibia Número / Vencimento / Valor empilhados verticalmente dentro de cada célula, ocupando 3× mais altura que o necessário. Refatora para layout horizontal, alinhando com o padrão visual do BlocoCalculoImposto e demais blocos com campos labelados.

Mudanças

  • DanfeSharp/Elementos/Duplicata.cs: o Draw passa a dividir o bounding rect em 3 colunas iguais e desenhar cada par (label no topo + valor abaixo) lado a lado em vez de empilhar com CutTop. Height reduzida de 3linha → 2linha (label + valor).

  • DanfeSharp/Blocos/BlocoDuplicataFatura.cs: numeroElementosLinha reduzido de 6→3 (retrato) e 7→4 (paisagem) para acomodar a largura necessária por célula no novo layout. Sem essa redução, os valores ("11/10/2018", "R$ 3.977,00") seriam cortados.

Validação visual

  • v4_ComLocalEntrega.pdf re-renderizado. Texto extraído por pdftotext:
    Número Vencimento: Valor:
    001 11/10/2018 R$ 3.977,00
    (em uma única linha, 3 colunas legíveis)

  • dotnet vstest DanfeXmlTests: 5/5 aprovados, zero regressão.

Fora do escopo original do PR nfe#42 (que cobre o bloco FORMA DE PAGAMENTO). Aplicado como hotfix de polish a pedido do criador da PR, conforme feedback durante revisão visual da DANFE rendererizada.

Base normativa: o MOC 7.0 Anexo II §3.1.6 ("Quadro Fatura/Duplicatas") não prescreve layout específico para os campos internos da duplicata — "Poderá conter linhas divisórias internas separando as informações." Layout escolhido alinha com a convenção dos demais blocos do DanfeSharp.

Refs nfe#39

  • test(danfe): fixture v4_RevendaMaisDemo para visualizar ambos os ajustes

XML sintético derivado de v4_ComLocalEntrega.xml mas com cobr/pag expandidos para exercitar todos os caminhos novos:

FATURA / DUPLICATA (validação do refactor 371ed39)

  • 6 duplicatas mensais R$ 662,83 cada (sum 3.977,00 igual ao vLiq da fat). Demonstra o novo layout horizontal de campos + paginação em 2 linhas de 3 duplicatas (numeroElementosLinha=3 em retrato).

FORMA DE PAGAMENTO (validação da feature aaf6e9f)

  • 3 variados: • tPag=17 (PIX, NT 2020.001) — R$ 1.500,00 → renderiza "Pagamento Instantâneo (PIX)" via fallback do enum • tPag=03 (Cartão de Crédito) + grupo — R$ 1.000,00 → renderiza "Cartão de Crédito" via fallback do enum • tPag=99 (Outros) + VALE TROCO — R$ 1.477,00 → renderiza "VALE TROCO" (xPag prevalecendo sobre "Outros", regra da spec danfe-payment-block)

Soma dos detPag = 3.977,00 = soma das duplicatas = vNF (consistente).

Wiring

  • v4_RevendaMaisDemo.xml em DanfeSharp.Test/Xml/NFe/v4.00/
  • + CopyToOutputDirectory em DanfeSharp.Test.csproj
  • DanfeXmlTests.v4_RevendaMaisDemo() chama TestXml(...) que gera Output/DeXml/v4_RevendaMaisDemo.pdf

Validação

  • dotnet vstest da suite completa (DanfeXmlTests + FormaPagamentoTests
    • IntegracaoPagamentoNFe) → 22/22 verde.
  • Texto extraído do PDF confirma: FATURA / DUPLICATA: 6 duplicatas em 2 linhas, layout horizontal. FORMA DE PAGAMENTO: 3 linhas com FormaPagamento + Valor distintos.

Refs nfe#39

  • test(fixture): adiciona em 2 produtos do demo

Para visualizar como a tag (informações adicionais do produto, YA02 do MOC) é renderizada na DANFE:

  • Item Update #1 (SOFTWARE A): Licença digital · Não-reembolsável
    · S/N: ABC123-VERSAO-3.0
  • Item Update #2 (SOFTWARE B): Garantia: 12 meses contra defeitos
    de fabricação · Suporte: 0800-XXX-YYYY

A informação aparece logo abaixo do nome do produto, dentro da mesma célula da coluna "DESCRIÇÃO DO PRODUTO / SERVIÇO" (concatenada via ProdutoViewModel.DescricaoCompleta com separador \r\n). Conformidade com MOC 7.0 Anexo II §3.1.7 (pg 10): "As informações adicionais de produto deverão constar impressas no DANFE logo abaixo do item ao qual se referirem."

Validação: dotnet vstest v4_RevendaMaisDemo → aprovado; texto extraído por pdftotext confirma render correto com line wrap automático em strings longas.

Refs nfe#39

  • style(danfe): rename QUANTI.→QUANT. e diminui coluna para favorecer DESCRIÇÃO

Pedido durante revisão visual da PR nfe#42:

  • Renomear o cabeçalho da coluna Quantidade de "QUANTI." para "QUANT."
  • Diminuir a largura da coluna (sobrava espaço pra abreviação mais curta)
  • Devolver o espaço ganho à coluna DESCRIÇÃO DO PRODUTO / SERVIÇO (que é flex, width=0 — absorve automaticamente)

Mudanças em DanfeSharp/Blocos/TabelaProdutosServicos.cs:

  • Retrato: 6F → 4F (-2F devolvidos à descrição)
  • Paisagem: 5.25F → 3.75F (-1.5F devolvidos à descrição)
  • Label em ambos os modos: "QUANTI." → "QUANT."

Validação: dotnet vstest DanfeXmlTests → 6/6 verde, zero regressão. Texto extraído do PDF confirma cabeçalho "QUANT." e descrição mais larga.

Refs nfe#39

  • style(danfe): diminui coluna NCM/SH em favor da DESCRIÇÃO

Continuação do ajuste visual da PR nfe#42:

  • NCM/SH: o conteúdo é sempre 8 dígitos (49111090) — não precisa do espaço atual. Diminuir e devolver à coluna DESCRIÇÃO (flex).

Retrato: 5.6F → 4.5F (-1.1F devolvidos à descrição)
Paisagem: 5.5F → 4.5F (-1.0F devolvido à descrição)

Validação: dotnet vstest DanfeXmlTests → 6/6 verde.

Refs nfe#39

  • fix(danfe): ajusta largura da coluna NCM/SH (4.5F → 5F)

O commit anterior (c72ebab) reduziu NCM/SH para 4.5F, mas a coluna ficou apertada demais — o último dígito do NCM de 8 dígitos era truncado (49111090 virava 4911109). Aumenta para 5F (ainda ganha 0.6F vs original em retrato, 0.5F em paisagem) garantindo que NCMs de 8 dígitos rendereizem completos.

Refs nfe#39

  • style(danfe): ajuste fino paisagem QUANT. 3.75F → 4.00F

Pedido durante review da PR nfe#42: dar +0.25F à coluna QUANT. em modo paisagem (que estava apertada visualmente). DESCRIÇÃO (flex, width=0) absorve o ajuste automaticamente cedendo 0.25F.

Refs nfe#39

  • test(danfe): adiciona test methods para paisagem (demo + light fixture)

Pedido durante review da PR nfe#42: gerar exemplos completos em retrato e paisagem. Adiciona 2 test methods em DanfeXmlTests:

  • v4_RevendaMaisDemo_Paisagem: força Orientacao=Paisagem na fixture rica. Atualmente FALHA com "Height is invalid" — overflow vertical porque a fixture tem 6 duplicatas + 3 detPag que não cabem em A4 paisagem (mais baixa que retrato). Documentado como bug pre-existente do layout engine (mesmo problema dos testes Paisagem*/FabricaFake que já falhavam em main).

  • v4_ComLocalEntrega_Paisagem: força paisagem na fixture leve (1 duplicata + 1 detPag) — funciona. Demonstra que a renderização paisagem em si está OK; falha do demo é por excesso de conteúdo.

DanfeViewModelCreator.cs:411 ainda hardcoda Orientacao=Retrato (comentário sobre netcore2.0 bug). Esses tests forçam override após o load.

Refs nfe#39

  • fix(danfe): paginar quando blocos topo overflow vertical (em vez de crash)

Bug: Danfe.Gerar() lançava "InvalidOperationException: Height is invalid" quando os blocos topo (Emitente, Destinatário, Local Entrega/Retirada, Fatura, Cálculo, Forma de Pagamento, Transportador, Dados Adicionais) consumiam toda a altura disponível da página — situação comum em paisagem com NF-e ricas (caso típico do cliente Revenda Mais com 6 duplicatas + 3 detPag + bloco FORMA).

Root cause: o while loop em Gerar() já suportava paginação via TabelaProdutosServicos.CompletamenteDesenhada, mas faltava um guard para casos onde RetanguloCorpo.Height ≤ 0 — ele tentava desenhar mesmo assim e o DrawableBase.Draw lançava.

Fix: adiciona guard em Gerar() — quando RetanguloCorpo.Height ≤ 0, pula a página sem desenhar a tabela. A próxima iteração cria uma nova página onde apenas BlocoIdentificacaoEmitente é desenhado (único com VisivelSomentePrimeiraPagina=false), liberando espaço para a tabela.

Proteção contra loop infinito: se 2 páginas consecutivas têm Height ≤ 0, lança erro descritivo orientando reduzir blocos opcionais ou usar Retrato.

Validação:

  • v4_RevendaMaisDemo_Paisagem (heavy: 6 duplicatas + 3 detPag) — antes lançava NPE, agora gera PDF com 2 páginas: pág 1 = blocos topo, pág 2 = identificação + tabela produtos.
  • v4_ComLocalEntrega_Paisagem (light) — continua passando.
  • Retrato continua passando para todas as fixtures.
  • Falhas pre-existentes em FabricaFake-based Paisagem tests (PaisagemSemIcmsInterestadual etc.) não são afetadas — são causadas por bug independente (TextoReservadoFisco():355 NotImplementedException por TipoEmissao=0 default no fake).

Refs nfe#39

  • test(fixtures): adiciona 3 demos cobrindo cenários extremos + intermediário

Para visualizar a renderização da DANFE em variações de complexidade (pedido durante review da PR nfe#42):

  • v4_DanfeMinimo.xml — 1 item sem infAdProd, sem , sem . Caso de NF-e mínima (ex.: doação, brinde) sem fatura/pagamento. PDF gerado: 5.8 KB, 1 página.

  • v4_DanfeIntermediario.xml — 5 itens (3 com infAdProd: notebook, teclado, monitor; 2 sem: mouse, cabo), 2 duplicatas R$ 500 cada, 2 detPag (Cartão Crédito R$ 600 + PIX R$ 400). Total R$ 1000. PDF: 6.7 KB.

  • v4_DanfeCompleto.xml — 20 itens (todos com infAdProd "Lote LT-N-2026 · Validade 12 meses · Origem: Brasil"), 6 duplicatas mensais R$ 333.34 cada, 3 detPag (PIX 800 + Cartão Crédito 700 + Outros R$ 500 com xPag="PROMOCIONAL BLACK FRIDAY"). Total R$ 2000. PDF: 11 KB, múltiplas páginas (paginação automática graças ao fix em Danfe.Gerar()).

Geração via script reproduzível (generate-fixtures.sh) com helpers para gen_item, gen_dup, gen_detpag, gen_header, gen_footer. Reexecutável: bash DanfeSharp.Test/Xml/NFe/v4.00/generate-fixtures.sh

Wiring

  • Content + CopyToOutputDirectory em DanfeSharp.Test.csproj
  • 3 test methods em DanfeXmlTests (v4_DanfeMinimo, v4_DanfeIntermediario, v4_DanfeCompleto) que chamam TestXml(...) para gerar PDFs em bin/Debug/Output/DeXml/

Validação

  • dotnet vstest dos 3 → todos aprovados.
  • Sanity checks via pdftotext confirmam contagens esperadas: EX1: 0 FATURA, 0 FORMA, 1 PROD-, 0 infAdProd EX2: 1 FATURA, 1 FORMA, 5 PROD- EX3: 1 FATURA, 1 FORMA, 20 PROD-, 20 "Lote" (infAdProd)

Refs nfe#39

  • test(fixtures): adiciona 3 demos em paisagem (Mínimo + Intermediário + Completo)

Mesmas fixtures do commit anterior, agora renderizadas em paisagem para visualização completa do layout em ambos os modos.

Adicionado helper RenderPaisagem(xmlPath, outName) que carrega o XML e força Orientacao = Paisagem antes de gerar (necessário porque DanfeViewModelCreator hardcoda Retrato — comentário sobre netcore2.0 em DanfeViewModelCreator.cs:411).

3 novos test methods:

  • v4_DanfeMinimo_Paisagem
  • v4_DanfeIntermediario_Paisagem
  • v4_DanfeCompleto_Paisagem (20 itens em paisagem — paginação automática graças ao fix do bug "Height is invalid" aplicado anteriormente)

Todos os 3 passam. PDFs gerados em bin/Debug/Output/DeXml/ com sufixo _Paisagem.

Refs nfe#39

Tema 3 do umbrella Revenda Mais (issue nfe#37). Renderiza marca d'água "DOCUMENTO CANCELADO" centralizada em todas as páginas da DANFE quando a NF-e está cancelada.

ARQUITETURA — primeiro detectar via flag, depois cStat=101 como fallback

O cancelamento em NF-e modelo 55 é registrado via EVENTO SEPARADO (NFeProcEvento com tpEvento=110111), NÃO via mudança no da NF-e original — o XML protocolo da nota mantém cStat=100 (Autorizado) mesmo após cancelamento autorizado. Quem sabe que a nota foi cancelada é o sistema do emissor consultando o histórico de eventos.

A PR #8 do @mateuszanini (2023, branch feature/aviso-cancelamento) tinha a renderização visualmente correta mas detectava via cStat=101 do XML protocolo — que na prática raramente é 101 para uma nota cancelada. Esta change reaproveita o método dele (DesenharAvisoCancelamento) mas usa flag explícita IsCancelled como mecanismo primário + cStat=101 como fallback OR (zero custo).

CÓDIGO

  • DanfeSharp/Modelo/DanfeViewModel.cs:223 — nova propriedade IsCancelled (bool, default false) com XML doc explicando o motivo arquitetural (cancelamento via evento, não via cStat do XML).

  • DanfeSharp/DanfePagina.cs:106 — novo método DesenharAvisoCancelamento espelhando DesenharAvisoHomologacao: TextStack centralizado no RetanguloCorpo, fonte 48pt regular, cor RGB(0.35, 0.35, 0.35), texto "DOCUMENTO CANCELADO". Padrão visual idêntico ao "SEM VALOR FISCAL" já existente — vamos da PR feat(danfe): ajustes Revenda Mais (CST/CSOSN + Forma de Pagamento + Marca Cancelada) #8.

  • DanfeSharp/Danfe.cs:CriarPagina — após DesenharAvisoHomologacao, adiciona "if (ViewModel.IsCancelled || ViewModel.CodigoStatusReposta == 101) p.DesenharAvisoCancelamento();". Sobreposição com aviso de homologação aceita (NF-e cancelada em homologação → ambos os avisos aparecem, comportamento correto).

TESTES (DanfeCanceladaTests.cs, 11 tests)

  • Unit-level: IsCancelled default false, settable.
  • Integration: render com IsCancelled=true, com cStat=101, com ambos, e regressão sem nenhum trigger.
  • Demos: render dos 3 fixtures (Mínimo/Intermediário/Completo) + versões paisagem do Intermediário e Completo, todos com IsCancelled=true.
  • pdftotext -raw confirma "DOCUMENTO CANCELADO" presente em todos os 5 PDFs cancelados (1x em página única, 2x em multi-página do Completo), e 0 ocorrências em DANFEs não-canceladas.
  • DanfeXmlTests existentes: 14/14 verde (zero regressão).

BASE NORMATIVA

MOC 7.0 Anexo II §3.10.1 (Marca d'Água) autoriza literalmente: "O formulário poderá conter marca d'água desde que não prejudique a legibilidade dos dados impressos." NÃO obriga marca para nota cancelada, NÃO especifica texto/posição/opacidade. Texto e estilo são convenção universal de mercado (TOTVS/SAP/SmartGo/eMissor) alinhada ao "SEM VALOR FISCAL" do ambiente de homologação.

OpenSpec change: openspec/changes/add-cancelled-watermark/

  • proposal.md, design.md (5 decisões), specs/danfe-cancelled-watermark/spec.md (5 requirements + 11 cenários), tasks.md (10 grupos).
  • openspec validate ✓

Fixes nfe#40
Refs PR #8 (@mateuszanini, 2023 — render mantida, gatilho substituído)

  • refactor(danfe): bloco FORMA DE PAGAMENTO usa padrão visual de FATURA/DUPLICATA

Ajustes cosméticos pedidos durante review da PR nfe#43:

  1. Mover bloco FORMA DE PAGAMENTO para logo abaixo de FATURA/DUPLICATA
    (em Danfe.cs:CriarPagina). Ordem antiga:
    FATURA → CÁLCULO IMPOSTO → FORMA PAGAMENTO → TRANSPORTADOR
    Nova:
    FATURA → FORMA PAGAMENTO → CÁLCULO IMPOSTO → TRANSPORTADOR
    Agrupa os blocos de informação comercial (fatura + pagamento)
    adjacentes, separados dos fiscais (imposto + transportador).

  2. Refatorar BlocoFormaPagamento para mesmo padrão visual do
    BlocoDuplicataFatura — cards lado a lado em FlexibleLine, cada
    um com 2 sub-colunas (FORMA PAGAMENTO + VALOR), label em cima
    e valor abaixo. Antes era 1 linha por detalhe via
    AdicionarLinhaCampos (estilo do BlocoCalculoImposto).

    Novo elemento DanfeSharp/Elementos/PagamentoDetalhe.cs espelhando
    Duplicata.cs:

    • 2 colunas iguais (FORMA PAGAMENTO + VALOR) com label em FonteA e valor em FonteB (mesmas fontes da Duplicata).
    • Descricao prevalece sobre [Description] do enum (regra da spec danfe-payment-block já existente).
    • Valor formatado com R$ via Formatador.FormatarMoeda (cast decimal→double).

    BlocoFormaPagamento usa FlexibleLine com N cards por linha:

    • Retrato: 2 (descrições podem ser longas, ex. "Pagamento Instantâneo (PIX)" = 28 chars; 4 cards/linha colidia)
    • Paisagem: 3

Validação visual em demo Completo (retrato, 3 detPag):
Linha 1: PIX R$ 800,00 | Cartão de Crédito R$ 700,00
Linha 2: VALE TROCO R$ 500,00

Em paisagem, os 3 detPag cabem na mesma linha.

Testes: 56/56 verde (zero regressão em ProdutoIcmsColumnTests, FormaPagamentoTests, IntegracaoPagamentoNFe, DanfeXmlTests, DanfeCanceladaTests). PDFs demo atualizados em
../danfe-payment-validation/{exemplos,exemplos-cancelada}/.

Refs nfe#39

  • style(danfe): Title Case nas labels de PagamentoDetalhe (espelha Duplicata)

Pedido durante review da PR nfe#43: as labels internas do PagamentoDetalhe estavam em ALL UPPERCASE ("FORMA PAGAMENTO", "VALOR") enquanto as do Duplicata.cs estão em Title Case ("Número", "Vencimento:", "Valor:"). Alinhar para padrão único.

Mudança:

  • "FORMA PAGAMENTO" → "Forma de Pagamento" (sem ':', como "Número")
  • "VALOR" → "Valor:" (com ':', como "Vencimento:" e "Valor:" em Duplicata)

Refs nfe#39

  • chore(openspec): archive add-danfe-payment-block + add-cancelled-watermark

Promove 2 specs para openspec/specs/ ao final da sprint Revenda Mais:

Purpose preenchido para os 2 specs (substituindo o placeholder TBD gerado pelo openspec archive), citando base normativa MOC 7.0 Anexo II + NT 2016.002 e reconhecendo que os formatos visuais são convenção universal de mercado (não mandato literal do MOC).

Changes movidas para openspec/changes/archive/:

  • 2026-05-28-add-danfe-payment-block
  • 2026-05-28-add-cancelled-watermark

(Já estavam arquivadas: 2026-05-28-fix-danfe-csosn-rendering, do tema 1 da sprint.)

Refs nfe#37 (umbrella), nfe#38, nfe#39, nfe#40

  • ProdutoViewModel.OCst: restaura setter público como [Obsolete] com backing field _ocstLegacy, usado apenas quando Origem/Cst/Csosn estão todos vazios. Preserva ABI/source-compat para consumers que ainda atribuem OCst diretamente (uso anterior a Origem/Cst/Csosn).
  • DanfeCanceladaTests: testes IsCancelledTrue, cStat101 e NaoCancelada agora extraem texto do PDF via TextExtractor do PDFClown (sem dependencia externa) e asseguram presenca/ausencia real da marca "DOCUMENTO CANCELADO"
    • antes eram smoke tests que passariam mesmo se a marca nunca fosse desenhada.
  • fix(danfe): celulas vazias com borda em Fatura/Duplicata e Forma de Pagamento

Quando o numero de duplicatas/formas de pagamento nao fecha a linha de colunas (1 duplicata num bloco que cabe 3, p.ex.), as posicoes restantes ficavam como espaco em branco sem borda, truncando visualmente o quadro antes da margem direita. Substituido o ElementoVazio (Draw vazio, herdava DrawableBase) por CelulaVazia (herda ElementoBase, recebe Estilo e desenha apenas a borda via base.Draw -> StrokeRectangle), preenchendo o restante da linha com celulas com borda em retrato e paisagem.

Aplicado em BlocoDuplicataFatura e BlocoFormaPagamento; ElementoVazio mantido intacto (sem outros usuarios).


…arca Cancelada) (#43)

* chore(openspec): bootstrap + change fix-danfe-csosn-rendering (#38)

Initialize OpenSpec (--tools=claude) and propose change covering
the ICMS column rendering bug reported in #38:

- Concatenation without separator (e.g. "120" instead of "1/20")
  in DanfeViewModelCreator.cs:497
- Static header label "O/CSOSN" derived from Emitente.CRT only,
  fragile when CRT is missing in the XML

Artifacts in openspec/changes/fix-danfe-csosn-rendering/:
- proposal.md: motivation, capabilities, impact
- design.md: 4 decisions anchored on real code (item-derived
  header, separate Origem/Cst/Csosn properties, OCst as
  computed, no feature flag)
- specs/danfe-icms-column/spec.md: 3 requirements + scenarios
- tasks.md: 9 task groups (investigation, model, producer,
  renderer, tests, visual validation, docs, PR, archive)

No code changes yet — implementation lands via /opsx:apply on a
follow-up commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(danfe): coluna ICMS — separador / e cabeçalho derivado dos itens (#38)

Corrige dois sintomas reportados pelo cliente Revenda Mais na coluna
"O/CSOSN" do bloco produtos da DANFE:

1. Origem e código (CST/CSOSN) concatenados sem separador ("120" em vez
   de "1/20"). Causa: `produto.OCst = icms.orig + icms.CST + icms.CSOSN`
   em `DanfeViewModelCreator.cs:497`.

2. Cabeçalho "O/CSOSN" mesmo para emitentes do Regime Normal quando
   `<emit><CRT>` está ausente/inconsistente no XML. Causa: lógica
   dependia só de `Emitente.CRT == "3"`.

Mudanças

- `ProdutoViewModel` ganha `Origem`/`Cst`/`Csosn` separados. `OCst`
  vira propriedade calculada que aplica "<origem>/<código>" e omite
  componentes vazios (XMLs sem <orig> exibem só o código).
- `ProdutoViewModel.CalcularCabecalhoColunaIcms(produtos)` — helper
  estático que decide o cabeçalho inspecionando os itens da nota
  (se algum tem CST → "O/CST"; senão CSOSN → "O/CSOSN"; fallback
  "O/CST"). Mais robusto que confiar em `Emitente.CRT`.
- `DanfeViewModelCreator` popula `Origem`/`Cst`/`Csosn` normalizando
  strings vazias do XmlSerializer para null.
- `TabelaProdutosServicos` consome o helper.

Testes

`DanfeSharp.Test/ProdutoIcmsColumnTests.cs`: 15 testes (8 de célula,
7 de cabeçalho), incluindo regressão do caso Revenda Mais (CRT
ausente + CST nos itens → "O/CST"). 15/15 verdes via `dotnet vstest`.

Hotfix tangencial (pre-existente em `origin/main`, bloqueava build do
projeto de teste)

- `FabricaFake.cs:51`: cast `(decimal?)v` para `vFCPUFDest`.
- `DanfeTest.cs:63`: `DanfeCCC` renomeado para `DanfeCartaCorrecao`
  (rename antigo não propagado).

Pendente (manual)

- §1 pre-flight: confirmar com Carolina que separador é `/` e
  grep cross-repo por `ProdutoViewModel.OCst`.
- §6 validação visual: gerar DANFE da invoice de referência do
  Revenda Mais e comparar PDF antes/depois.

OpenSpec change: openspec/changes/fix-danfe-csosn-rendering/
Spec validated: `openspec validate` OK.

Fixes #38

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(openspec): calibrar base normativa do change #38 (MOC 7.0 §3.1.7)

Após leitura literal do MOC 7.0 Anexo II (PDF oficial do CONFAZ),
constatei que:

- §3.1.7 (página 11) lista a coluna "CST" como obrigatória no
  quadro "Dados dos Produtos/Serviços", mas NÃO prescreve
  literalmente o formato combinado "O/CST" nem o separador "/".
- O campo <orig> faz parte do schema do XML (Anexo I) mas o Anexo II
  não menciona apresentação visual conjunta com CST/CSOSN.
- O formato "O/CST" com separador "/" é convenção universal de
  mercado (TOTVS, SAP, SmartGo, eMissor, etc.) — não mandato literal.

Calibragem aplicada em:
- proposal.md (Why): substituída "conformidade visual com MOC v7.0+"
  por "convenção universal de mercado", com referência §3.1.7
  para a parte que é mandato (coluna CST).
- design.md (Decision 3): nova seção "Base normativa" com URLs
  do CONFAZ para Anexo I + Anexo II. Open Question sobre separador
  marcada como resolvida.
- specs/danfe-icms-column/spec.md: nota normativa no topo do bloco
  ADDED Requirements explicando o que é mandato literal vs convenção.
- tasks.md §1.2: contexto expandido — separador "/" é convenção
  universal, não MOC literal.

Sem mudança de código nem de comportamento; só de citação/justificativa.
Implementation alinhada com convenção do mercado, que é o que
receptores fiscais esperam ver (raiz do problema com cliente
Revenda Mais).

Fixes #38 (continued)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(openspec): archive fix-danfe-csosn-rendering → specs/danfe-icms-column

- Move openspec/changes/fix-danfe-csosn-rendering/ →
  openspec/changes/archive/2026-05-28-fix-danfe-csosn-rendering/
- Promote ADDED requirements to openspec/specs/danfe-icms-column/spec.md
- Preencher Purpose com base normativa (MOC 7.0 §3.1.7) — substituindo
  o boilerplate "TBD" gerado pelo CLI

A spec danfe-icms-column passa a ser fonte de verdade para qualquer
mudança futura na coluna ICMS da DANFE. Mudanças subsequentes devem
ser propostas como MODIFIED/ADDED via /opsx:new ou /opsx:propose.

Note: 11 tasks ficaram incompletas no archive (manuais/externas —
§1.1 grep cross-repo, §1.2 confirmar com Carolina, §4.3 validar
largura visual, §6 validação visual com XML real do Revenda Mais,
§8 review + merge, §9 archive já feita aqui). O archive forçou via
--yes flag por se tratar de tarefas extra-código.

Fixes #38 (continued)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(openspec): plan add-danfe-payment-block (#39)

Initialize OpenSpec na branch (igual a #38) e propor o change cobrindo
a implementação do bloco "Forma de Pagamento" no DANFE de NF-e modelo 55:

PROBLEMA RAIZ
- DanfeSharp não renderiza bloco visual de pagamento mesmo quando o
  XML traz <pag><detPag> preenchido (NT 2016.002 v1.50 tornou
  obrigatório desde 2018).
- Cliente Revenda Mais reportou via Carolina Fagundes / Teams 2026-05-27;
  invoice 9175b02ac0cf4ed898025c4bad09e2fe.

ESTADO DO CÓDIGO (auditado)
- Schema XML: pag/detPag/FormaPagamento enum já existem (ProcNFe.cs)
  com Description em cada tPag (01..19, 90, 99); FALTA xPag em detPag.
- ViewModel: PagamentoViewModel + DetalheViewModel + CartaoViewModel
  já existem; FALTA Descricao em DetalheViewModel.
- Producer (DanfeViewModelCreator) já popula model.Pagamento; falta
  apenas mapear xPag → Descricao.
- FALTA renderer visual (BlocoFormaPagamento.cs).
- FALTA inserção no fluxo do construtor de Danfe.cs.

BASE NORMATIVA CALIBRADA (lição aprendida do #38)
- MOC 7.0 Anexo II não tem seção dedicada a forma de pagamento para
  DANFE NF-e modelo 55 (verifiquei literalmente o PDF do CONFAZ;
  zero ocorrências de "pagamento"/"detPag"/"tPag" no texto).
- NT 2016.002 obrigou o grupo <pag> no XML; o manual do DANFE NFCe
  (Anexo III) mandou exibir o bloco no cupom da NFCe.
- Para NF-e modelo 55, o bloco visual é CONVENÇÃO UNIVERSAL DE MERCADO
  (TOTVS, SAP, SmartGo, eMissor), não mandato literal. Spec deixa
  isso explícito.

ARTEFATOS
- proposal.md (5.9 KB): motivação + capability danfe-payment-block + impacto
- design.md (14 KB): 6 decisões ancoradas no código + open questions
- specs/danfe-payment-block/spec.md (8.2 KB): 8 requirements + 18 cenários BDD
- tasks.md (8.3 KB): 13 grupos (32 itens) cobrindo schema, viewmodel,
  producer, helper, renderer, fluxo, testes, fixtures, validação
  visual, docs, PR, archive

`openspec validate` OK.

Nenhuma mudança de código ainda — implementação virá via /opsx:apply
no próximo commit.

Fixes #39

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(danfe): adiciona bloco FORMA DE PAGAMENTO (#39)

Adiciona o bloco "Forma de Pagamento" na DANFE de NF-e modelo 55,
lendo o grupo <pag><detPag> do XML. Demanda do cliente Revenda Mais
(invoice 9175b02ac0cf4ed898025c4bad09e2fe), umbrella #37.

MUDANÇAS

Schema (DanfeSharp/Esquemas/ProcNFe.cs)
- detPag ganha campo xPag (YA03 — descrição do pagamento).

ViewModel (DanfeSharp/Modelo/PagamentoViewModel.cs)
- DetalheViewModel ganha propriedade Descricao (mapeada de xPag).

Producer (DanfeSharp/Modelo/DanfeViewModelCreator.cs)
- Producer existente para <pag> em CreateFromProcNFCe ganha população
  de Descricao (xPag → Descricao com normalização "" → null).
- BUG RAIZ ADICIONAL: a população do grupo <pag> estava SÓ em
  CreateFromProcNFCe (NFC-e modelo 65) — faltava em CreateFromProcNFe
  (NF-e modelo 55, escopo desta change). Adicionado o foreach no
  CreateFromProcNFe antes do return — sem essa correção o renderer
  nunca dispararia para NF-e.

Helper (DanfeSharp/Modelo/FormaPagamentoExtensions.cs — novo)
- GetDescricao(this FormaPagamento) lê [DescriptionAttribute] via
  reflexão. Retorna string.Empty para enum value inválido (defensivo).

Renderer (DanfeSharp/Blocos/BlocoFormaPagamento.cs — novo)
- Herda BlocoBase. Cabecalho "Forma de Pagamento". Itera sobre
  Pagamento[].DetalhePagamento via SelectMany e cria uma LinhaCampos
  por detalhe com FORMA PAGAMENTO (Descricao ?? GetDescricao) + VALOR
  (ComCampoNumerico, sem prefixo R$ — convenção do BlocoCalculoImposto).
- Auto-omissão: early return quando detalhes == null/empty.

Sequência (DanfeSharp/Danfe.cs)
- AdicionarBloco<BlocoFormaPagamento>() inserido entre
  BlocoCalculoImposto e BlocoTransportador.
- Condicionado a Pagamento ser não-vazio (defense in depth).

TESTES (DanfeSharp.Test/FormaPagamentoTests.cs — novo)
- 14 testes em FormaPagamentoTests: helper para Dinheiro/CartaoCredito/
  CartaoDebito/Outro/PIX/inválido, schema parsing com/sem <xPag>,
  ViewModel propertes, regra Descricao-prevalece-sobre-enum.
- 2 testes de integração em IntegracaoPagamentoNFe: NF-e v4 popula
  model.Pagamento (regressão do bug raiz §4.2); NF-e v3.10 sem <pag>
  não quebra e mantém Pagamento vazio.
- Aliases FormaPagamentoVm/FormaPagamentoSchema para desambiguar os 2
  enums coexistentes (Modelo/Enums.cs e Esquemas/ProcNFe.cs).
- DanfeSharp.Test.csproj: <Reference Include="System.Xml" /> para
  acessar XmlSerializer nos testes de schema.

dotnet vstest:
- FormaPagamentoTests + IntegracaoPagamentoNFe: 16/16 verde (748 ms)
- DanfeXmlTests: 5/5 verde (zero regressão)
- PDF re-renderizado do v4_ComLocalEntrega.xml exibe o bloco no local
  esperado (texto extraído: "FORMA DE PAGAMENTO ... VALOR ... Outros
  ... 3.977,00").

HOTFIX TANGENCIAL (pré-existente em origin/main)
- FabricaFake.cs:51 cast (decimal?)v para vFCPUFDest
- DanfeTest.cs:63 DanfeCCC → DanfeCartaCorrecao
- Mesmas correções aplicadas na PR #41 (#38); necessárias para o
  projeto de teste compilar enquanto a #41 não merge na main.

OpenSpec change: openspec/changes/add-danfe-payment-block/
Spec validated: `openspec validate` OK.
38 tasks; tasks externas/manuais (§1, §10, §12, §13) seguem pendentes
do lado humano (cliente + reviewer + post-merge archive).

Fixes #39

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(danfe): layout horizontal dos campos da Duplicata (FATURA / DUPLICATA)

Reportado pelo cliente Revenda Mais durante revisão do PR #42: o quadro
"FATURA / DUPLICATA" exibia Número / Vencimento / Valor empilhados
verticalmente dentro de cada célula, ocupando 3× mais altura que o
necessário. Refatora para layout horizontal, alinhando com o padrão
visual do BlocoCalculoImposto e demais blocos com campos labelados.

Mudanças

- DanfeSharp/Elementos/Duplicata.cs: o Draw passa a dividir o
  bounding rect em 3 colunas iguais e desenhar cada par (label
  no topo + valor abaixo) lado a lado em vez de empilhar com CutTop.
  Height reduzida de 3*linha → 2*linha (label + valor).

- DanfeSharp/Blocos/BlocoDuplicataFatura.cs: numeroElementosLinha
  reduzido de 6→3 (retrato) e 7→4 (paisagem) para acomodar a largura
  necessária por célula no novo layout. Sem essa redução, os valores
  ("11/10/2018", "R$ 3.977,00") seriam cortados.

Validação visual

- v4_ComLocalEntrega.pdf re-renderizado. Texto extraído por pdftotext:
    Número                Vencimento:          Valor:
    001                   11/10/2018           R$ 3.977,00
  (em uma única linha, 3 colunas legíveis)

- dotnet vstest DanfeXmlTests: 5/5 aprovados, zero regressão.

Fora do escopo original do PR #42 (que cobre o bloco FORMA DE PAGAMENTO).
Aplicado como hotfix de polish a pedido do criador da PR, conforme
feedback durante revisão visual da DANFE rendererizada.

Base normativa: o MOC 7.0 Anexo II §3.1.6 ("Quadro Fatura/Duplicatas")
não prescreve layout específico para os campos internos da duplicata —
"Poderá conter linhas divisórias internas separando as informações."
Layout escolhido alinha com a convenção dos demais blocos do
DanfeSharp.

Refs #39
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(danfe): fixture v4_RevendaMaisDemo para visualizar ambos os ajustes

XML sintético derivado de v4_ComLocalEntrega.xml mas com cobr/pag
expandidos para exercitar todos os caminhos novos:

FATURA / DUPLICATA (validação do refactor 371ed39)
- 6 duplicatas mensais R$ 662,83 cada (sum 3.977,00 igual ao vLiq da
  fat). Demonstra o novo layout horizontal de campos + paginação em
  2 linhas de 3 duplicatas (numeroElementosLinha=3 em retrato).

FORMA DE PAGAMENTO (validação da feature aaf6e9f)
- 3 <detPag> variados:
  • tPag=17 (PIX, NT 2020.001) — R$ 1.500,00 → renderiza
    "Pagamento Instantâneo (PIX)" via fallback do enum
  • tPag=03 (Cartão de Crédito) + grupo <card> — R$ 1.000,00 →
    renderiza "Cartão de Crédito" via fallback do enum
  • tPag=99 (Outros) + <xPag>VALE TROCO</xPag> — R$ 1.477,00 →
    renderiza "VALE TROCO" (xPag prevalecendo sobre "Outros",
    regra da spec danfe-payment-block)

Soma dos detPag = 3.977,00 = soma das duplicatas = vNF (consistente).

Wiring
- v4_RevendaMaisDemo.xml em DanfeSharp.Test/Xml/NFe/v4.00/
- <Content Include> + CopyToOutputDirectory em DanfeSharp.Test.csproj
- DanfeXmlTests.v4_RevendaMaisDemo() chama TestXml(...) que gera
  Output/DeXml/v4_RevendaMaisDemo.pdf

Validação
- dotnet vstest da suite completa (DanfeXmlTests + FormaPagamentoTests
  + IntegracaoPagamentoNFe) → 22/22 verde.
- Texto extraído do PDF confirma:
    FATURA / DUPLICATA: 6 duplicatas em 2 linhas, layout horizontal.
    FORMA DE PAGAMENTO: 3 linhas com FormaPagamento + Valor distintos.

Refs #39
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(fixture): adiciona <infAdProd> em 2 produtos do demo

Para visualizar como a tag <infAdProd> (informações adicionais do
produto, YA02 do MOC) é renderizada na DANFE:

- Item #1 (SOFTWARE A): <infAdProd>Licença digital · Não-reembolsável
  · S/N: ABC123-VERSAO-3.0</infAdProd>
- Item #2 (SOFTWARE B): <infAdProd>Garantia: 12 meses contra defeitos
  de fabricação · Suporte: 0800-XXX-YYYY</infAdProd>

A informação aparece logo abaixo do nome do produto, dentro da mesma
célula da coluna "DESCRIÇÃO DO PRODUTO / SERVIÇO" (concatenada via
ProdutoViewModel.DescricaoCompleta com separador \r\n). Conformidade
com MOC 7.0 Anexo II §3.1.7 (pg 10): "As informações adicionais de
produto deverão constar impressas no DANFE logo abaixo do item ao qual
se referirem."

Validação: dotnet vstest v4_RevendaMaisDemo → aprovado; texto
extraído por pdftotext confirma render correto com line wrap
automático em strings longas.

Refs #39
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(danfe): rename QUANTI.→QUANT. e diminui coluna para favorecer DESCRIÇÃO

Pedido durante revisão visual da PR #42:
- Renomear o cabeçalho da coluna Quantidade de "QUANTI." para "QUANT."
- Diminuir a largura da coluna (sobrava espaço pra abreviação mais curta)
- Devolver o espaço ganho à coluna DESCRIÇÃO DO PRODUTO / SERVIÇO
  (que é flex, width=0 — absorve automaticamente)

Mudanças em DanfeSharp/Blocos/TabelaProdutosServicos.cs:
- Retrato:   6F   → 4F     (-2F devolvidos à descrição)
- Paisagem:  5.25F → 3.75F (-1.5F devolvidos à descrição)
- Label em ambos os modos: "QUANTI." → "QUANT."

Validação: dotnet vstest DanfeXmlTests → 6/6 verde, zero regressão.
Texto extraído do PDF confirma cabeçalho "QUANT." e descrição mais larga.

Refs #39
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(danfe): diminui coluna NCM/SH em favor da DESCRIÇÃO

Continuação do ajuste visual da PR #42:
- NCM/SH: o conteúdo é sempre 8 dígitos (49111090) — não precisa do
  espaço atual. Diminuir e devolver à coluna DESCRIÇÃO (flex).

Retrato:   5.6F  → 4.5F  (-1.1F devolvidos à descrição)
Paisagem:  5.5F  → 4.5F  (-1.0F devolvido à descrição)

Validação: dotnet vstest DanfeXmlTests → 6/6 verde.

Refs #39
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(danfe): ajusta largura da coluna NCM/SH (4.5F → 5F)

O commit anterior (c72ebab) reduziu NCM/SH para 4.5F, mas a coluna
ficou apertada demais — o último dígito do NCM de 8 dígitos era
truncado (49111090 virava 4911109). Aumenta para 5F (ainda ganha
0.6F vs original em retrato, 0.5F em paisagem) garantindo que NCMs
de 8 dígitos rendereizem completos.

Refs #39
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(danfe): ajuste fino paisagem QUANT. 3.75F → 4.00F

Pedido durante review da PR #42: dar +0.25F à coluna QUANT. em modo
paisagem (que estava apertada visualmente). DESCRIÇÃO (flex, width=0)
absorve o ajuste automaticamente cedendo 0.25F.

Refs #39
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(danfe): adiciona test methods para paisagem (demo + light fixture)

Pedido durante review da PR #42: gerar exemplos completos em retrato
e paisagem. Adiciona 2 test methods em DanfeXmlTests:

- v4_RevendaMaisDemo_Paisagem: força Orientacao=Paisagem na fixture
  rica. Atualmente FALHA com "Height is invalid" — overflow vertical
  porque a fixture tem 6 duplicatas + 3 detPag que não cabem em A4
  paisagem (mais baixa que retrato). Documentado como bug pre-existente
  do layout engine (mesmo problema dos testes Paisagem*/FabricaFake
  que já falhavam em main).

- v4_ComLocalEntrega_Paisagem: força paisagem na fixture leve (1
  duplicata + 1 detPag) — funciona. Demonstra que a renderização
  paisagem em si está OK; falha do demo é por excesso de conteúdo.

DanfeViewModelCreator.cs:411 ainda hardcoda Orientacao=Retrato
(comentário sobre netcore2.0 bug). Esses tests forçam override
após o load.

Refs #39
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(danfe): paginar quando blocos topo overflow vertical (em vez de crash)

Bug: Danfe.Gerar() lançava "InvalidOperationException: Height is
invalid" quando os blocos topo (Emitente, Destinatário, Local
Entrega/Retirada, Fatura, Cálculo, Forma de Pagamento, Transportador,
Dados Adicionais) consumiam toda a altura disponível da página —
situação comum em paisagem com NF-e ricas (caso típico do
cliente Revenda Mais com 6 duplicatas + 3 detPag + bloco FORMA).

Root cause: o while loop em Gerar() já suportava paginação via
TabelaProdutosServicos.CompletamenteDesenhada, mas faltava um guard
para casos onde RetanguloCorpo.Height ≤ 0 — ele tentava desenhar
mesmo assim e o DrawableBase.Draw lançava.

Fix: adiciona guard em Gerar() — quando RetanguloCorpo.Height ≤ 0,
pula a página sem desenhar a tabela. A próxima iteração cria uma
nova página onde apenas BlocoIdentificacaoEmitente é desenhado
(único com VisivelSomentePrimeiraPagina=false), liberando espaço
para a tabela.

Proteção contra loop infinito: se 2 páginas consecutivas têm Height
≤ 0, lança erro descritivo orientando reduzir blocos opcionais ou
usar Retrato.

Validação:
- v4_RevendaMaisDemo_Paisagem (heavy: 6 duplicatas + 3 detPag) — antes
  lançava NPE, agora gera PDF com 2 páginas: pág 1 = blocos topo,
  pág 2 = identificação + tabela produtos.
- v4_ComLocalEntrega_Paisagem (light) — continua passando.
- Retrato continua passando para todas as fixtures.
- Falhas pre-existentes em FabricaFake-based Paisagem tests (PaisagemSemIcmsInterestadual etc.)
  não são afetadas — são causadas por bug independente (TextoReservadoFisco():355
  NotImplementedException por TipoEmissao=0 default no fake).

Refs #39
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(fixtures): adiciona 3 demos cobrindo cenários extremos + intermediário

Para visualizar a renderização da DANFE em variações de complexidade
(pedido durante review da PR #42):

- v4_DanfeMinimo.xml — 1 item sem infAdProd, sem <cobr>, sem <pag>.
  Caso de NF-e mínima (ex.: doação, brinde) sem fatura/pagamento.
  PDF gerado: 5.8 KB, 1 página.

- v4_DanfeIntermediario.xml — 5 itens (3 com infAdProd: notebook,
  teclado, monitor; 2 sem: mouse, cabo), 2 duplicatas R$ 500 cada,
  2 detPag (Cartão Crédito R$ 600 + PIX R$ 400). Total R$ 1000.
  PDF: 6.7 KB.

- v4_DanfeCompleto.xml — 20 itens (todos com infAdProd
  "Lote LT-N-2026 · Validade 12 meses · Origem: Brasil"), 6
  duplicatas mensais R$ 333.34 cada, 3 detPag (PIX 800 + Cartão
  Crédito 700 + Outros R$ 500 com xPag="PROMOCIONAL BLACK FRIDAY").
  Total R$ 2000. PDF: 11 KB, múltiplas páginas (paginação automática
  graças ao fix em Danfe.Gerar()).

Geração via script reproduzível (generate-fixtures.sh) com helpers
para gen_item, gen_dup, gen_detpag, gen_header, gen_footer.
Reexecutável: bash DanfeSharp.Test/Xml/NFe/v4.00/generate-fixtures.sh

Wiring
- Content + CopyToOutputDirectory em DanfeSharp.Test.csproj
- 3 test methods em DanfeXmlTests (v4_DanfeMinimo, v4_DanfeIntermediario,
  v4_DanfeCompleto) que chamam TestXml(...) para gerar PDFs em
  bin/Debug/Output/DeXml/

Validação
- dotnet vstest dos 3 → todos aprovados.
- Sanity checks via pdftotext confirmam contagens esperadas:
  EX1: 0 FATURA, 0 FORMA, 1 PROD-, 0 infAdProd
  EX2: 1 FATURA, 1 FORMA, 5 PROD-
  EX3: 1 FATURA, 1 FORMA, 20 PROD-, 20 "Lote" (infAdProd)

Refs #39
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(fixtures): adiciona 3 demos em paisagem (Mínimo + Intermediário + Completo)

Mesmas fixtures do commit anterior, agora renderizadas em paisagem
para visualização completa do layout em ambos os modos.

Adicionado helper RenderPaisagem(xmlPath, outName) que carrega o XML
e força Orientacao = Paisagem antes de gerar (necessário porque
DanfeViewModelCreator hardcoda Retrato — comentário sobre netcore2.0
em DanfeViewModelCreator.cs:411).

3 novos test methods:
- v4_DanfeMinimo_Paisagem
- v4_DanfeIntermediario_Paisagem
- v4_DanfeCompleto_Paisagem (20 itens em paisagem — paginação automática
  graças ao fix do bug "Height is invalid" aplicado anteriormente)

Todos os 3 passam. PDFs gerados em bin/Debug/Output/DeXml/ com
sufixo _Paisagem.

Refs #39
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(danfe): marca d'água "DOCUMENTO CANCELADO" para NF-e cancelada (#40)

Tema 3 do umbrella Revenda Mais (issue #37). Renderiza marca d'água
"DOCUMENTO CANCELADO" centralizada em todas as páginas da DANFE
quando a NF-e está cancelada.

ARQUITETURA — primeiro detectar via flag, depois cStat=101 como fallback

O cancelamento em NF-e modelo 55 é registrado via EVENTO SEPARADO
(NFeProcEvento com tpEvento=110111), NÃO via mudança no <cStat> da
NF-e original — o XML protocolo da nota mantém cStat=100 (Autorizado)
mesmo após cancelamento autorizado. Quem sabe que a nota foi
cancelada é o sistema do emissor consultando o histórico de eventos.

A PR #8 do @mateuszanini (2023, branch feature/aviso-cancelamento)
tinha a renderização visualmente correta mas detectava via cStat=101
do XML protocolo — que na prática raramente é 101 para uma nota
cancelada. Esta change reaproveita o método dele (DesenharAvisoCancelamento)
mas usa flag explícita IsCancelled como mecanismo primário + cStat=101
como fallback OR (zero custo).

CÓDIGO

- DanfeSharp/Modelo/DanfeViewModel.cs:223 — nova propriedade
  IsCancelled (bool, default false) com XML doc explicando o motivo
  arquitetural (cancelamento via evento, não via cStat do XML).

- DanfeSharp/DanfePagina.cs:106 — novo método DesenharAvisoCancelamento
  espelhando DesenharAvisoHomologacao: TextStack centralizado no
  RetanguloCorpo, fonte 48pt regular, cor RGB(0.35, 0.35, 0.35),
  texto "DOCUMENTO CANCELADO". Padrão visual idêntico ao "SEM VALOR
  FISCAL" já existente — vamos da PR #8.

- DanfeSharp/Danfe.cs:CriarPagina — após DesenharAvisoHomologacao,
  adiciona "if (ViewModel.IsCancelled || ViewModel.CodigoStatusReposta == 101)
  p.DesenharAvisoCancelamento();". Sobreposição com aviso de
  homologação aceita (NF-e cancelada em homologação → ambos os
  avisos aparecem, comportamento correto).

TESTES (DanfeCanceladaTests.cs, 11 tests)

- Unit-level: IsCancelled default false, settable.
- Integration: render com IsCancelled=true, com cStat=101, com ambos,
  e regressão sem nenhum trigger.
- Demos: render dos 3 fixtures (Mínimo/Intermediário/Completo) +
  versões paisagem do Intermediário e Completo, todos com IsCancelled=true.
- pdftotext -raw confirma "DOCUMENTO CANCELADO" presente em
  todos os 5 PDFs cancelados (1x em página única, 2x em multi-página
  do Completo), e 0 ocorrências em DANFEs não-canceladas.
- DanfeXmlTests existentes: 14/14 verde (zero regressão).

BASE NORMATIVA

MOC 7.0 Anexo II §3.10.1 (Marca d'Água) autoriza literalmente:
"O formulário poderá conter marca d'água desde que não prejudique a
legibilidade dos dados impressos." NÃO obriga marca para nota
cancelada, NÃO especifica texto/posição/opacidade. Texto e estilo
são convenção universal de mercado (TOTVS/SAP/SmartGo/eMissor)
alinhada ao "SEM VALOR FISCAL" do ambiente de homologação.

OpenSpec change: openspec/changes/add-cancelled-watermark/
- proposal.md, design.md (5 decisões), specs/danfe-cancelled-watermark/spec.md
  (5 requirements + 11 cenários), tasks.md (10 grupos).
- openspec validate ✓

Fixes #40
Refs PR #8 (@mateuszanini, 2023 — render mantida, gatilho substituído)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(danfe): bloco FORMA DE PAGAMENTO usa padrão visual de FATURA/DUPLICATA

Ajustes cosméticos pedidos durante review da PR #43:

1) Mover bloco FORMA DE PAGAMENTO para logo abaixo de FATURA/DUPLICATA
   (em Danfe.cs:CriarPagina). Ordem antiga:
     FATURA → CÁLCULO IMPOSTO → FORMA PAGAMENTO → TRANSPORTADOR
   Nova:
     FATURA → FORMA PAGAMENTO → CÁLCULO IMPOSTO → TRANSPORTADOR
   Agrupa os blocos de informação comercial (fatura + pagamento)
   adjacentes, separados dos fiscais (imposto + transportador).

2) Refatorar BlocoFormaPagamento para mesmo padrão visual do
   BlocoDuplicataFatura — cards lado a lado em FlexibleLine, cada
   um com 2 sub-colunas (FORMA PAGAMENTO + VALOR), label em cima
   e valor abaixo. Antes era 1 linha por detalhe via
   AdicionarLinhaCampos (estilo do BlocoCalculoImposto).

   Novo elemento DanfeSharp/Elementos/PagamentoDetalhe.cs espelhando
   Duplicata.cs:
   - 2 colunas iguais (FORMA PAGAMENTO + VALOR) com label em FonteA
     e valor em FonteB (mesmas fontes da Duplicata).
   - Descricao prevalece sobre [Description] do enum (regra da spec
     danfe-payment-block já existente).
   - Valor formatado com R$ via Formatador.FormatarMoeda (cast
     decimal→double).

   BlocoFormaPagamento usa FlexibleLine com N cards por linha:
   - Retrato: 2 (descrições podem ser longas, ex. "Pagamento
     Instantâneo (PIX)" = 28 chars; 4 cards/linha colidia)
   - Paisagem: 3

Validação visual em demo Completo (retrato, 3 detPag):
  Linha 1: PIX R$ 800,00 | Cartão de Crédito R$ 700,00
  Linha 2: VALE TROCO R$ 500,00

Em paisagem, os 3 detPag cabem na mesma linha.

Testes: 56/56 verde (zero regressão em ProdutoIcmsColumnTests,
FormaPagamentoTests, IntegracaoPagamentoNFe, DanfeXmlTests,
DanfeCanceladaTests). PDFs demo atualizados em
../danfe-payment-validation/{exemplos,exemplos-cancelada}/.

Refs #39
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(danfe): Title Case nas labels de PagamentoDetalhe (espelha Duplicata)

Pedido durante review da PR #43: as labels internas do PagamentoDetalhe
estavam em ALL UPPERCASE ("FORMA PAGAMENTO", "VALOR") enquanto as do
Duplicata.cs estão em Title Case ("Número", "Vencimento:", "Valor:").
Alinhar para padrão único.

Mudança:
- "FORMA PAGAMENTO" → "Forma de Pagamento" (sem ':', como "Número")
- "VALOR" → "Valor:" (com ':', como "Vencimento:" e "Valor:" em Duplicata)

Refs #39
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(openspec): archive add-danfe-payment-block + add-cancelled-watermark

Promove 2 specs para openspec/specs/ ao final da sprint Revenda Mais:

- danfe-payment-block (#39) — bloco "Forma de Pagamento" da DANFE NF-e
  modelo 55 (schema <pag>/<detPag>/<xPag>, mapping tPag→descrição via
  [Description], prevalência de xPag, omissão graciosa, layout em cards
  estilo Duplicata).

- danfe-cancelled-watermark (#40) — marca d'água "DOCUMENTO CANCELADO"
  para NF-e cancelada (flag IsCancelled primária + cStat=101 fallback,
  fonte 48pt cinza centralizada em todas as páginas).

Purpose preenchido para os 2 specs (substituindo o placeholder TBD
gerado pelo openspec archive), citando base normativa MOC 7.0
Anexo II + NT 2016.002 e reconhecendo que os formatos visuais são
convenção universal de mercado (não mandato literal do MOC).

Changes movidas para openspec/changes/archive/:
- 2026-05-28-add-danfe-payment-block
- 2026-05-28-add-cancelled-watermark

(Já estavam arquivadas: 2026-05-28-fix-danfe-csosn-rendering, do
tema 1 da sprint.)

Refs #37 (umbrella), #38, #39, #40
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address PR #43 Copilot review (OCst setter + watermark assertions)

- ProdutoViewModel.OCst: restaura setter público como [Obsolete] com
  backing field _ocstLegacy, usado apenas quando Origem/Cst/Csosn estão
  todos vazios. Preserva ABI/source-compat para consumers que ainda
  atribuem OCst diretamente (uso anterior a Origem/Cst/Csosn).
- DanfeCanceladaTests: testes IsCancelledTrue, cStat101 e NaoCancelada
  agora extraem texto do PDF via TextExtractor do PDFClown (sem dependencia
  externa) e asseguram presenca/ausencia real da marca "DOCUMENTO CANCELADO"
  - antes eram smoke tests que passariam mesmo se a marca nunca fosse desenhada.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(danfe): celulas vazias com borda em Fatura/Duplicata e Forma de Pagamento

Quando o numero de duplicatas/formas de pagamento nao fecha a linha de
colunas (1 duplicata num bloco que cabe 3, p.ex.), as posicoes restantes
ficavam como espaco em branco sem borda, truncando visualmente o quadro
antes da margem direita. Substituido o ElementoVazio (Draw vazio, herdava
DrawableBase) por CelulaVazia (herda ElementoBase, recebe Estilo e
desenha apenas a borda via base.Draw -> StrokeRectangle), preenchendo o
restante da linha com celulas com borda em retrato e paisagem.

Aplicado em BlocoDuplicataFatura e BlocoFormaPagamento; ElementoVazio
mantido intacto (sem outros usuarios).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@adrbarros adrbarros merged commit 2205d75 into adrbarros:main May 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants