From b5416d242c3b774cf83a791861ebe8975ba479ce Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Tue, 12 May 2026 14:35:29 -0400 Subject: [PATCH] =?UTF-8?q?test(numbering):=20E2E=20walkthrough=20?= =?UTF-8?q?=E2=80=94=20quote=20->=20SO=20->=20WO=20->=20IN=20->=20CoC=20->?= =?UTF-8?q?=20DLV=20->=20RCV=20->=20Hold=20->=20RMA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../scripts/numbering_e2e_walkthrough.py | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 fusion_plating/fusion_plating_jobs/scripts/numbering_e2e_walkthrough.py diff --git a/fusion_plating/fusion_plating_jobs/scripts/numbering_e2e_walkthrough.py b/fusion_plating/fusion_plating_jobs/scripts/numbering_e2e_walkthrough.py new file mode 100644 index 00000000..235b22d6 --- /dev/null +++ b/fusion_plating/fusion_plating_jobs/scripts/numbering_e2e_walkthrough.py @@ -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)')