feat(plating): close 2 workflow gaps surfaced by workforce E2E simulation
Built a comprehensive simulator (scripts/fp_e2e_workforce.py) that
role-plays 10 employees driving an order quote → invoice using real
operator timers (button_start / button_finish with elapsed time.sleep).
Initial run: 31 PASS / 2 WARN / 0 FAIL exposed two gaps that would
hurt a real shop:
**Gap 1 — Thickness readings never reached the CoC**
The Fischerscope readings inspectors take during post-plate inspection
had no path to the CoC. The cert came out empty, useless for AS9100
or aerospace audits.
Fixes:
- New tablet endpoint `/fp/shopfloor/log_thickness_reading` so the
inspector can record one reading at a time during the inspection WO
(auto-numbers, defaults the operator, supports microscope image).
- mrp_production._fp_mark_done_post_actions now bulk-links any
orphan thickness readings (those with production_id=mo.id but no
certificate_id) to the freshly-created CoC. So inspectors can log
during inspection AND the cert PDF picks them up automatically.
**Gap 2 — Operator queue leaked other people's work + simulator missed it**
fusion.plating.operator.queue.build_for_user pulled EVERY ready /
in-progress WO regardless of assignment. Tom would see John's masking
WO in his "Up Next" list — bad for aerospace traceability where you
want strict per-operator accountability.
Fix: build_for_user now filters MRP WOs by
`(x_fc_assigned_user_id == user_id OR x_fc_assigned_user_id == False)`.
Operators see their own assigned tasks first, plus any unassigned
tasks anyone can grab. Other operators' assigned WOs no longer leak
through.
Also caught: simulator was using wrong field name on the queue model.
Fixed and added a "queue isolation" check that verifies no operator
sees another operator's assigned WOs.
After fixes: **39 PASS / 2 WARN / 0 FAIL** (out of 41 checks).
Remaining WARNs are both expected behaviour:
- bake-window auto-create: this coating doesn't require_bake_relief
(the recipe has an inline Oven step instead)
- first-piece gate: same — coating-driven, only fires when needed
Areas validated end-to-end:
- quote → SO with PO# carried into client_order_ref
- SO confirm → MO + portal job auto-created
- receiving qty prefill + accept
- 9 WOs generated from recipe + assigned to specific operators
- All 9 WOs ran with real elapsed timers + 17 productivity records
across 4 distinct operators
- MO done triggers CoC auto-issue with 5 thickness readings linked,
319 KB rich PDF, customer-slug filename
- Delivery auto-created with prefilled date + driver + CoC link
- Delivery delivered, 2 chain-of-custody entries
- Invoice posted (NOT auto-paid)
- All 5 customer notifications fired (so_confirmed +
parts_received + mo_complete + shipped + invoice_posted) with
correct attachments
- Portal job → complete, SO workflow_stage → invoicing
- Chemistry log persisted, operator proficiency tracked
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Shop Floor',
|
||||
'version': '19.0.14.1.0',
|
||||
'version': '19.0.14.2.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer, '
|
||||
'first-piece inspection gates.',
|
||||
|
||||
@@ -256,6 +256,75 @@ class FpShopfloorController(http.Controller):
|
||||
'duration': wo.duration,
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Thickness reading — Fischerscope log entry from inspection station
|
||||
# ----------------------------------------------------------------------
|
||||
@http.route('/fp/shopfloor/log_thickness_reading', type='jsonrpc', auth='user')
|
||||
def log_thickness_reading(self, production_id, nip_mils=None,
|
||||
ni_percent=None, p_percent=None,
|
||||
position_label=None, reading_number=None,
|
||||
equipment_model=None, calibration_std_ref=None,
|
||||
microscope_image=None,
|
||||
microscope_image_filename=None):
|
||||
"""Record a single Fischerscope reading against an MO.
|
||||
|
||||
Auto-links to the CoC certificate later when the MO is marked
|
||||
done (see mrp_production._fp_mark_done_post_actions). Keeps the
|
||||
endpoint simple so the inspector can fire-and-forget per reading.
|
||||
"""
|
||||
Reading = request.env.get('fp.thickness.reading')
|
||||
if Reading is None:
|
||||
return {'ok': False, 'error': 'Certificates module not installed'}
|
||||
mo = request.env['mrp.production'].browse(int(production_id))
|
||||
if not mo.exists():
|
||||
return {'ok': False, 'error': f'MO {production_id} not found'}
|
||||
|
||||
# Auto-number if caller didn't pass one.
|
||||
if not reading_number:
|
||||
existing = Reading.search_count([('production_id', '=', mo.id)])
|
||||
reading_number = existing + 1
|
||||
|
||||
vals = {
|
||||
'production_id': mo.id,
|
||||
'reading_number': int(reading_number),
|
||||
'nip_mils': float(nip_mils or 0.0),
|
||||
'ni_percent': float(ni_percent or 0.0),
|
||||
'p_percent': float(p_percent or 0.0),
|
||||
'position_label': position_label or '',
|
||||
'operator_id': request.env.user.id,
|
||||
}
|
||||
if equipment_model:
|
||||
vals['equipment_model'] = equipment_model
|
||||
if calibration_std_ref:
|
||||
vals['calibration_std_ref'] = calibration_std_ref
|
||||
# If the inspector snapped a microscope image, attach it.
|
||||
if microscope_image:
|
||||
import base64 as _b64
|
||||
att = request.env['ir.attachment'].create({
|
||||
'name': microscope_image_filename or f'thickness_{reading_number}.jpg',
|
||||
'datas': microscope_image,
|
||||
'res_model': 'fp.thickness.reading',
|
||||
'mimetype': 'image/jpeg',
|
||||
})
|
||||
vals['microscope_image_id'] = att.id
|
||||
|
||||
# Auto-link to existing CoC if one already exists for this MO.
|
||||
Cert = request.env.get('fp.certificate')
|
||||
if Cert is not None:
|
||||
existing_cert = Cert.search([
|
||||
('production_id', '=', mo.id),
|
||||
('certificate_type', '=', 'coc'),
|
||||
], limit=1)
|
||||
if existing_cert:
|
||||
vals['certificate_id'] = existing_cert.id
|
||||
|
||||
reading = Reading.create(vals)
|
||||
return {
|
||||
'ok': True,
|
||||
'reading_id': reading.id,
|
||||
'reading_number': reading.reading_number,
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Quality hold — partial qty split
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
@@ -81,11 +81,22 @@ class FpOperatorQueue(models.TransientModel):
|
||||
})
|
||||
|
||||
# ----- MRP work orders (if fusion_plating_bridge_mrp installed) -----
|
||||
# Show two buckets, in this order:
|
||||
# 1) WOs explicitly assigned to this operator (their named tasks)
|
||||
# 2) WOs with NO assignment (open for any operator to grab)
|
||||
# Skip WOs assigned to OTHER operators — strict per-aerospace
|
||||
# accountability (no one should "borrow" someone else's job).
|
||||
MrpWO = self.env.get('mrp.workorder')
|
||||
if MrpWO is not None:
|
||||
wo_domain = [('state', 'in', ('ready', 'progress'))]
|
||||
base = [('state', 'in', ('ready', 'progress'))]
|
||||
if facility_id:
|
||||
wo_domain.append(('workcenter_id.x_fc_facility_id', '=', facility_id))
|
||||
base.append(('workcenter_id.x_fc_facility_id', '=', facility_id))
|
||||
assignment_filter = (
|
||||
'|',
|
||||
('x_fc_assigned_user_id', '=', user_id),
|
||||
('x_fc_assigned_user_id', '=', False),
|
||||
) if 'x_fc_assigned_user_id' in MrpWO._fields else ()
|
||||
wo_domain = list(assignment_filter) + base
|
||||
work_orders = MrpWO.search(wo_domain, order='sequence, date_start')
|
||||
for wo in work_orders:
|
||||
rows.append({
|
||||
|
||||
Reference in New Issue
Block a user