AgentSkillsCN

validacao-nf-po

执行并调试 NF 与 PO 核对流程(物料收货阶段二)。 适用场景: - 排查 NF 与 PO 核对中的错误:“DFE 校验失败”、“未找到 DFE” - 调整匹配逻辑:“调整容差范围”、“变更价格规则” - 修复候选 PO 的预览问题:“弹窗无法打开”、“PO 弹窗出现报错” - 理解“从—到”转换流程:“如何进行 UM 转换”、“转换因子” - 实施新的差异处理规则:“新增锁定类型” - 了解本地表结构:“MatchNfPoItem 中有哪些字段?”、“校验环节涉及哪些字段” 不适用场景: - 在 Odoo 中追踪单据——请改用“rastreando-odoo” - 发现未知 Odoo 模型字段——请改用“descobrindo-odoo-estrutura” - 创建付款或对账单——请改用“executando-odoo-financeiro” - 创建 CTe 或处理费用——请改用“integracao-odoo” - 调试实物收货(阶段四)——请改用“recebimento-fisico” - 对 PO 进行拆分或合并——请改用“conciliando-odoo-po”

SKILL.md
--- frontmatter
name: validacao-nf-po
description: |
  Executa e depura o processo de Validacao NF x PO (Fase 2 do Recebimento de Materiais).

  USAR QUANDO:
  - Depurar erro na validacao NF x PO: "erro ao validar DFE", "DFE nao encontrado"
  - Modificar logica de match: "alterar tolerancia", "mudar regra de preco"
  - Corrigir preview de POs candidatos: "modal nao abre", "erro no modal POs"
  - Entender fluxo de conversao De-Para: "como converte UM", "fator conversao"
  - Implementar nova regra de divergencia: "novo tipo de bloqueio"
  - Entender tabelas locais: "o que tem em MatchNfPoItem", "campos da validacao"

  NAO USAR QUANDO:
  - Rastrear documentos no Odoo -> usar rastreando-odoo
  - Descobrir campos de modelo Odoo desconhecido -> usar descobrindo-odoo-estrutura
  - Criar pagamentos ou reconciliar extratos -> usar executando-odoo-financeiro
  - Criar CTe ou despesas -> usar integracao-odoo
  - Depurar recebimento fisico (Fase 4) -> usar recebimento-fisico
  - Conciliar POs (split/consolidacao) -> usar conciliando-odoo-po

Validacao NF x PO - Processo Completo

Visao Geral

O processo de Validacao NF x PO compara itens de uma NF-e (DFE no Odoo) contra Pedidos de Compra (POs) do mesmo fornecedor, verificando preco, quantidade e data.

Arquivo Principal

code
app/recebimento/services/validacao_nf_po_service.py

Modelos/Tabelas Envolvidos

Tabela LocalModeloProposito
validacao_nf_po_dfeValidacaoNfPoDfeCabecalho da validacao (status, contadores)
match_nf_po_itemMatchNfPoItemResultado match item-a-item (NF vs PO)
match_nf_po_alocacaoMatchAlocacaoAlocacao split (1 item NF → N linhas PO)
divergencia_nf_poDivergenciaNfPoDivergencias para resolucao manual
produto_fornecedor_deparaProdutoFornecedorDeparaConversao codigo/UM fornecedor → interno
pedido_comprasPedidoComprasPOs locais (sincronizados do Odoo)
Modelo OdooUso
l10n_br_ciel_it_account.dfeNF-e eletronica (cabecalho)
l10n_br_ciel_it_account.dfe.lineLinhas/itens da NF-e

Fluxo Principal

