chore(plating): de-dash shipped code + intake-neutral customer emails
Replace em-dashes and en-dashes with hyphens across 789 shipped source files (py/xml/js/scss) so the delivered module reads as human-written; em-dashes had become a recognizable AI-generated tell. Internal .md dev notes are excluded. The WO-sticker mojibake strippers keep their dash search targets (now written — / –). No logic changes: comments and display strings only; validated with py_compile + lxml parse. Rewrite the 7 customer notification emails to be intake-neutral (ship-in / drop-off / pickup) and repair-aware, and fix the Shipped email documents line (packing slip vs bill of lading; certificate only when issued). Subjects use a hyphen separator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Comprehensive E2E simulator — workforce edition.
|
||||
"""Comprehensive E2E simulator - workforce edition.
|
||||
|
||||
Role-plays each employee touching a job from quote → invoice. For
|
||||
each work order:
|
||||
@@ -17,7 +17,7 @@ Then audits:
|
||||
• Notification log with attachment names
|
||||
• Portal job final state + SO workflow_stage
|
||||
|
||||
Findings printed at the end as PASS/FAIL/WARN — each FAIL/WARN is a
|
||||
Findings printed at the end as PASS/FAIL/WARN - each FAIL/WARN is a
|
||||
gap that needs fixing before this can ship to a real shop floor.
|
||||
"""
|
||||
from datetime import datetime
|
||||
@@ -53,7 +53,7 @@ def finding(level, area, msg):
|
||||
stamp = datetime.now().strftime('%y%m%d-%H%M%S')
|
||||
|
||||
# =====================================================================
|
||||
banner(f'PHASE 0 — Set up cast of employees ({stamp})')
|
||||
banner(f'PHASE 0 - Set up cast of employees ({stamp})')
|
||||
# =====================================================================
|
||||
|
||||
# Reuse existing users when present so we don't bloat the DB on reruns.
|
||||
@@ -99,10 +99,10 @@ for key, (name, desc) in PERSONAS.items():
|
||||
'name': name,
|
||||
'user_id': u.id,
|
||||
})
|
||||
show(f'{key:<8}', f'{u.name} ({desc}) — uid={u.id}, emp={emp.id}')
|
||||
show(f'{key:<8}', f'{u.name} ({desc}) - uid={u.id}, emp={emp.id}')
|
||||
|
||||
# =====================================================================
|
||||
banner('PHASE 1 — Sandra builds a quote (estimator)')
|
||||
banner('PHASE 1 - Sandra builds a quote (estimator)')
|
||||
# =====================================================================
|
||||
|
||||
customer = env['res.partner'].sudo().create({
|
||||
@@ -160,10 +160,10 @@ finding('PASS' if so.client_order_ref == po_number else 'FAIL',
|
||||
'quote→SO PO#', f'client_order_ref="{so.client_order_ref}"')
|
||||
|
||||
# =====================================================================
|
||||
banner('PHASE 2 — Customer accepts → SO confirm → auto-MO + portal job')
|
||||
banner('PHASE 2 - Customer accepts → SO confirm → auto-MO + portal job')
|
||||
# =====================================================================
|
||||
|
||||
step('CUSTOMER', 'Accepts quote — Sandra confirms SO')
|
||||
step('CUSTOMER', 'Accepts quote - Sandra confirms SO')
|
||||
so.with_user(users['sandra']).sudo().action_confirm()
|
||||
finding('PASS' if so.state == 'sale' else 'FAIL', 'SO confirm', f'state={so.state}')
|
||||
|
||||
@@ -178,10 +178,10 @@ job = mo.x_fc_portal_job_id if mo else False
|
||||
finding('PASS' if job else 'FAIL', 'portal job', job.name if job else 'MISSING')
|
||||
|
||||
# =====================================================================
|
||||
banner('PHASE 3 — Carlos receives parts')
|
||||
banner('PHASE 3 - Carlos receives parts')
|
||||
# =====================================================================
|
||||
|
||||
step('CARLOS', 'Logs receiving — 40 housings in 2 boxes from FedEx')
|
||||
step('CARLOS', 'Logs receiving - 40 housings in 2 boxes from FedEx')
|
||||
recv = env['fp.receiving'].with_user(users['carlos']).sudo().create({
|
||||
'partner_id': customer.id,
|
||||
'sale_order_id': so.id,
|
||||
@@ -205,7 +205,7 @@ finding('PASS' if recv.state == 'accepted' else 'FAIL',
|
||||
'receiving accept', f'state={recv.state}')
|
||||
|
||||
# =====================================================================
|
||||
banner('PHASE 4 — Hannah plans the job')
|
||||
banner('PHASE 4 - Hannah plans the job')
|
||||
# =====================================================================
|
||||
|
||||
step('HANNAH', 'Assigns recipe + generates work orders')
|
||||
@@ -275,8 +275,8 @@ if Cert is not None and test_bath and test_bath.process_type_id:
|
||||
'notes': 'Auto-issued for E2E workforce simulation',
|
||||
})
|
||||
show(' certifications', f'pre-issued for {pt.name} → 5 operators')
|
||||
show(' test bath', f'{test_bath.name}' if test_bath else '(none — wet-WO assignment will fail)')
|
||||
show(' test tank', f'{test_tank.name}' if test_tank else '(none — wet-WO assignment will fail)')
|
||||
show(' test bath', f'{test_bath.name}' if test_bath else '(none - wet-WO assignment will fail)')
|
||||
show(' test tank', f'{test_tank.name}' if test_tank else '(none - wet-WO assignment will fail)')
|
||||
|
||||
assignments = []
|
||||
wet_assignments = []
|
||||
@@ -302,18 +302,18 @@ for wo in mo.workorder_ids:
|
||||
'x_fc_tank_id': test_tank.id,
|
||||
})
|
||||
wet_assignments.append(wo)
|
||||
extras = f' [WET — bath={test_bath.name}, tank={test_tank.name}]'
|
||||
extras = f' [WET - bath={test_bath.name}, tank={test_tank.name}]'
|
||||
elif kind == 'bake' and test_oven:
|
||||
wo.sudo().x_fc_oven_id = test_oven.id
|
||||
extras = f' [BAKE — oven={test_oven.name}]'
|
||||
extras = f' [BAKE - oven={test_oven.name}]'
|
||||
elif kind == 'rack':
|
||||
rack = env['fusion.plating.rack'].search([], limit=1)
|
||||
if rack:
|
||||
wo.sudo().x_fc_rack_id = rack.id
|
||||
extras = f' [RACK — fixture={rack.name}]'
|
||||
extras = f' [RACK - fixture={rack.name}]'
|
||||
elif kind == 'mask':
|
||||
wo.sudo().x_fc_masking_material = 'tape'
|
||||
extras = ' [MASK — material=tape]'
|
||||
extras = ' [MASK - material=tape]'
|
||||
|
||||
assignments.append((wo, op_user, operator_key))
|
||||
show(f' WO {wo.id}', f'"{wo.name}" → {op_user.name}{extras}')
|
||||
@@ -328,10 +328,10 @@ finding('PASS' if (not wet_assignments) or (wet_with_bath == len(wet_assignments
|
||||
f'{wet_with_bath}/{len(wet_assignments)} wet WOs have both bath + tank')
|
||||
|
||||
# ===== Negative tests: validation MUST block bad starts =====
|
||||
banner('PHASE 4b — Negative tests: validation gates fire correctly')
|
||||
banner('PHASE 4b - Negative tests: validation gates fire correctly')
|
||||
|
||||
# Test 1: try to start a WO with operator stripped → expect UserError
|
||||
step('SYSTEM', 'Test 1 — un-assigning operator and trying to start')
|
||||
step('SYSTEM', 'Test 1 - un-assigning operator and trying to start')
|
||||
test_wo = mo.workorder_ids[0]
|
||||
saved_op = test_wo.x_fc_assigned_user_id.id
|
||||
test_wo.sudo().x_fc_assigned_user_id = False
|
||||
@@ -343,12 +343,12 @@ except Exception as e:
|
||||
show(' blocked with', str(e).splitlines()[0][:120])
|
||||
finding('PASS' if gate_fired else 'FAIL',
|
||||
'gate: missing operator',
|
||||
'blocked' if gate_fired else 'NOT blocked — validation broken')
|
||||
'blocked' if gate_fired else 'NOT blocked - validation broken')
|
||||
test_wo.sudo().x_fc_assigned_user_id = saved_op
|
||||
|
||||
# Test 2: try to start a WET WO without bath/tank → expect UserError
|
||||
if wet_assignments:
|
||||
step('SYSTEM', 'Test 2 — wet WO with bath/tank stripped')
|
||||
step('SYSTEM', 'Test 2 - wet WO with bath/tank stripped')
|
||||
wet_wo = wet_assignments[0]
|
||||
saved_bath = wet_wo.x_fc_bath_id.id
|
||||
saved_tank = wet_wo.x_fc_tank_id.id
|
||||
@@ -362,7 +362,7 @@ if wet_assignments:
|
||||
show(' blocked with', msg.splitlines()[0][:120])
|
||||
finding('PASS' if gate_fired else 'FAIL',
|
||||
'gate: missing bath/tank on wet WO',
|
||||
'blocked' if gate_fired else 'NOT blocked — validation broken')
|
||||
'blocked' if gate_fired else 'NOT blocked - validation broken')
|
||||
wet_wo.sudo().write({
|
||||
'x_fc_bath_id': saved_bath,
|
||||
'x_fc_tank_id': saved_tank,
|
||||
@@ -370,7 +370,7 @@ if wet_assignments:
|
||||
|
||||
# ===== Negative tests for the 6 new gates (wrapped in savepoints
|
||||
# so an SQL-level constraint failure doesn't abort the txn) =====
|
||||
banner('PHASE 4c — Negative tests for the new compliance gates')
|
||||
banner('PHASE 4c - Negative tests for the new compliance gates')
|
||||
|
||||
|
||||
def neg_test(label, fn, expect_keywords):
|
||||
@@ -396,7 +396,7 @@ def neg_test(label, fn, expect_keywords):
|
||||
|
||||
|
||||
# Test 3: MO confirm without facility → expect block
|
||||
step('SYSTEM', 'Test 3 — MO confirm with no facility → blocked')
|
||||
step('SYSTEM', 'Test 3 - MO confirm with no facility → blocked')
|
||||
|
||||
|
||||
def t_mo_facility():
|
||||
@@ -410,7 +410,7 @@ def t_mo_facility():
|
||||
'product_qty': 1,
|
||||
'company_id': env.company.id,
|
||||
})
|
||||
m.action_confirm() # should raise — no facility resolvable
|
||||
m.action_confirm() # should raise - no facility resolvable
|
||||
finally:
|
||||
fac0.sudo().write({'active': True})
|
||||
env.company.sudo().x_fc_default_facility_id = saved_default
|
||||
@@ -420,7 +420,7 @@ neg_test('MO confirm without facility', t_mo_facility,
|
||||
['facility'])
|
||||
|
||||
# Test 4: Cert issue without spec_reference
|
||||
step('SYSTEM', 'Test 4 — Cert action_issue() without spec_reference → blocked')
|
||||
step('SYSTEM', 'Test 4 - Cert action_issue() without spec_reference → blocked')
|
||||
|
||||
|
||||
def t_cert_spec():
|
||||
@@ -437,7 +437,7 @@ neg_test('cert issue without spec_reference', t_cert_spec,
|
||||
['Spec', 'spec_reference'])
|
||||
|
||||
# Test 5: Delivery mark_delivered without POD
|
||||
step('SYSTEM', 'Test 5 — Delivery mark_delivered() with no POD → blocked')
|
||||
step('SYSTEM', 'Test 5 - Delivery mark_delivered() with no POD → blocked')
|
||||
|
||||
|
||||
def t_dlv_pod():
|
||||
@@ -453,7 +453,7 @@ neg_test('delivery delivered without POD', t_dlv_pod,
|
||||
['POD', 'Proof of Delivery'])
|
||||
|
||||
# Test 6: Invoice post without payment terms
|
||||
step('SYSTEM', 'Test 6 — Invoice post() with no payment terms → blocked')
|
||||
step('SYSTEM', 'Test 6 - Invoice post() with no payment terms → blocked')
|
||||
|
||||
|
||||
def t_inv_terms():
|
||||
@@ -480,7 +480,7 @@ neg_test('invoice post without payment terms', t_inv_terms,
|
||||
['payment term'])
|
||||
|
||||
# Test 7: Thickness reading without calibration_std_ref
|
||||
step('SYSTEM', 'Test 7 — Thickness reading without calibration_std_ref → blocked')
|
||||
step('SYSTEM', 'Test 7 - Thickness reading without calibration_std_ref → blocked')
|
||||
|
||||
|
||||
def t_thickness_cal():
|
||||
@@ -496,7 +496,7 @@ neg_test('thickness reading without cal std', t_thickness_cal,
|
||||
['calibration', 'required', 'not-null', 'null value'])
|
||||
|
||||
# Test 8: NCR close without root cause / containment / disposition
|
||||
step('SYSTEM', 'Test 8 — NCR close() with missing fields → blocked')
|
||||
step('SYSTEM', 'Test 8 - NCR close() with missing fields → blocked')
|
||||
|
||||
|
||||
def t_ncr_close():
|
||||
@@ -515,7 +515,7 @@ neg_test('NCR close without RC/containment/disposition', t_ncr_close,
|
||||
['Root Cause', 'Containment', 'Disposition'])
|
||||
|
||||
# Test 9: CAPA close without root cause analysis / action plan / verification
|
||||
step('SYSTEM', 'Test 9 — CAPA close() with missing fields → blocked')
|
||||
step('SYSTEM', 'Test 9 - CAPA close() with missing fields → blocked')
|
||||
|
||||
|
||||
def t_capa_close():
|
||||
@@ -531,7 +531,7 @@ neg_test('CAPA close without analysis/plan/verification', t_capa_close,
|
||||
['Root Cause Analysis', 'Action Plan', 'Verification'])
|
||||
|
||||
# Test 10: Discharge sample close without lab evidence
|
||||
step('SYSTEM', 'Test 10 — Discharge sample close() with no lab evidence → blocked')
|
||||
step('SYSTEM', 'Test 10 - Discharge sample close() with no lab evidence → blocked')
|
||||
|
||||
|
||||
def t_discharge_close():
|
||||
@@ -546,7 +546,7 @@ neg_test('discharge sample close without lab evidence', t_discharge_close,
|
||||
['Lab Report', 'Results Received', 'parameter', 'Lab certificate'])
|
||||
|
||||
# Test 11: Invoice ref auto-fill from SO at create time
|
||||
step('SYSTEM', 'Test 11 — Invoice ref auto-fills from SO.client_order_ref')
|
||||
step('SYSTEM', 'Test 11 - Invoice ref auto-fills from SO.client_order_ref')
|
||||
test_inv2 = env['account.move'].sudo().create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': customer.id,
|
||||
@@ -562,7 +562,7 @@ finding('PASS' if test_inv2.ref == so.client_order_ref else 'FAIL',
|
||||
test_inv2.sudo().unlink()
|
||||
|
||||
# =====================================================================
|
||||
banner('PHASE 5 — Operators run their work orders (REAL-TIME timers)')
|
||||
banner('PHASE 5 - Operators run their work orders (REAL-TIME timers)')
|
||||
# =====================================================================
|
||||
|
||||
# Pick a bath for the plating step so chemistry logging has somewhere
|
||||
@@ -592,7 +592,7 @@ WO_DURATIONS_BEFORE = {wo.id: wo.duration for wo in mo.workorder_ids}
|
||||
|
||||
for wo, op_user, op_key in assignments:
|
||||
actor = PERSONAS[op_key][0].split()[0].upper()
|
||||
step(actor, f'Picks up "{wo.name}" on iPad — taps START')
|
||||
step(actor, f'Picks up "{wo.name}" on iPad - taps START')
|
||||
wo_op = wo.with_user(op_user).sudo()
|
||||
started_state = wo_op.state
|
||||
try:
|
||||
@@ -603,7 +603,7 @@ for wo, op_user, op_key in assignments:
|
||||
continue
|
||||
show(f' state', f'{started_state} → {wo_op.state}')
|
||||
|
||||
# Real-time work — sleep 2s for non-plating, 4s for plating
|
||||
# Real-time work - sleep 2s for non-plating, 4s for plating
|
||||
work_seconds = 4 if 'plating' in (wo.name or '').lower() else 2
|
||||
show(f' working...', f'{work_seconds}s elapsed')
|
||||
time.sleep(work_seconds)
|
||||
@@ -624,7 +624,7 @@ for wo, op_user, op_key in assignments:
|
||||
})
|
||||
show(' chemistry log', f'{log.id} ({len(log.line_ids)} readings)')
|
||||
else:
|
||||
finding('WARN', 'chemistry', 'no fusion.plating.bath.parameter records — log skipped')
|
||||
finding('WARN', 'chemistry', 'no fusion.plating.bath.parameter records - log skipped')
|
||||
|
||||
# Frank logs Fischerscope thickness readings during inspection
|
||||
if 'inspect' in (wo.name or '').lower() and op_key == 'frank':
|
||||
@@ -687,14 +687,14 @@ finding('PASS' if distinct_operators_logged > 1 else 'WARN',
|
||||
f'{distinct_operators_logged} distinct operators recorded')
|
||||
|
||||
# =====================================================================
|
||||
banner('PHASE 6 — Hannah closes the MO')
|
||||
banner('PHASE 6 - Hannah closes the MO')
|
||||
# =====================================================================
|
||||
|
||||
step('HANNAH', 'Marks MO done')
|
||||
try:
|
||||
mo_h.button_mark_done()
|
||||
except Exception as e:
|
||||
print(f' [info] mark_done: {e} — falling back')
|
||||
print(f' [info] mark_done: {e} - falling back')
|
||||
try:
|
||||
mo_h.qty_producing = mo.product_qty
|
||||
mo_h._action_done()
|
||||
@@ -703,7 +703,7 @@ except Exception as e:
|
||||
finding('PASS' if mo.state == 'done' else 'FAIL', 'MO done', f'state={mo.state}')
|
||||
|
||||
# =====================================================================
|
||||
banner('PHASE 7 — Frank inspects + CoC')
|
||||
banner('PHASE 7 - Frank inspects + CoC')
|
||||
# =====================================================================
|
||||
|
||||
certs = env['fp.certificate'].search([('production_id', '=', mo.id)])
|
||||
@@ -727,7 +727,7 @@ if coc:
|
||||
step('FRANK', 'Reviews + signs CoC (already auto-issued)')
|
||||
|
||||
# =====================================================================
|
||||
banner('PHASE 8 — Dave drives the delivery')
|
||||
banner('PHASE 8 - Dave drives the delivery')
|
||||
# =====================================================================
|
||||
|
||||
dlv = env['fusion.plating.delivery'].search(
|
||||
@@ -749,13 +749,13 @@ if dlv:
|
||||
if dlv.state == 'scheduled': dlv.with_user(users['dave']).sudo().action_start_route()
|
||||
# POD must be captured BEFORE marking delivered (new gate)
|
||||
if dlv.state == 'en_route' and not dlv.pod_id:
|
||||
step('DAVE', 'Captures POD on iPad — recipient signs + photo')
|
||||
step('DAVE', 'Captures POD on iPad - recipient signs + photo')
|
||||
POD = env['fusion.plating.proof.of.delivery']
|
||||
pod = POD.with_user(users['dave']).sudo().create({
|
||||
'delivery_id': dlv.id,
|
||||
'partner_id': dlv.partner_id.id,
|
||||
'recipient_name': 'Dock Receiver',
|
||||
'notes': 'E2E sim — recipient on dock signed for parts',
|
||||
'notes': 'E2E sim - recipient on dock signed for parts',
|
||||
})
|
||||
dlv.sudo().pod_id = pod.id
|
||||
show(' POD captured', f'{pod.name} (id={pod.id})')
|
||||
@@ -775,7 +775,7 @@ if dlv:
|
||||
'chain of custody', f'{len(coc_logs)} entries')
|
||||
|
||||
# =====================================================================
|
||||
banner('PHASE 9 — Linda creates + posts invoice')
|
||||
banner('PHASE 9 - Linda creates + posts invoice')
|
||||
# =====================================================================
|
||||
|
||||
step('LINDA', 'Creates invoice from SO')
|
||||
@@ -797,7 +797,7 @@ if inv:
|
||||
'invoice posted', f'state={inv.state}, payment_state={inv.payment_state}')
|
||||
|
||||
# =====================================================================
|
||||
banner('PHASE 10 — Compliance + notification audit')
|
||||
banner('PHASE 10 - Compliance + notification audit')
|
||||
# =====================================================================
|
||||
|
||||
# Notification log
|
||||
@@ -858,7 +858,7 @@ if BakeWin is not None and job:
|
||||
# Each operator can see their OWN assigned WOs via the tablet
|
||||
# (queue is a TransientModel; tablet calls build_for_user on load)
|
||||
# Reset MO to make some WOs ready/progress for queue test BEFORE this is run
|
||||
# would be needed — but the queue should still work for any in-progress WOs
|
||||
# would be needed - but the queue should still work for any in-progress WOs
|
||||
# elsewhere in the system that match the user.
|
||||
OpQueue = env.get('fusion.plating.operator.queue')
|
||||
if OpQueue is not None:
|
||||
|
||||
Reference in New Issue
Block a user