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:
gsinghpal
2026-06-05 00:16:19 -04:00
parent c9eb61ee0c
commit 8c76a16366
789 changed files with 4692 additions and 4692 deletions

View File

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