code
┌─────────────────────────────────────────────────────────────┐
│                    validar_dfe(odoo_dfe_id)                  │
│                                                             │
│  1. Buscar DFE no Odoo (_buscar_dfe)                        │
│  2. Verificar se DFE ja tem PO vinculado                    │
│     └─ SIM → status='finalizado_odoo', retorna             │
│  3. Buscar linhas DFE no Odoo (_buscar_dfe_lines)           │
│  4. Converter itens com De-Para BATCH                       │
│     ├─ itens_convertidos (tem cod_produto_interno)          │
│     └─ itens_sem_depara (bloqueio imediato)                 │
│  5. Buscar POs LOCAL (_buscar_pos_fornecedor_local)         │
│  6. Para cada produto: match com split                      │
│     ├─ Filtrar POs por preco (0%) e data (±dias uteis)      │
│     ├─ Alocar qtd_nf nos POs (por data ASC)                │
│     └─ Registrar MatchNfPoItem + MatchAlocacao              │
│  7. Se 100% match → status='aprovado'                       │
│     Se <100% → status='bloqueado' + divergencias            │
└─────────────────────────────────────────────────────────────┘

Constantes de Tolerancia

python
TOLERANCIA_QTD_PERCENTUAL = Decimal('10.0')       # Qtd NF pode ser ate 10% MAIOR que PO
TOLERANCIA_PRECO_PERCENTUAL = Decimal('0.0')      # Preco deve ser EXATO (0% tolerancia)
TOLERANCIA_DATA_ANTECIPADO_DIAS = 5               # NF pode chegar 5 dias CORRIDOS ANTES
TOLERANCIA_DATA_ATRASADO_DIAS = 15                # NF pode chegar 15 dias CORRIDOS DEPOIS

REGRA CRITICA: Preview vs Validacao

OperacaoFonte de DadosChama Odoo?
validar_dfe()Odoo + LocalSIM (leitura DFE/linhas)
buscar_preview_pos_candidatos()100% LOCALNUNCA

O preview (modal POs Candidatos) usa EXCLUSIVAMENTE dados locais:

  • ValidacaoNfPoDfe para cabecalho
  • MatchNfPoItem para itens (ja convertidos)
  • PedidoCompras para POs candidatos

Tipos de Divergencia

TipoCausaResolucao
sem_deparaCodigo fornecedor sem cadastroCriar De-Para
sem_poNenhum PO com saldo para o produtoAjustar PO ou aprovar
preco_divergePreco NF != Preco PO (0% tolerancia)Aprovar ou corrigir PO
data_divergeData NF fora da janela ±dias uteisAprovar
qtd_divergeQtd NF > saldo PO + 10%Ajustar PO
saldo_insuficienteMesmo com split, saldo total < qtd NFCriar PO complementar

Job de Validacao (Scheduler)

Arquivo: app/recebimento/jobs/validacao_recebimento_job.py Scheduler: Cada 30 minutos (automatico) Execucao Manual: Botao "Executar Validacao" na tela NF×PO (com modal De/Ate)

Fluxo do Job (4 Etapas):

code
[1/4] Sync De-Para (Odoo -> Sistema)
      |-- Importa product.supplierinfo com product_code
      +-- Limite: 200 registros/execucao

[2/4] Sync POs Vinculados (SEM limite de data)
      |-- Busca TODAS as ValidacaoNfPoDfe sem PO (sem janela temporal)
      |-- Verifica 3 caminhos no Odoo:
      |   |-- Caminho 1: DFE.purchase_id
      |   |-- Caminho 2: DFE.purchase_fiscal_id
      |   +-- Caminho 3: PO.dfe_id -> DFE (inverso, BATCH)
      +-- Atualiza ValidacaoNfPoDfe com PO encontrado

[3/4] Buscar DFEs Pendentes (COM janela temporal)
      |-- Filtro Odoo: tipo=compra, status=04, write_date >= janela
      |-- Exclui: devolucoes, CTes, CNPJs do grupo (Nacom/Goya)
      |-- Limite: 100 DFEs por execucao
      +-- Filtra: ja processados em Fase 1 E Fase 2

[4/4] Processar DFEs (Fase 1 + Fase 2)
      |-- Fase 1: Validacao Fiscal (ValidacaoFiscalService)
      +-- Fase 2: Validacao NF x PO (ValidacaoNfPoService)

