AgentSkillsCN

recebimento-fisico-odoo

查询并操作实物收货模块(第4阶段):在Odoo中通过Redis队列进行批次填写、数量录入与质量检测的异步处理。 适用场景: - 调试实物收货:“验证拣货时出错”、“批次未创建”、“质量检测失败” - 修改Worker逻辑:“调整处理步骤”、“新增环节” - 查询收货状态:“拣货尚未完成”、“状态仍待定” - 扩展功能:“添加字段”、“新增检查类型”、“完善验证机制” - 了解流程:“收货是如何运作的?”、“Worker具体执行了哪些步骤?” - 处理批次问题:“批次重复”、“数量有误”、“move.line未创建” 不适用场景: - 追踪财务单据(NF、PO、SO)→ 可使用“追踪Odoo”技能 - 发现未知模型字段 → 可使用“发现Odoo结构”技能 - 创建付款或对账流水 → 可使用“执行Odoo财务”技能 - 排查NF与PO的匹配问题(第二阶段验证)→ 可使用“NF-PO验证”技能 - 对PO进行对账(拆分/合并)→ 可使用“Odoo PO对账”技能

SKILL.md
--- frontmatter
name: recebimento-fisico-odoo
description: |
  Consulta e opera o modulo de Recebimento Fisico (Fase 4): preenchimento de lotes, quantidades e quality checks com processamento assincrono no Odoo via Redis Queue.

  USAR QUANDO:
  - Debugar recebimento fisico: "erro ao validar picking", "lote nao criou", "quality check falhou"
  - Modificar logica do worker: "alterar passos do processamento", "adicionar etapa"
  - Consultar status de recebimento: "picking nao foi para done", "status pendente"
  - Extender funcionalidade: "adicionar campo", "novo tipo de check", "nova validacao"
  - Entender fluxo: "como funciona o recebimento?", "quais passos o worker executa?"
  - Problema com lotes: "lote duplicado", "quantidade errada", "move.line nao criou"

  NAO USAR QUANDO:
  - Rastrear documentos fiscais (NF, PO, SO) -> usar rastreando-odoo
  - Descobrir campos de modelo desconhecido -> usar descobrindo-odoo-estrutura
  - Criar pagamentos ou reconciliar extratos -> usar executando-odoo-financeiro
  - Depurar match NF x PO (fase 2 validacao) -> usar validacao-nf-po
  - Conciliar POs (split/consolidacao) -> usar conciliando-odoo-po

Recebimento Fisico - Odoo (Fase 4)

Skill para operar o modulo de Recebimento Fisico: preencher lotes, quantidades e quality checks, com processamento assincrono no Odoo via Redis Queue (RQ).

Contexto

Quando um picking de compra (incoming) chega ao armazem, o operador precisa:

  1. Informar os lotes e quantidades de cada produto recebido
  2. Executar quality checks (aprovacao pass/fail ou medicao)
  3. Validar o picking no Odoo (button_validate → state=done)

O sistema sincroniza pickings do Odoo para cache local (APScheduler 30min), salva LOCALMENTE (imediato) e processa no Odoo via worker RQ (assincrono).

Arquitetura

code
APScheduler (30 min)              TELA 1 (pickings)              WORKER RQ
    Sync Odoo → Local         ─┐     Lista (cache local)         Processar pendentes
    4 tabelas normalizadas     │     Ao clicar → refresh Odoo    Enviar para Odoo
    Incremental (write_date)   │     Preencher lotes + QC        button_validate
                               └┐    SALVAR (banco local)    ────┘
                                │
TELA 2 (status)                 │
    Consultar Odoo              │
    Mostrar status              │
    Retry erros                 │

Arquivos Principais

  • Sync (cache): app/recebimento/services/picking_recebimento_sync_service.py
  • Service local: app/recebimento/services/recebimento_fisico_service.py
  • Service Odoo: app/recebimento/services/recebimento_fisico_odoo_service.py
  • Worker RQ: app/recebimento/workers/recebimento_fisico_jobs.py
  • Scheduler: app/scheduler/sincronizacao_incremental_definitiva.py (variavel: picking_recebimento_sync_service)

Classe Principal

python
from app.recebimento.services.recebimento_fisico_service import RecebimentoFisicoService
from app.recebimento.services.recebimento_fisico_odoo_service import RecebimentoFisicoOdooService

