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:
gsinghpal
2026-04-19 09:30:56 -04:00
parent 2d64f7efab
commit bbbd222b89
6 changed files with 688 additions and 4 deletions

View File

@@ -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
# ----------------------------------------------------------------------