feat(plating): Sub 5 — order-line fields (serial, job#, thickness, revision)
Four new fields on every sale.order.line, propagated through to MO, Delivery, and Invoice for end-to-end traceability: - fp.serial registry (new model in configurator) with smart-button traceability to Sale Order, MO, Delivery, Invoice, Part. M2O on SO line; optional; user types a customer serial or clicks Generate Serial for a sequence-backed one. Reverse O2M links split across configurator (invoice) / bridge_mrp (MO) / logistics (delivery) so module load order is respected. - x_fc_job_number on SO line, auto-sequenced FP-JOB-NNNNN on SO confirm. Editable — shops can override for customer/legacy schemes. - fp.coating.thickness (new child of fp.coating.config) with per- config discrete thickness options; x_fc_thickness_id on SO line domain-filtered to the line's coating. Auto-clears when coating changes. - x_fc_revision_snapshot Char on SO line, frozen from x_fc_part_catalog_id.revision at save. Protects historical SOs from later catalog edits. Secondary "Revision" picker on the tree view lets users switch between prior revisions of the same part number; the Part M2O still surfaces only is_latest_revision rows. Reports (CoC, packing slip, invoice, BoL) pick up all four via the Sub 2 customer_line_header macro — one macro edit, four reports. Smoke on entech: 11 assertions pass including revision snapshot, generate-serial button, typed-serial create-on-fly, coating→thickness domain reset, SO confirm auto job#, and MO traceability carry. Module version bumps: fusion_plating_configurator → 19.0.12.0.0 fusion_plating_bridge_mrp → 19.0.11.0.0 fusion_plating_logistics → 19.0.2.0.0 (+depends configurator) fusion_plating_reports → 19.0.5.1.0 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
149
fusion_plating/docs/superpowers/tests/2026-04-22-sub5-smoke.py
Normal file
149
fusion_plating/docs/superpowers/tests/2026-04-22-sub5-smoke.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""Sub 5 smoke test — runs inside odoo-shell on entech."""
|
||||
env = env
|
||||
|
||||
Partner = env['res.partner']
|
||||
Part = env['fp.part.catalog']
|
||||
Coating = env['fp.coating.config']
|
||||
Thickness = env['fp.coating.thickness']
|
||||
Serial = env['fp.serial']
|
||||
SO = env['sale.order']
|
||||
admin = env.ref('base.user_admin')
|
||||
|
||||
# ---- Seed customer + part + coating + thickness options ---------------
|
||||
cust = Partner.create({
|
||||
'name': 'Sub5 Smoke Customer',
|
||||
'is_company': True,
|
||||
'customer_rank': 1,
|
||||
})
|
||||
part_a = Part.create({
|
||||
'partner_id': cust.id,
|
||||
'part_number': 'SUB5-001',
|
||||
'revision': 'A',
|
||||
})
|
||||
# Bump to a newer revision so we have A + B in the chain
|
||||
part_a.action_create_revision()
|
||||
part_b = Part.search([
|
||||
('parent_part_id', '=', (part_a.parent_part_id or part_a).id),
|
||||
('is_latest_revision', '=', True),
|
||||
], limit=1, order='revision_number desc')
|
||||
assert part_b.revision != part_a.revision, 'revision chain failed'
|
||||
print(f'[OK] Part revisions: A={part_a.revision} / latest={part_b.revision}')
|
||||
|
||||
coating = Coating.search([], limit=1) or Coating.create({
|
||||
'name': 'Sub5 Coating Test',
|
||||
'process_type_id': env['fusion.plating.process.type'].search([], limit=1).id,
|
||||
})
|
||||
t1 = Thickness.create({
|
||||
'coating_config_id': coating.id,
|
||||
'value': 0.001, 'uom': 'inches', 'sequence': 10,
|
||||
})
|
||||
t2 = Thickness.create({
|
||||
'coating_config_id': coating.id,
|
||||
'value': 0.0015, 'uom': 'inches', 'sequence': 20,
|
||||
})
|
||||
print(f'[OK] Thickness options: {t1.display_name} / {t2.display_name}')
|
||||
|
||||
# ---- Create SO with latest revision, assign serial via create-on-fly --
|
||||
product = env['product.product'].search([('sale_ok', '=', True)], limit=1)
|
||||
so = SO.create({
|
||||
'partner_id': cust.id,
|
||||
'x_fc_po_number': 'PO-SUB5-SMOKE',
|
||||
'x_fc_po_received': True,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': 10,
|
||||
'name': 'Sub5 smoke line',
|
||||
'x_fc_part_catalog_id': part_b.id,
|
||||
'x_fc_coating_config_id': coating.id,
|
||||
'x_fc_thickness_id': t1.id,
|
||||
'x_fc_internal_description': 'smoke',
|
||||
})],
|
||||
})
|
||||
line = so.order_line[0]
|
||||
line.invalidate_recordset()
|
||||
assert line.x_fc_revision_snapshot == part_b.revision, (
|
||||
f'snapshot mismatch: {line.x_fc_revision_snapshot} vs {part_b.revision}'
|
||||
)
|
||||
print(f'[OK] Revision snapshot captured on create: {line.x_fc_revision_snapshot}')
|
||||
|
||||
# ---- Generate serial button -----------------------------------------
|
||||
line.action_generate_serial()
|
||||
line.invalidate_recordset()
|
||||
assert line.x_fc_serial_id, 'serial should be assigned'
|
||||
assert line.x_fc_serial_id.name.startswith('FP-SN-'), (
|
||||
f'unexpected serial name: {line.x_fc_serial_id.name}'
|
||||
)
|
||||
print(f'[OK] Generate serial: {line.x_fc_serial_id.name}')
|
||||
|
||||
# ---- Customer-typed serial --------------------------------------------
|
||||
so2 = SO.create({
|
||||
'partner_id': cust.id,
|
||||
'x_fc_po_number': 'PO-SUB5-SMOKE-2',
|
||||
'x_fc_po_received': True,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': 5,
|
||||
'name': 'Sub5 smoke line 2',
|
||||
'x_fc_part_catalog_id': part_b.id,
|
||||
'x_fc_coating_config_id': coating.id,
|
||||
'x_fc_internal_description': 'smoke2',
|
||||
})],
|
||||
})
|
||||
line2 = so2.order_line[0]
|
||||
typed_serial = Serial.create({'name': 'CUST-999'})
|
||||
line2.x_fc_serial_id = typed_serial.id
|
||||
line2.invalidate_recordset()
|
||||
assert line2.x_fc_serial_id.name == 'CUST-999'
|
||||
print('[OK] Customer-typed serial attached')
|
||||
|
||||
# ---- Revision picker: switch to Rev A --------------------------------
|
||||
line.x_fc_revision_pick_id = part_a.id
|
||||
line.invalidate_recordset()
|
||||
assert line.x_fc_part_catalog_id == part_a, (
|
||||
f'picker did not re-point part: {line.x_fc_part_catalog_id.revision}'
|
||||
)
|
||||
assert line.x_fc_revision_snapshot == part_a.revision
|
||||
print(f'[OK] Revision picker: switched to {part_a.revision}')
|
||||
|
||||
# ---- Confirm SO → job number auto-assigned ---------------------------
|
||||
so.action_confirm()
|
||||
line.invalidate_recordset()
|
||||
assert line.x_fc_job_number, 'job number should be auto-assigned on confirm'
|
||||
assert line.x_fc_job_number.startswith('FP-JOB-'), (
|
||||
f'unexpected job number: {line.x_fc_job_number}'
|
||||
)
|
||||
print(f'[OK] SO confirmed → job number: {line.x_fc_job_number}')
|
||||
|
||||
# ---- MO carry-over (if bridge_mrp auto-created an MO) -----------------
|
||||
MO = env['mrp.production']
|
||||
mos = MO.search([('origin', '=', so.name)])
|
||||
if mos:
|
||||
mo = mos[0]
|
||||
print(f'[OK] MO created ({mo.name}) → serial={mo.x_fc_serial_id.name if mo.x_fc_serial_id else "—"} / job={mo.x_fc_job_number or "—"} / thickness={mo.x_fc_thickness_id.display_name if mo.x_fc_thickness_id else "—"} / rev={mo.x_fc_revision_snapshot or "—"}')
|
||||
if mo.x_fc_serial_id:
|
||||
assert mo.x_fc_serial_id == line.x_fc_serial_id, 'MO serial mismatch'
|
||||
print('[OK] Serial carried MO ← SO line')
|
||||
else:
|
||||
print('[SKIP] No MO auto-created for this SO')
|
||||
|
||||
# ---- fp.serial smart-button counts reflect reality -------------------
|
||||
serial = line.x_fc_serial_id
|
||||
serial.invalidate_recordset()
|
||||
prod_cnt = serial.production_count
|
||||
del_cnt = serial.delivery_count
|
||||
print(f'[OK] Serial {serial.name}: MOs={prod_cnt}, Deliveries={del_cnt}, Invoices={serial.invoice_count}')
|
||||
|
||||
# ---- Thickness dropdown domain ----------------------------------------
|
||||
another_coating = Coating.create({
|
||||
'name': 'Sub5 Other Coating',
|
||||
'process_type_id': coating.process_type_id.id,
|
||||
})
|
||||
line.x_fc_coating_config_id = another_coating.id
|
||||
line._onchange_coating_clears_thickness()
|
||||
line.invalidate_recordset()
|
||||
assert not line.x_fc_thickness_id, 'thickness should clear when coating changes'
|
||||
print('[OK] Thickness clears when coating switches')
|
||||
|
||||
# ---- Cleanup ---------------------------------------------------------
|
||||
env.cr.rollback()
|
||||
print('\n=== SUB 5 SMOKE PASS — all assertions held ===')
|
||||
Reference in New Issue
Block a user