service = RecebimentoFisicoService()
odoo_service = RecebimentoFisicoOdooService()

Fluxo do Worker (8 Passos)

code
ENTRADA:
  - recebimento_id (RecebimentoFisico com status='pendente')

PASSO 1: Conectar Odoo e verificar picking (state=assigned)
  → get_odoo_connection() + authenticate()
  → Ler picking_data → se state != assigned, erro

PASSO 2: Resolver lotes com expiration_date (stock.lot manual)
  → Para cada lote com data_validade + use_expiration_date=True:
    a) Verificar se stock.lot ja existe (name + product_id + company_id)
    b) Se NAO: create('stock.lot', {name, product_id, company_id, expiration_date})
    c) Se SIM: write('stock.lot', lot_id, {expiration_date})
    d) Usar lot_id no passo 3 (em vez de lot_name)

PASSO 3: Preencher lotes (stock.move.line)
  → Para cada lote do recebimento:
    a) 1o lote: write(move_line_id, {lot_id/lot_name, quantity})
    b) Lotes extras: create('stock.move.line', {picking_id, move_id, product_id, lot_id/lot_name, quantity, locations})

PASSO 4: Quality checks passfail
  → execute_kw('quality.check', 'do_pass', [[check_id]])
  → execute_kw('quality.check', 'do_fail', [[check_id]])

PASSO 5: Quality checks measure
  → write('quality.check', check_id, {'measure': valor})
  → execute_kw('quality.check', 'do_measure', [[check_id]])

PASSO 6: Validar picking
  → execute_kw('stock.picking', 'button_validate', [[picking_id]])
  → Tratar "cannot marshal None" como sucesso

PASSO 7: Verificar resultado
  → Ler picking: state='done' = sucesso

PASSO 8: Atualizar status local
  → recebimento.status = 'processado' / 'erro'
  → Marcar lotes e checks como processado=True

Operacoes Suportadas

OperacaoMetodoResultado
Sync pickings (scheduler)sincronizar_pickings_incremental()Importa pickings do Odoo → cache local
Sync por periodosincronizar_por_periodo(data_de, data_ate)Importa pickings por datas absolutas (YYYY-MM-DD)
Refresh 1 pickingrefresh_picking(odoo_id)Busca fresco do Odoo e atualiza cache
Buscar pickings disponiveisbuscar_pickings_disponiveis()Le do cache local (dedup por PO/write_date)
Buscar detalhes pickingbuscar_detalhes_picking()Refresh Odoo + le cache
Salvar recebimentosalvar_recebimento()Grava local + enqueue RQ
Validar lotesvalidar_lotes()Soma lotes == qtd esperada
Processar no Odooprocessar_recebimento()8 passos completos
Retryretry_recebimento()Re-enqueue job
Consultar Odooconsultar_status_odoo()Estado real do picking

Tabelas de Cache (Sync APScheduler - 4 tabelas normalizadas)

TabelaModeloDescricao
picking_recebimentoPickingRecebimento1 linha por picking (state, partner, PO, dates)
picking_recebimento_produtoPickingRecebimentoProduto1 linha por stock.move (product, qty, tracking)
picking_recebimento_move_linePickingRecebimentoMoveLine1 linha por stock.move.line (lot, qty)
picking_recebimento_quality_checkPickingRecebimentoQualityCheck1 linha por quality.check

Sync: APScheduler a cada 30 min, janela 90 min (env: JANELA_PICKINGS) Filtro: picking_type_code='incoming' AND purchase_id != False Dedup: Por write_date (mais recente por PO)

Tabelas de Trabalho (Recebimento local)

TabelaModeloCampos-chave
recebimento_fisicoRecebimentoFisicoodoo_picking_id, status, tentativas, job_id, erro_mensagem
recebimento_loteRecebimentoLotelote_nome, quantidade, data_validade, processado, odoo_lot_id
recebimento_quality_checkRecebimentoQualityChecktest_type, resultado, valor_medido, processado

Modelos Odoo Envolvidos

