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:
@@ -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)')
|
||||
Reference in New Issue
Block a user