fp.receiving simplifies to box-count-only (new primary state machine: draft → counted → staged → closed). Legacy inspecting/accepted/discrepancy/resolved states stay in the Selection so existing records load without error but are surfaced behind a manager-only toggle. New box_count_in field + banner that tells the receiver "count boxes only — parts are inspected by the racking crew." New fp.racking.inspection + fp.racking.inspection.line models — one record per MO, auto-created by mrp.production.create() with one line per contributing SO line (qty_expected seeded, qty_found + condition filled in by the racking crew when they open the boxes). State: draft → inspecting → done | discrepancy_flagged (flagged when any line has a non-ok condition or qty variance). Reopen restricted to Plating Manager. WO soft gate: first plating WO button_start raises a UserError when the MO's racking inspection is still Draft or Inspecting. Plating Manager bypasses; later WOs are not gated. fp.delivery gains x_fc_box_count_out. action_mark_delivered calls _fp_check_box_parity which posts a non-blocking chatter warning when boxes out ≠ boxes in (resolved via job_ref → MO.origin → SO → receiving). Warning only — never blocks shipping. Menu entry: Plating → Operations → Racking Inspection. Module version bumps: fusion_plating_receiving → 19.0.3.0.0 fusion_plating_logistics → 19.0.3.0.0 fusion_plating_bridge_mrp → 19.0.12.0.0 (+depends receiving) Smoke on entech: 12/12 assertions pass (one gate test skipped — MO had no WOs to test) including box-count state machine, inspection auto-create, lifecycle, discrepancy flag, and box-parity chatter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
169 lines
5.8 KiB
Python
169 lines
5.8 KiB
Python
"""Sub 8 smoke test — runs inside odoo-shell on entech."""
|
|
env = env
|
|
|
|
Partner = env['res.partner']
|
|
SO = env['sale.order']
|
|
Receiving = env['fp.receiving']
|
|
Insp = env['fp.racking.inspection']
|
|
Delivery = env['fusion.plating.delivery']
|
|
|
|
# ---- Field / model presence -----------------------------------------
|
|
assert 'box_count_in' in Receiving._fields
|
|
assert 'x_fc_box_count_out' in Delivery._fields
|
|
assert hasattr(Insp, 'action_start')
|
|
assert hasattr(Insp, 'action_complete')
|
|
print('[OK] Models + fields present')
|
|
|
|
# ---- Receiving state machine -----------------------------------------
|
|
cust = Partner.create({
|
|
'name': 'Sub 8 Smoke Customer',
|
|
'is_company': True,
|
|
'customer_rank': 1,
|
|
'email': 'sub8@test.com',
|
|
})
|
|
Product = env['product.product']
|
|
product = Product.search([('sale_ok', '=', True)], limit=1)
|
|
so = SO.create({
|
|
'partner_id': cust.id,
|
|
'x_fc_po_number': 'PO-SUB8',
|
|
'x_fc_po_received': True,
|
|
'order_line': [(0, 0, {
|
|
'product_id': product.id,
|
|
'product_uom_qty': 10,
|
|
'name': 'Sub 8 smoke',
|
|
'x_fc_internal_description': 'smoke',
|
|
})],
|
|
})
|
|
|
|
recv = Receiving.create({'sale_order_id': so.id, 'expected_qty': 10})
|
|
assert recv.state == 'draft'
|
|
|
|
# Fail: mark counted without box count
|
|
try:
|
|
recv.action_mark_counted()
|
|
assert False, 'should require box_count_in'
|
|
except Exception as e:
|
|
assert 'box' in str(e).lower() or 'Boxes' in str(e)
|
|
print('[OK] mark_counted blocked without box_count_in')
|
|
|
|
recv.box_count_in = 4
|
|
recv.action_mark_counted()
|
|
assert recv.state == 'counted'
|
|
print(f'[OK] mark_counted → state={recv.state}, boxes={recv.box_count_in}')
|
|
|
|
recv.action_mark_staged()
|
|
assert recv.state == 'staged'
|
|
print(f'[OK] mark_staged → state={recv.state}')
|
|
|
|
recv.action_close()
|
|
assert recv.state == 'closed'
|
|
print(f'[OK] close → state={recv.state}')
|
|
|
|
# ---- MO auto-creates racking inspection ------------------------------
|
|
so.action_confirm()
|
|
MO = env['mrp.production']
|
|
mos = MO.search([('origin', '=', so.name)])
|
|
assert mos, 'MO should be created by SO confirm'
|
|
mo = mos[0]
|
|
print(f'[OK] MO created: {mo.name}')
|
|
|
|
inspections = Insp.search([('production_id', '=', mo.id)])
|
|
assert len(inspections) == 1, f'expected 1 inspection, got {len(inspections)}'
|
|
inspection = inspections[0]
|
|
assert inspection.state == 'draft'
|
|
assert len(inspection.line_ids) == 1
|
|
assert inspection.line_ids[0].qty_expected == 10
|
|
print(f'[OK] Racking inspection auto-created: {inspection.name}')
|
|
|
|
# ---- Inspection lifecycle --------------------------------------------
|
|
inspection.action_start()
|
|
assert inspection.state == 'inspecting'
|
|
assert inspection.inspector_id == env.user
|
|
print('[OK] Inspection started')
|
|
|
|
# OK case
|
|
inspection.line_ids[0].write({'qty_found': 10, 'condition': 'ok'})
|
|
inspection.action_complete()
|
|
assert inspection.state == 'done', f'expected done, got {inspection.state}'
|
|
print(f'[OK] Inspection complete → state={inspection.state}')
|
|
|
|
# ---- Flag a discrepancy on a separate inspection ---------------------
|
|
# Reopen + set damage
|
|
inspection.action_reopen()
|
|
inspection.line_ids[0].write({'qty_found': 8, 'condition': 'major'})
|
|
inspection.action_complete()
|
|
assert inspection.state == 'discrepancy_flagged'
|
|
print(f'[OK] Discrepancy flagged → state={inspection.state}')
|
|
|
|
# ---- WO soft gate --------------------------------------------------
|
|
# Reopen inspection to 'inspecting' to test the gate
|
|
inspection.action_reopen()
|
|
assert inspection.state == 'inspecting'
|
|
first_wo = mo.workorder_ids.sorted('sequence')[:1]
|
|
if first_wo:
|
|
try:
|
|
# Soft gate — manager bypasses. Try with a non-manager user if available.
|
|
demo_user = env['res.users'].search([('login', '=', 'demo')], limit=1)
|
|
if demo_user:
|
|
first_wo.with_user(demo_user)._fp_warn_if_racking_inspection_pending()
|
|
assert False, 'soft gate should raise for non-manager'
|
|
else:
|
|
# No demo user — check manager bypass works (env.user is admin)
|
|
first_wo._fp_warn_if_racking_inspection_pending()
|
|
print('[OK] Manager bypass (no demo user to test block)')
|
|
except Exception as e:
|
|
if 'Racking inspection' in str(e) or 'Inspecting' in str(e):
|
|
print('[OK] Soft gate blocks non-manager when inspection pending')
|
|
else:
|
|
raise
|
|
else:
|
|
print('[SKIP] No WOs on this MO to test gate')
|
|
|
|
# Mark inspection done and verify gate is clear
|
|
inspection.line_ids[0].qty_found = 10
|
|
inspection.line_ids[0].condition = 'ok'
|
|
inspection.action_complete()
|
|
assert inspection.state == 'done'
|
|
if first_wo:
|
|
first_wo._fp_warn_if_racking_inspection_pending() # should not raise
|
|
print('[OK] Gate clears when inspection is Done')
|
|
|
|
# ---- Box parity on delivery ------------------------------------------
|
|
delivery = Delivery.create({
|
|
'partner_id': cust.id,
|
|
'job_ref': mo.name,
|
|
'x_fc_box_count_out': 3,
|
|
})
|
|
delivery._fp_check_box_parity()
|
|
# Check chatter for the warning
|
|
messages = env['mail.message'].search([
|
|
('model', '=', 'fusion.plating.delivery'),
|
|
('res_id', '=', delivery.id),
|
|
])
|
|
body = ' '.join(m.body for m in messages if m.body)
|
|
assert 'parity' in body.lower() or 'boxes' in body.lower() or 'shipped' in body.lower(), (
|
|
f'expected parity warning; got messages: {body[:200]}'
|
|
)
|
|
print('[OK] Box-parity warning posted on delivery chatter')
|
|
|
|
# Matching counts — no warning
|
|
delivery2 = Delivery.create({
|
|
'partner_id': cust.id,
|
|
'job_ref': mo.name,
|
|
'x_fc_box_count_out': 4, # matches receiving.box_count_in
|
|
})
|
|
before = env['mail.message'].search_count([
|
|
('model', '=', 'fusion.plating.delivery'),
|
|
('res_id', '=', delivery2.id),
|
|
])
|
|
delivery2._fp_check_box_parity()
|
|
after = env['mail.message'].search_count([
|
|
('model', '=', 'fusion.plating.delivery'),
|
|
('res_id', '=', delivery2.id),
|
|
])
|
|
assert after == before, f'expected no new message, before={before} after={after}'
|
|
print('[OK] No warning when boxes match')
|
|
|
|
env.cr.rollback()
|
|
print('\n=== SUB 8 SMOKE PASS — all assertions held ===')
|