ModeloCampos PrincipaisOperacoes
stock.pickingstate, picking_type_code, partner_id, origin, purchase_id, move_line_idssearch_read, button_validate
stock.moveproduct_id, product_uom_qty, move_line_idssearch_read
stock.move.linelot_name, quantity, qty_done, product_id, move_id, picking_id, location_idsearch_read, write, create
stock.lotname, product_id, company_id, expiration_datesearch, create, write (manual quando use_expiration_date=True)
quality.checkquality_state, test_type, measure, tolerance_min/max, norm_unitsearch_read, write, do_pass, do_fail, do_measure
quality.pointtest_type, product_category_ids, picking_type_ids, measure_onsearch_read (templates)
product.producttracking, use_expiration_datesearch_read

Campos stock.move.line (ATENCAO - Versao Odoo)

IMPORTANTE: O campo reserved_uom_qty NAO existe nesta versao do Odoo.

Campo OdooDescricaoUso
quantityQuantidade reservada/atribuidaQtd que o Odoo reservou
qty_doneQuantidade realizadaQtd efetivamente recebida
lot_nameNome do lote (cria auto)Usado quando NAO precisa de expiration_date
lot_idID do stock.lot existenteUsado quando precisa de expiration_date
location_idOrigem (obrigatorio no create)Vem do picking
location_dest_idDestino (obrigatorio no create)Vem do picking

Coluna local reserved_uom_qty: Armazena valor de qty_done do Odoo (nome historico mantido por compatibilidade).

Metodo de Conexao Odoo

python
from app.odoo.utils.connection import get_odoo_connection

odoo = get_odoo_connection()
odoo.authenticate()

# UNICO metodo generico:
resultado = odoo.execute_kw(modelo, metodo, args, kwargs)

# Helpers (wrappers de execute_kw):
odoo.search(modelo, domain, limit=None)
odoo.read(modelo, ids, fields)
odoo.write(modelo, id_ou_ids, values)
odoo.create(modelo, values)

NUNCA usar odoo.execute() — nao existe, causa AttributeError.

Redis (Worker RQ)

ChaveFormatoTTL
recebimento_progresso:{recebimento_id}JSON: {etapa, total, mensagem}1 hora
recebimento_lock:{picking_id}String: "locked" (nx=True)30 min

Queue: 'recebimento' (dedicada, timeout 10min por job)

APIs Disponiveis

RotaMetodoQuery Params / Body
/recebimento/fisico/pickingsGETcompany_id, filtro_nf, filtro_fornecedor
/recebimento/fisico/picking/<id>/detalhesGET-
/recebimento/fisico/salvarPOSTJSON: picking_id, company_id, lotes[], quality_checks[]
/recebimento/fisico/status/listarGETcompany_id, status, limit
/recebimento/fisico/status/<id>/retryPOST-
/recebimento/fisico/status/<id>/consultar-odooGET-

Tracking de Produtos

  • 73.6% dos produtos tem tracking='lot' (1191 de 1618)
  • Produtos com tracking='lot' EXIGEM lote no recebimento
  • Produtos com tracking='none' usam 'SEM_LOTE' como placeholder (nao cria lote no Odoo)

Cenarios de Uso

Recebimento simples (1 lote por produto)

code
Picking IN/00123: 500 un produto X (tracking=lot)
Operador preenche: Lote "L2026-001", Qtd 500

Worker:
  1. write(move_line_id, {lot_name: 'L2026-001', quantity: 500})
  2. button_validate → state=done
  3. Odoo cria stock.lot automaticamente

Recebimento multi-lote (N lotes por produto)

code
Picking IN/00124: 1000 un produto Y (tracking=lot)
Operador preenche:
  - Lote "L2026-A", Qtd 600
  - Lote "L2026-B", Qtd 400

Worker:
  1. write(move_line_existente, {lot_name: 'L2026-A', quantity: 600})
  2. create('stock.move.line', {..., lot_name: 'L2026-B', quantity: 400})
  3. button_validate → state=done

Recebimento com data de validade (expiration_date)

code
Picking IN/00125: 500 un produto W (tracking=lot, use_expiration_date=True)
Operador preenche: Lote "L2026-VAL", Qtd 500, Validade 15/06/2026

Worker (_resolver_lote):
  1. Buscar stock.lot existente: search([name='L2026-VAL', product_id=W])
  2. NAO existe → create('stock.lot', {name, product_id, company_id, expiration_date: '2026-06-15 00:00:00'})
  3. write(move_line, {lot_id: novo_lot_id, quantity: 500})  ← usa lot_id, NAO lot_name
  4. button_validate → state=done (lote JA existe com validade)

