test(numbering): E2E walkthrough — quote -> SO -> WO -> IN -> CoC -> DLV -> RCV -> Hold -> RMA

Verified pass on entech (parent=30015): all linked docs share the
parent number, immutability + unlink-block + direct-invoice-block
all enforced. NCR/CAPA fall back to legacy sequences as designed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-12 14:35:29 -04:00
parent fdbbd2852a
commit b5416d242c

View File

@@ -0,0 +1,136 @@
"""End-to-end numbering walkthrough for the 2026-05-12 parent-number
hierarchy. Quote -> confirm -> 2 invoices (partial billing) -> CoC ->
delivery -> receiving -> NCR (legacy fallback) -> Hold (parent-derived)
-> immutability check -> unlink block check -> direct invoice block.
Asserts every SO-linked doc shares the same parent number. Re-runnable;
rolls back at the end so no DB state is left behind.
Run via odoo-shell:
exec(open('/path/to/numbering_e2e_walkthrough.py').read())
"""
from odoo.exceptions import UserError
SO = env['sale.order']
AM = env['account.move']
journal = env['account.journal'].search([('type', '=', 'sale')], limit=1)
parts = env['fp.part.catalog'].search([('default_process_id', '!=', False)], limit=2)
assert len(parts) >= 2, 'need at least 2 parts with default recipes'
partner = env['res.partner'].search([], limit=1)
facility = env['fusion.plating.facility'].search([], limit=1)
product = env['product.product'].search([('type', '!=', 'service')], limit=1) or env['product.product'].search([], limit=1)
print('=' * 60)
print('Numbering hierarchy E2E walkthrough')
print('=' * 60)
# === A: Quote -> confirm ===
so = SO.create({
'partner_id': partner.id,
'x_fc_po_override': True,
'order_line': [
(0, 0, {'product_id': product.id, 'product_uom_qty': 5,
'x_fc_part_catalog_id': parts[0].id, 'sequence': 10}),
(0, 0, {'product_id': product.id, 'product_uom_qty': 3,
'x_fc_part_catalog_id': parts[1].id, 'sequence': 20}),
],
})
quote_name = so.name
print(f'A. Quote: {quote_name}')
assert quote_name.startswith('Q'), f'expected Q-prefix, got {quote_name}'
so.action_confirm()
parent = so.x_fc_parent_number
print(f'A. Confirmed: {so.name} (parent={parent}, quote_ref={so.x_fc_quote_ref})')
assert so.name == f'SO-{parent}'
assert so.x_fc_quote_ref == quote_name
# === B: WOs (2 recipes split SO into -01, -02) ===
jobs = env['fp.job'].search([('sale_order_id', '=', so.id)], order='x_fc_doc_index')
print(f'B. WOs: {jobs.mapped("name")}')
assert len(jobs) == 2
assert jobs[0].name == f'WO-{parent}-01'
assert jobs[1].name == f'WO-{parent}-02'
# === C: Two invoices (partial billing) ===
inv1 = AM.with_context(fp_from_so_invoice=True, fp_invoice_source_so_id=so.id).create({
'move_type': 'out_invoice', 'partner_id': partner.id,
'journal_id': journal.id, 'invoice_origin': so.name,
})
inv2 = AM.with_context(fp_from_so_invoice=True, fp_invoice_source_so_id=so.id).create({
'move_type': 'out_invoice', 'partner_id': partner.id,
'journal_id': journal.id, 'invoice_origin': so.name,
})
print(f'C. Invoices: {inv1.name}, {inv2.name}')
assert inv1.name == f'IN-{parent}'
assert inv2.name == f'IN-{parent}-02'
# === D: CoC ===
coc = env['fp.certificate'].create({'sale_order_id': so.id, 'partner_id': partner.id})
print(f'D. CoC: {coc.name}')
assert coc.name == f'CoC-{parent}'
# === E: Delivery (linked via job_ref) ===
dlv = env['fusion.plating.delivery'].create({'partner_id': partner.id, 'job_ref': jobs[0].name})
print(f'E. Delivery: {dlv.name}')
assert dlv.name == f'DLV-{parent}'
# === F: Receiving (already auto-created at confirm; manual is -02) ===
existing_rcv = env['fp.receiving'].search([('sale_order_id', '=', so.id)])
print(f'F. Receivings (incl. auto-created): {existing_rcv.mapped("name")}')
assert any(r.name == f'RCV-{parent}' for r in existing_rcv)
# === G: Hold (via job_id) ===
hold = env['fusion.plating.quality.hold'].create({
'job_id': jobs[0].id, 'hold_reason': 'qc_failure', 'qty_on_hold': 1,
'description': 'E2E test hold',
})
print(f'G. Hold: {hold.name}')
assert hold.name == f'HOLD-{parent}'
# === H: RMA (via sale_order_id directly) ===
rma = env['fusion.plating.rma'].create({'sale_order_id': so.id, 'partner_id': partner.id})
print(f'H. RMA: {rma.name}')
assert rma.name == f'RMA-{parent}'
# === I: NCR + CAPA (no SO link in core -> legacy seq) ===
ncr = env['fusion.plating.ncr'].create({
'description': 'E2E test', 'customer_partner_id': partner.id, 'facility_id': facility.id,
})
print(f'I. NCR (no SO link): {ncr.name}')
assert not ncr.name.startswith('NCR-3'), f'expected legacy seq, got {ncr.name}'
capa = env['fusion.plating.capa'].create({
'description': 'E2E test capa', 'ncr_id': ncr.id, 'facility_id': facility.id,
})
print(f'I. CAPA (no SO link): {capa.name}')
assert not capa.name.startswith('CAPA-3'), f'expected legacy seq, got {capa.name}'
# === J: Immutability ===
try:
jobs[0].name = 'HACKED'
print('FAIL J: name mutation succeeded')
except UserError:
print('J. OK: WO name immutable')
# === K: Unlink block ===
try:
coc.unlink()
print('FAIL K: unlink succeeded')
except UserError:
print('K. OK: CoC unlink blocked')
# === L: Direct invoice creation block ===
try:
AM.create({
'move_type': 'out_invoice', 'partner_id': partner.id, 'journal_id': journal.id,
})
print('FAIL L: direct invoice succeeded')
except UserError:
print('L. OK: direct invoice blocked')
print('=' * 60)
print(f'PASS: every doc tied to parent {parent}')
print('=' * 60)
env.cr.rollback()
print('(rolled back — DB unchanged)')