Diferenca Critica entre Etapas 2 e 3:

Aspecto_sync_pos_vinculados_buscar_dfes_pendentes
Janela temporalNENHUMA (todos)minutos_janela (default 48h)
O que buscaValidacaoNfPoDfe sem PODFEs no Odoo (compra, status=04)
Afetado pelo modal De/AteNAOSIM
PropositoVincular POs que apareceram depoisProcessar novos DFEs

Vinculacao DFE <-> PO (3 Caminhos)

Arquivo: validacao_recebimento_job.py:206-345 (_sync_pos_vinculados())

O Odoo possui 3 formas de vincular um DFE (NF-e) a um PO:

#CampoModeloDirecaoEstatistica
1purchase_idDFE -> POmany2one direto14.6% (EXCEPCIONAL)
2purchase_fiscal_idDFE -> POmany2one escrituracao75% dos status=06
3PO.dfe_idPO -> DFEmany2one inverso85.4% dos status=04 (PRINCIPAL)

Prioridade de Consulta:

python
# 2 queries no Odoo (batch):
# Query 1: search_read('l10n_br_ciel_it_account.dfe', [['id','in',dfe_ids]], ['purchase_id','purchase_fiscal_id'])
# Query 2: search_read('purchase.order', [['dfe_id','in',dfe_ids]], ['id','name','dfe_id','invoice_status'])

for validacao in validacoes_sem_po:
    # Caminho 1: DFE.purchase_id (14.6%) → status='finalizado_odoo'
    if dfe_data.get('purchase_id'):
        validacao.odoo_po_vinculado_id = purchase_id_data[0]
        validacao.status = 'finalizado_odoo'  # Vinculo direto = fatura gerada

    # Caminho 2: DFE.purchase_fiscal_id (status=06) → status='finalizado_odoo'
    elif dfe_data.get('purchase_fiscal_id'):
        validacao.odoo_po_fiscal_id = purchase_fiscal_data[0]
        validacao.status = 'finalizado_odoo'  # Vinculo fiscal = fatura gerada

    # Caminho 3: PO.dfe_id (85.4% dos status=04 - PRINCIPAL)
    elif pos_por_dfe.get(validacao.odoo_dfe_id):
        validacao.odoo_po_vinculado_id = po_inverso['id']
        # Se PO ja faturado, marcar como finalizado
        if po_inverso.get('invoice_status') == 'invoiced':
            validacao.status = 'finalizado_odoo'

Verificacao de invoice_status (NOVO - Jan/2026)

Quando o vinculo e via PO.dfe_id (Caminho 3), verificamos PO.invoice_status:

invoice_statusSignificadoAcao
noNenhuma faturaManter status atual
to invoiceAguardando faturamentoManter status atual
invoicedJA FATURADOstatus='finalizado_odoo'

Por que isso importa?

  • NFs com PO ja faturado (invoiced) nao devem permitir consolidacao
  • Antes da correcao, essas NFs ficavam com status='aprovado' permitindo acoes indevidas
  • Exemplo real: NF 150602 / PO C2512302 vinculado ha 20h, mas status era aprovado

Atualizacao de PedidoCompras.dfe_id (NOVO - Jan/2026)

Quando um vinculo PO <-> DFE e detectado, o job agora tambem atualiza a tabela pedido_compras:

python
# Metodo: _atualizar_pedido_compras_dfe()
# Campos atualizados:
# - dfe_id (string do ID Odoo)
# - nf_numero (se disponivel)
# - nf_chave_acesso (se disponivel)

Isso mantem compatibilidade entre ValidacaoNfPoDfe e PedidoCompras.

Por que o Caminho 3 e o principal?

No workflow Odoo do recebimento de compras:

  1. DFE chega (status=04 = "PO vinculado")
  2. Operador vincula PO ao DFE — isso preenche PO.dfe_id, NAO DFE.purchase_id
  3. DFE.purchase_id so e preenchido em casos excepcionais (importacao direta)
  4. DFE.purchase_fiscal_id e preenchido na escrituracao (status=06)