Recebimento com quality check (measure)

code
Picking IN/00126: 200 kg produto Z + check de umidade
  - Quality check: measure, tolerance 10-15%

Operador: Valor medido = 12.5% → PASS

Worker:
  1. Preencher lotes
  2. write('quality.check', check_id, {'measure': 12.5})
  3. execute_kw('quality.check', 'do_measure', [[check_id]])
  4. button_validate

Validacao Cross-Phase (Pipeline Obrigatorio)

A Fase 4 (Recebimento Fisico) e a etapa FINAL de um pipeline sequencial obrigatorio:

code
Fase 1 (Fiscal) → Fase 2 (Match NF×PO) → Fase 3 (Consolidacao PO) → Fase 4 (Recebimento)

Service: CrossPhaseValidationService

Arquivo: app/recebimento/services/cross_phase_validation_service.py

python
from app.recebimento.services.cross_phase_validation_service import CrossPhaseValidationService

service = CrossPhaseValidationService()

# Validar 1 picking
phase_status = service.validar_fases_picking(purchase_order_id, origin)
# → PhaseStatus: pode_receber, tipo_liberacao, bloqueio_motivo, contato_resolucao, diagnostico

# Validar batch (lista de pickings, 2 queries max)
results = service.validar_fases_batch(pickings)
# → Dict: {odoo_picking_id: PhaseStatus}

Tipos de Liberacao

tipo_liberacaoSignificadoQuando
'full'Consolidacao executada (Fase 3 manual)ValidacaoNfPoDfe.status='consolidado'
'finalizado_odoo'Odoo ja vinculou PO corretoValidacaoNfPoDfe.status='finalizado_odoo'
'legacy'Picking anterior ao sistemaSem DFE ou sem PO

Bloqueios Possiveis

BloqueioCausaContato
Fase 1 bloqueadaDivergencia fiscalFiscal
Fase 1 nao executadaScheduler nao rodouSistema
Fase 1 primeira compraCadastro pendenteFiscal
Fase 2 bloqueadaMatch NF×PO incompletoCompras
Fase 3 aguardandoMatch 100% OK mas consolidacao nao executadaCompras
Fase 3 pendenteFases anteriores nao concluiramCompras

REGRA CRITICA: status='aprovado' NAO e pass-through

ValidacaoNfPoDfe.status='aprovado' significa "pronto para consolidar" — a Fase 3 AINDA NAO foi executada. A consolidacao SEMPRE cria um PO Conciliador novo. Pular causa: NF desvinculada, estoque duplicado.

Pontos de Integracao

MetodoTipoEfeito
buscar_pickings_disponiveis()Badge na listaCampo fase_validacao em cada picking
buscar_detalhes_picking()Diagnostico completoCampos validacao_fases + validacao_id_sugerido
salvar_recebimento()HARD BLOCKRaise ValueError se pode_receber=False

Lookup Chain

code
PickingRecebimento.odoo_purchase_order_id
    → ValidacaoNfPoDfe.odoo_po_vinculado_id (ou po_consolidado_id)
        → ValidacaoFiscalDfe.odoo_dfe_id

Indice de Performance

sql
CREATE INDEX idx_validacao_nf_po_po_consolidado
ON validacao_nf_po_dfe (po_consolidado_id)
WHERE po_consolidado_id IS NOT NULL;

Script de migration: scripts/add_index_po_consolidado.py

Pre-requisitos para Processar

  1. Validacao cross-phase aprovada (Fases 1→2→3 concluidas)
  2. Picking com state='assigned'
  3. picking_type_code='incoming'
  4. Soma dos lotes == product_uom_qty do move
  5. TODOS os quality checks com resultado definido
  6. Conexao Odoo funcional

Referencia Cruzada com Outras Skills

SkillQuando usar em vez desta
validacao-nf-poANTES do recebimento fisico (match NF x PO, fase 2)
conciliando-odoo-poSe precisar split/consolidar POs antes de receber
rastreando-odooCONSULTAR documentos (nao executar)
descobrindo-odoo-estruturaExplorar campos nao documentados
executando-odoo-financeiroOperacoes financeiras pos-recebimento