feat(plating): QC gate + mobile checklist + Fischerscope thickness capture

Phase 1 — Backend QC gate (bridge_mrp)
* fp.qc.checklist.template / .line — per-customer checklist definitions
* fusion.plating.quality.check / .line — per-MO instances walked by inspectors
* res.partner.x_fc_requires_qc + x_fc_qc_template_id toggles policy per customer
* mrp.production.button_mark_done blocks close until QC passes (plus optional
  thickness-readings + thickness-PDF gates on aerospace templates)
* Auto-spawns the QC on MO confirm from the customer's resolved template
* Fischerscope XDAL 600 PDF parser auto-extracts NiP / Ni% / P% readings on upload
* fp.thickness.reading gains quality_check_id + auto_extracted

Phase 2 — Mobile QC checklist (OWL client action)
* fp_qc_checklist registered under registry.category("actions")
* Reuses shopfloor design tokens (_fp_shopfloor_tokens.scss) — 48 px touch
  targets, shadow-based elevation, three-tier contrast, light + dark bundles
* Per-line pass/fail/N/A with numeric value range, mandatory photo, notes
* Fischerscope PDF drop-zone → server-side pdftotext parse
* Sign-off bar with pass / fail / rework actions

Phase 3 — Admin config
* Starter global default + aerospace/Nadcap templates seeded
* Plating → Configuration → QC Checklist Templates (manager-only)
* Plating → Quality → Quality Checks menu
* "Plating Documents" tab on res.partner gains the QC toggle + template picker
* MO form smart button opens the active QC in the mobile checklist

Gap fixes
* Scanner handles FP-QC:<ref> and FP-MO:<name> — launches the checklist
  directly on the tablet
* action_spawn_retry clones a fresh QC from a failed one so rework doesn't
  need a new MO

All 12 models / routes / gates smoke + E2E tested: 24 assertions pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-21 00:15:58 -04:00
parent 4d6095cd2a
commit e86d897bce
21 changed files with 3210 additions and 1 deletions

View File

@@ -143,6 +143,61 @@ class FpShopfloorController(http.Controller):
'product_name': wo.production_id.product_id.display_name or '',
}
# FP-QC:<ref> → directly into the mobile checklist screen
if code.startswith('FP-QC:'):
QC = request.env.get('fusion.plating.quality.check')
if QC is None:
return {'ok': False, 'error': 'QC module not installed'}
qc = QC.search(
[('name', '=', code.split(':', 1)[1])], limit=1,
)
if not qc:
return {'ok': False, 'error': f'QC {code} not found'}
return {
'ok': True,
'model': 'fusion.plating.quality.check',
'id': qc.id,
'name': qc.name,
'state': qc.state,
'production_id': qc.production_id.id or False,
'production_name': qc.production_id.name or '',
'partner_name': qc.partner_id.name or '',
'action_tag': 'fp_qc_checklist',
'action_params': {'check_id': qc.id},
}
# FP-MO:<name> → the MO. If it has a pending QC, surface that.
if code.startswith('FP-MO:'):
MO = request.env.get('mrp.production')
QC = request.env.get('fusion.plating.quality.check')
if MO is None:
return {'ok': False, 'error': 'MRP not installed'}
mo = MO.search(
[('name', '=', code.split(':', 1)[1])], limit=1,
)
if not mo:
return {'ok': False, 'error': f'MO {code} not found'}
resp = {
'ok': True,
'model': 'mrp.production',
'id': mo.id,
'name': mo.name,
'state': mo.state,
'product_name': mo.product_id.display_name or '',
}
if QC is not None:
active = QC.search([
('production_id', '=', mo.id),
('state', 'in', ('draft', 'in_progress')),
], order='create_date desc', limit=1)
if active:
resp['pending_qc_id'] = active.id
resp['pending_qc_name'] = active.name
resp['pending_qc_state'] = active.state
resp['action_tag'] = 'fp_qc_checklist'
resp['action_params'] = {'check_id': active.id}
return resp
return {'ok': False, 'error': f'Unrecognised QR payload: {code}'}
# ----------------------------------------------------------------------

View File

@@ -107,6 +107,20 @@ export class ShopfloorTablet extends Component {
this.state.stationId = result.id;
localStorage.setItem("fp_tablet_station_id", String(result.id));
this.setMessage(`Station paired: ${result.name}`, "success");
} else if (result.action_tag) {
// e.g. FP-QC:<ref> or FP-MO:<name> with a pending QC →
// launch the mobile checklist directly.
this.setMessage(`Launching ${result.pending_qc_name || result.name}`, "info");
this.action.doAction({
type: "ir.actions.client",
tag: result.action_tag,
name: result.pending_qc_name || result.name,
params: result.action_params || {},
target: "current",
});
this.state.scannedCode = "";
this.state.loading = false;
return;
} else {
this.setMessage(`Scanned ${result.model}${result.name || ""}`, "info");
}