Resultado: Para DFEs em status=04, o unico caminho confiavel e PO.dfe_id (85.4%).

Status DFE no Odoo (l10n_br_status)

CodigoNomeSignificadoRelevancia para Validacao
01RascunhoDFE recem-importadoNao processar
02SincronizadoSincronizado com SEFAZNao processar
03CienciaCiencia da operacao confirmadaNao processar
04PO VinculadoPO foi vinculado ao DFEALVO DA VALIDACAO
05RateioEm processo de rateioNao processar
06ConcluidoProcesso finalizadoJa processado (purchase_fiscal_id preenchido)
07RejeitadoDFE rejeitadoIgnorar

Filtro do Job: ['l10n_br_status', '=', '04'] — Apenas DFEs com PO vinculado.

Execucao Manual (Modal De/Ate)

Tela: Validacoes NF x PO (/api/recebimento/validacoes-nf-po) Botao: "Executar Validacao" (abre modal com selecao de periodo) Rota: POST /api/recebimento/executar-validacao

Modal:

  • Campo "De" (date) — padrao: 7 dias atras
  • Campo "Ate" (date) — padrao: hoje
  • Periodo maximo: 90 dias
  • Limite: 100 DFEs por execucao

Logica de Conversao (rota, validacao_nf_po_routes.py:1397):

python
# Converte datas absolutas para minutos_janela
dt_de = datetime.strptime(data_de, '%Y-%m-%d')
agora = datetime.now(timezone.utc)
minutos_janela = int((agora - dt_de).total_seconds() / 60)
# Usa minutos_janela no job (afeta APENAS etapa 3)

O que o botao NAO afeta:

  • _sync_pos_vinculados() (etapa 2) — sempre busca TODOS sem PO, sem janela
  • _sync_depara_odoo() (etapa 1) — sempre importa De-Para

O que o botao AFETA:

  • _buscar_dfes_pendentes() (etapa 3) — usa minutos_janela para filtrar write_date

Sincronizacao PedidoCompras.dfe_id

O scheduler de PedidoCompras (pedido_compras_service.py) tambem sincroniza campos de DFE/NF:

Campos Sincronizados pelo Scheduler:

Campo PedidoComprasFonte OdooQuando
dfe_idPO.dfe_id[0]Se PO tem DFE vinculado
nf_numeroDFE.nfe_infnfe_ide_nnfBatch query em l10n_br_ciel_it_account.dfe
nf_serieDFE.nfe_infnfe_ide_serieBatch query
nf_chave_acessoDFE.nfe_chNFeBatch query
nf_data_emissaoDFE.nfe_infnfe_ide_dhemiBatch query
nf_valor_totalDFE.nfe_infnfe_total_icmstot_vnfBatch query

Fluxo de Sincronizacao:

code
sincronizar_pedidos_incremental()
    |
    +-- _buscar_pedidos_odoo()  // inclui 'dfe_id', 'invoice_status'
    |
    +-- _buscar_dfes_batch()    // 1 query para TODOS os DFEs
    |
    +-- _processar_pedidos_otimizado()
        |
        +-- _criar_pedido()     // preenche dfe_id, nf_* em novos
        +-- _atualizar_pedido() // atualiza dfe_id, nf_* em existentes

IMPORTANTE: O scheduler busca dados de l10n_br_ciel_it_account.dfe (modelo DFE do modulo CIEL IT).

Referencias

Skills Relacionadas

  • rastreando-odoo - Para CONSULTAR documentos (nao executar)
  • descobrindo-odoo-estrutura - Para explorar campos Odoo desconhecidos
  • executando-odoo-financeiro - Para operacoes financeiras (pagamentos, reconciliacao)
  • conciliando-odoo-po - Para split/consolidacao de POs (Fase 3)
  • recebimento-fisico - Para recebimento fisico (Fase 4)