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,4 +1,4 @@
# Battle test real shop failure modes.
# Battle test - real shop failure modes.
#
# This is the "what if my operator is sloppy / forgetful / lazy" suite.
# We document what the system does TODAY, then identify what's missing.
@@ -41,7 +41,7 @@ def make_job(po_suffix):
# ====================================================================== 1
print('='*72)
print('SCENARIO 1 Carlos forgot to click Start. Realizes 2 hours later.')
print('SCENARIO 1 - Carlos forgot to click Start. Realizes 2 hours later.')
print('='*72)
job = make_job('S1-' + fields.Datetime.now().strftime('%H%M%S'))
step = job.step_ids.sorted('sequence')[0]
@@ -58,7 +58,7 @@ print(f' GAP: No "Adjust Time" affordance for forgetful operators.')
# ====================================================================== 2
print()
print('='*72)
print('SCENARIO 2 Carlos finished step physically. Forgot Finish. Went home.')
print('SCENARIO 2 - Carlos finished step physically. Forgot Finish. Went home.')
print('='*72)
job = make_job('S2-' + fields.Datetime.now().strftime('%H%M%S'))
step = job.step_ids.sorted('sequence')[0]
@@ -77,7 +77,7 @@ print(f' GAP: No way to retroactively correct the timelog interval.')
# ====================================================================== 3
print()
print('='*72)
print('SCENARIO 3 Two operators tap Start on the same step.')
print('SCENARIO 3 - Two operators tap Start on the same step.')
print('='*72)
job = make_job('S3-' + fields.Datetime.now().strftime('%H%M%S'))
step = job.step_ids.sorted('sequence')[0]
@@ -97,7 +97,7 @@ except Exception as e:
# ====================================================================== 4
print()
print('='*72)
print('SCENARIO 4 Operator finishes step #6 before #5 is started.')
print('SCENARIO 4 - Operator finishes step #6 before #5 is started.')
print('='*72)
job = make_job('S4-' + fields.Datetime.now().strftime('%H%M%S'))
steps = job.step_ids.sorted('sequence')
@@ -119,7 +119,7 @@ print(f' But there\'s no "force serial" flag for steps that MUST be in order.')
# ====================================================================== 5
print()
print('='*72)
print('SCENARIO 5 Job stuck mid-process. Manager wants to take over.')
print('SCENARIO 5 - Job stuck mid-process. Manager wants to take over.')
print('='*72)
job = make_job('S5-' + fields.Datetime.now().strftime('%H%M%S'))
step = job.step_ids.sorted('sequence')[0]
@@ -141,7 +141,7 @@ print(f' Bob finishes: state={step.state}, finished_by={step.finished_by_user_i
# ====================================================================== 6
print()
print('='*72)
print('SCENARIO 6 Bake window expired (operator at lunch). Override?')
print('SCENARIO 6 - Bake window expired (operator at lunch). Override?')
print('='*72)
BW = env['fusion.plating.bake.window']
Bath = env['fusion.plating.bath']
@@ -169,7 +169,7 @@ except Exception as e:
# ====================================================================== 7
print()
print('='*72)
print('SCENARIO 7 Operator clocks 6 hours on a step expected to take 30 min.')
print('SCENARIO 7 - Operator clocks 6 hours on a step expected to take 30 min.')
print('='*72)
job = make_job('S7-' + fields.Datetime.now().strftime('%H%M%S'))
step = job.step_ids.sorted('sequence')[0]
@@ -186,7 +186,7 @@ print(f' GAP: System silently accepted 12x overrun. No alert, no chatter post.'
# ====================================================================== 8
print()
print('='*72)
print('SCENARIO 8 Operator did 4 of 5 parts. 1 contaminated. Qty drift.')
print('SCENARIO 8 - Operator did 4 of 5 parts. 1 contaminated. Qty drift.')
print('='*72)
job = make_job('S8-' + fields.Datetime.now().strftime('%H%M%S'))
print(f' Job qty={job.qty}, qty_done={job.qty_done}, qty_scrapped={job.qty_scrapped}')
@@ -196,7 +196,7 @@ for s in job.step_ids.sorted('sequence'):
s.button_start()
if s.state == 'in_progress':
s.button_finish()
# Try to mark done qty_done is still 0
# Try to mark done - qty_done is still 0
try:
job.button_mark_done()
print(f' Job done: qty_done={job.qty_done}, qty_scrapped={job.qty_scrapped}')

View File

@@ -1,4 +1,4 @@
# Battle test v2 re-verify after fixes for: bake-window override,
# Battle test v2 - re-verify after fixes for: bake-window override,
# duration overrun chatter, qty reconciliation, recompute-duration.
from datetime import timedelta
@@ -32,7 +32,7 @@ def make_job(po_suffix):
# ====================================================================== Fix 1
print('='*72)
print('FIX 1 Bake-window: missed_window blocks, manager override allowed + audited')
print('FIX 1 - Bake-window: missed_window blocks, manager override allowed + audited')
print('='*72)
BW = env['fusion.plating.bake.window']
Bath = env['fusion.plating.bath']
@@ -47,7 +47,7 @@ BW._cron_update_states()
expired.invalidate_recordset()
print(f' Window {expired.name} state: {expired.state}')
# Naive operator (no override) should fail
# Naive operator (no override) - should fail
try:
expired.action_start_bake()
print(f' ❌ start_bake worked without override')
@@ -67,7 +67,7 @@ except Exception as e:
# ====================================================================== Fix 2
print()
print('='*72)
print('FIX 2 Duration overrun: > 1.5x expected posts chatter warning')
print('FIX 2 - Duration overrun: > 1.5x expected posts chatter warning')
print('='*72)
job = make_job('F2-' + fields.Datetime.now().strftime('%H%M%S'))
step = job.step_ids.sorted('sequence')[0]
@@ -90,7 +90,7 @@ if overrun_msgs:
# ====================================================================== Fix 3
print()
print('='*72)
print('FIX 3 Qty reconciliation: job mark-done blocks if qty mismatch')
print('FIX 3 - Qty reconciliation: job mark-done blocks if qty mismatch')
print('='*72)
job = make_job('F3-' + fields.Datetime.now().strftime('%H%M%S'))
for s in job.step_ids.sorted('sequence'):
@@ -120,7 +120,7 @@ except Exception as e:
# ====================================================================== Fix 4
print()
print('='*72)
print('FIX 4 Supervisor edits timelog → Recompute Duration action picks it up')
print('FIX 4 - Supervisor edits timelog → Recompute Duration action picks it up')
print('='*72)
job = make_job('F4-' + fields.Datetime.now().strftime('%H%M%S'))
step = job.step_ids.sorted('sequence')[0]

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""Battle test post-shop state machine (awaiting_cert + awaiting_ship).
"""Battle test - post-shop state machine (awaiting_cert + awaiting_ship).
Spec: docs/superpowers/specs/2026-05-25-post-shop-cert-shipping-job-states-design.md
Plan: docs/superpowers/plans/2026-05-25-post-shop-cert-shipping-job-states-plan.md
@@ -16,7 +16,7 @@ The script rolls back at the end so it leaves the DB clean.
2. Walk every step to terminal → assert state='awaiting_cert' (or
'awaiting_ship' if no cert required)
3. Assert card appears in plant_kanban under 'inspection' / 'shipping'
4. Assert activity scheduled on a QM (notification fire is async
4. Assert activity scheduled on a QM (notification fire is async -
skip strict email assertion in this test)
5. As a Technician, call cert.action_issue() → assert AccessError
6. As a QM, call cert.action_issue() → state='issued', job state→'awaiting_ship'
@@ -51,7 +51,7 @@ _assert(bool(partner), 'partner exists')
product = env['product.product'].search([], limit=1)
_assert(bool(product), 'product exists')
# Role lookups (transitive via all_group_ids Owners reach QM via implication)
# Role lookups (transitive via all_group_ids - Owners reach QM via implication)
qm_gid = env.ref('fusion_plating.group_fp_quality_manager').id
mgr_gid = env.ref('fusion_plating.group_fp_manager').id
tech_gid = env.ref('fusion_plating.group_fp_technician').id
@@ -88,7 +88,7 @@ step = env['fp.job.step'].create({
_assert(job.state == 'in_progress', 'job created in_progress')
# ---- 2. Finish the step → auto-advance -----------------------------
# Bypass other gates that aren't relevant here (qty, bake, qc not
# Bypass other gates that aren't relevant here (qty, bake, qc - not
# the system under test).
ctx = {
'fp_skip_required_inputs_gate': True,
@@ -135,11 +135,11 @@ if cert_required_path:
except AccessError:
print('OK - Technician issue raised AccessError')
except UserError as e:
# Tech might hit a UserError gate before the ACL check fires
# Tech might hit a UserError gate before the ACL check fires -
# accept that as "tech blocked" too.
print(f'OK - Technician blocked: UserError: {str(e)[:80]}')
# QM issues first pre-fill the gates so action_issue can proceed
# QM issues - first pre-fill the gates so action_issue can proceed
if not cert.spec_reference:
cert.spec_reference = 'TEST-SPEC'
if not cert.process_description:
@@ -201,7 +201,7 @@ if cert_required_path and job.state == 'awaiting_cert':
try:
new_cert.with_user(qm).action_issue()
job.invalidate_recordset()
# Need ALL required certs issued for the advance there may be
# Need ALL required certs issued for the advance - there may be
# a remaining voided cert from step 8 that's still in draft/etc.
# Just check that state has moved off awaiting_cert.
print(f'OK - re-issue path: job state now {job.state}')
@@ -218,6 +218,6 @@ if job.state == 'awaiting_ship':
print()
print('--- bt_post_shop_states: ALL PASS ---')
# Leave DB clean rollback the test data.
# Leave DB clean - rollback the test data.
env.cr.rollback()
print('rolled back test data')

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""Quality Dashboard redesign entech smoke.
"""Quality Dashboard redesign - entech smoke.
Spec: docs/superpowers/specs/2026-05-25-quality-dashboard-redesign-design.md
Plan: docs/superpowers/plans/2026-05-25-quality-dashboard-redesign-plan.md

View File

@@ -1,9 +1,9 @@
# Scenario 10 Carlos paused for lunch. Got pulled to another job. Step
# Scenario 10 - Carlos paused for lunch. Got pulled to another job. Step
# is now sitting in 'paused' state for 3 days. No alert. Costing is wrong
# (the open timelog row was already closed at pause, but the step shows
# zero progress).
#
# Real shop pattern: this happens daily interruptions, shift change,
# Real shop pattern: this happens daily - interruptions, shift change,
# operator pulled to rush job.
#
# What we want:
@@ -42,7 +42,7 @@ step.button_start()
step.button_pause()
print(f'[Carlos] Started + paused step "{step.name}" (state={step.state})')
# Simulate 3 days passing backdate the pause by setting date_started
# Simulate 3 days passing - backdate the pause by setting date_started
step.date_started = fields.Datetime.now() - timedelta(days=3)
print(f' Pretending it has been paused 3 days')

View File

@@ -1,6 +1,6 @@
# Scenario 11 Carlos plating step #4 in tank 3. 8 minutes in, the
# Scenario 11 - Carlos plating step #4 in tank 3. 8 minutes in, the
# rectifier dies. Parts come out half-plated. Carlos needs to:
# 1. Abort the current step (parts not finished but partial work
# 1. Abort the current step (parts not finished - but partial work
# already happened)
# 2. Switch to backup tank 5
# 3. Restart the step there
@@ -14,7 +14,7 @@
#
# Operator's options today:
# A) button_cancel → state=cancelled, but then step shows as cancelled
# and won't be replayed. Not what we want we WANT a retry.
# and won't be replayed. Not what we want - we WANT a retry.
# B) button_finish + open NCR manually + create a new step manually?
# Way too much paperwork.
# C) button_pause + change tank_id + button_start → preserves history
@@ -88,7 +88,7 @@ print()
# Today's options:
print(f' Today\'s options the operator has:')
print(f' A) button_cancel → step becomes cancelled (job stuck no replay)')
print(f' A) button_cancel → step becomes cancelled (job stuck - no replay)')
print(f' B) button_pause + write tank_id + button_start (no failure record)')
print(f' C) ???')
print()

View File

@@ -1,5 +1,5 @@
# Scenario 12 Sarah enters SO qty=5. Job spawns with qty=5. Carlos
# starts step 1. Customer calls they want 8 instead of 5. Sarah edits
# Scenario 12 - Sarah enters SO qty=5. Job spawns with qty=5. Carlos
# starts step 1. Customer calls - they want 8 instead of 5. Sarah edits
# the SO line from 5 to 8.
#
# Question: does the job pick up the change?
@@ -42,7 +42,7 @@ step.button_start()
print(f' Carlos started step "{step.name}" (state={step.state})')
print()
# Customer calls wants 8 not 5
# Customer calls - wants 8 not 5
print(f' 📞 Customer: "Make it 8 instead of 5"')
print(f' Sarah edits SO line qty from 5 to 8...')
try:
@@ -63,7 +63,7 @@ if job.qty != sol.product_uom_qty:
else:
print(f' ✓ Job qty auto-updated.')
# Try the reverse what if Sarah tries to LOWER the qty?
# Try the reverse - what if Sarah tries to LOWER the qty?
print()
print(f' Customer changes mind: now wants 3 instead of 8')
try:

View File

@@ -37,7 +37,7 @@ warn = job.message_ids.filtered(lambda m: 'qty changed mid-job' in (m.body or ''
print(f' Warning messages on job: {len(warn)}')
if warn:
print(f' ✓ Chatter warning posted')
print(f' Job.qty still: {job.qty} (unchanged supervisor must explicitly sync)')
print(f' Job.qty still: {job.qty} (unchanged - supervisor must explicitly sync)')
print()
print(f' Bob clicks "Sync qty from SO" on the job')

View File

@@ -32,9 +32,9 @@ if not plating:
plating = job.step_ids.sorted('sequence')[3:4]
plating.instructions = (
'<p><b>Plating bath checklist:</b></p><ul>'
'<li>Verify nickel concentration is 4.05.5 g/L (Fischerscope reading)</li>'
'<li>pH must be 4.44.8 adjust with ammonium hydroxide if needed</li>'
'<li>Bath temp 8893°C, agitation ON</li>'
'<li>Verify nickel concentration is 4.0-5.5 g/L (Fischerscope reading)</li>'
'<li>pH must be 4.4-4.8 - adjust with ammonium hydroxide if needed</li>'
'<li>Bath temp 88-93°C, agitation ON</li>'
'<li>Dwell 45 minutes for 25 µm coating; longer for thicker</li>'
'<li>Rinse for 60s before next station</li></ul>'
)
@@ -57,7 +57,7 @@ from odoo.http import request as _req
# Find code prefix used
print(f' Step code: {plating.id}, name: {plating.name}')
# Direct call to the scan response builder (no http) easier approach:
# Direct call to the scan response builder (no http) - easier approach:
# The scan endpoint builds the dict inline. Verify by replicating its code path.
step = plating
payload = {

View File

@@ -1,10 +1,10 @@
# Scenario 14 Recipe author wants step "Plating" to be hard-blocked
# Scenario 14 - Recipe author wants step "Plating" to be hard-blocked
# until step "Acid Etch" finishes. (Real reason: passivation layer
# starts forming on bare metal in seconds; if Plating starts before
# acid etch is done, adhesion fails.)
#
# Today the system allows ANY step to start any time. Out-of-order is
# allowed for parallel work but for SERIAL-MUST steps, there's no
# allowed for parallel work - but for SERIAL-MUST steps, there's no
# enforcement. We need an opt-in flag the recipe author can set per
# step: requires_predecessor_done.

View File

@@ -1,7 +1,7 @@
# Scenario 15 Job has a coating that requires hydrogen embrittlement
# Scenario 15 - Job has a coating that requires hydrogen embrittlement
# bake. Operator finishes plating step → bake.window auto-spawns
# (state=awaiting_bake). Operator finishes the rest of the steps and
# clicks Mark Done on the job but never started the bake.
# clicks Mark Done on the job - but never started the bake.
#
# Today: job closes done. Customer ships parts. Field failure 3 weeks
# later. AS9100 auditor: "Show me the bake record for lot X." There's
@@ -44,7 +44,7 @@ so = env['sale.order'].browse(r['res_id'])
so.action_confirm()
job = env['fp.job'].search([('sale_order_id', '=', so.id)], limit=1)
# Walk all steps to done the plating step will spawn a bake.window
# Walk all steps to done - the plating step will spawn a bake.window
for s in job.step_ids.sorted('sequence'):
if s.state in ('pending', 'ready'):
s.button_start()
@@ -61,11 +61,11 @@ for bw in bws:
print(f' {bw.name}: state={bw.state}, required_by={bw.bake_required_by}')
print()
print(f' [Operator careless] Clicks Mark Done WITHOUT starting bake')
print(f' [Operator - careless] Clicks Mark Done WITHOUT starting bake')
try:
job.button_mark_done()
print(f' ❌ Job closed with bake awaiting! state={job.state}')
print(f' COMPLIANCE BOMB no bake record but parts ship.')
print(f' COMPLIANCE BOMB - no bake record but parts ship.')
except Exception as e:
print(f' ✓ Blocked: {str(e)[:200]}')

View File

@@ -1,4 +1,4 @@
# Scenario 16 Carlos clicked Start on a step. Got pulled to a rush
# Scenario 16 - Carlos clicked Start on a step. Got pulled to a rush
# job. Forgot to come back. The original step is still in_progress 8
# hours later. The open timelog row is accumulating phantom time. Cost
# rollup is wrong. Manager has no nudge.

View File

@@ -1,6 +1,6 @@
# Scenario 17 Mid-job Carlos drops 2 parts (out of 5). Sets
# Scenario 17 - Mid-job Carlos drops 2 parts (out of 5). Sets
# qty_scrapped from 0 → 2. With my qty-reconciliation gate, he MUST
# update this for the job to close but there's no NCR / hold record
# update this for the job to close - but there's no NCR / hold record
# explaining WHY 2 parts went away.
#
# Real shop: every scrap event is investigated. Material cost lost,

View File

@@ -1,4 +1,4 @@
# Scenario 18 Certificate flow simulation.
# Scenario 18 - Certificate flow simulation.
# Persona: Sarah (CSR) → Carlos (operator) → Tom (shipper)
# Goal: complete cert issuance from SO entry to customer email.
# Track every gap.
@@ -81,7 +81,7 @@ if not certs:
cert = certs[0]
# DISCOVERABILITY would Tom find the cert from the job form?
# DISCOVERABILITY - would Tom find the cert from the job form?
print()
print(f' [Tom] Looking at the job form, smart-button row:')
print(f' job.certificate_count = {getattr(job, "certificate_count", "no field")}')
@@ -99,7 +99,7 @@ except Exception as e:
# If blocked due to spec_reference, fix and retry
if cert.state == 'draft' and not cert.spec_reference:
print()
print(f' [Tom] Manually fills spec_reference (workflow gap should auto-fill from coating)')
print(f' [Tom] Manually fills spec_reference (workflow gap - should auto-fill from coating)')
cert.spec_reference = coating.spec_reference or 'AMS 2404'
try:
cert.action_issue()
@@ -110,7 +110,7 @@ if cert.state == 'draft' and not cert.spec_reference:
# Try Send to Customer
print()
print(f' [Tom] Clicks Send to Customer:')
print(f' cert.attachment_id = {cert.attachment_id.name if cert.attachment_id else "(none PDF not generated!)"}')
print(f' cert.attachment_id = {cert.attachment_id.name if cert.attachment_id else "(none - PDF not generated!)"}')
try:
act = cert.action_send_to_customer()
print(f' Composer opens. Default attachments: '

View File

@@ -1,4 +1,4 @@
# Scenario 19 Fischerscope thickness report PDF appended to CoC.
# Scenario 19 - Fischerscope thickness report PDF appended to CoC.
#
# Goal: when QC has a thickness_report_pdf_id uploaded by the operator
# on the tablet, action_issue should produce a multi-page CoC with the
@@ -104,7 +104,7 @@ cert = Cert.search([('x_fc_job_id', '=', job.id)], limit=1)
print()
print(f' Cert: {cert.name}, state={cert.state}')
# v19.0.6.20.0 new UI visibility fields (S19 Phase 2). Assert the
# v19.0.6.20.0 - new UI visibility fields (S19 Phase 2). Assert the
# operator would see "Will Append on Issue" badge BEFORE clicking Issue.
print(f' x_fc_thickness_status (pre-Issue): {cert.x_fc_thickness_status!r}')
print(f' x_fc_thickness_qc_id: {cert.x_fc_thickness_qc_id.name if cert.x_fc_thickness_qc_id else "(none)"}')
@@ -112,11 +112,11 @@ print(f' x_fc_thickness_pdf_id: {cert.x_fc_thickness_pdf_id.name
if cert.x_fc_thickness_status == 'pending':
print(f' ✓ UI banner WILL show "Fischerscope thickness PDF is on file"')
elif cert.x_fc_thickness_status == 'none':
print(f' ❌ UI says no PDF merge would not run on Issue')
print(f' ❌ UI says no PDF - merge would not run on Issue')
else:
print(f' ⚠️ unexpected status: {cert.x_fc_thickness_status}')
# Issue the cert should render CoC + merge Fischerscope as page 2
# Issue the cert - should render CoC + merge Fischerscope as page 2
cert.action_issue()
cert.invalidate_recordset(['x_fc_thickness_status', 'x_fc_thickness_qc_id', 'x_fc_thickness_pdf_id'])
print(f' After Issue: state={cert.state}')
@@ -141,7 +141,7 @@ if cert.attachment_id:
if len(reader.pages) >= 2:
print(f' ✓ CoC + Fischerscope merged (multi-page)')
else:
print(f' ❌ Only 1 page merge did not run')
print(f' ❌ Only 1 page - merge did not run')
except Exception as e:
print(f' ⚠️ couldn\'t parse output PDF: {e}')

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""Battle test S24 Live step priority chain + board state filter.
"""Battle test S24 - Live step priority chain + board state filter.
Run end-to-end via odoo shell with stdin redirection:
@@ -32,7 +32,7 @@ def run():
partner = env['res.partner'].search([('customer_rank', '>', 0)], limit=1)
if not partner:
raise AssertionError(
'No customer partner found seed test data first'
'No customer partner found - seed test data first'
)
recipe = env['fusion.plating.process.node'].search([
@@ -40,7 +40,7 @@ def run():
('child_ids', '!=', False),
], limit=1)
if not recipe:
raise AssertionError('No recipe found seed test data first')
raise AssertionError('No recipe found - seed test data first')
job = env['fp.job'].create({
'partner_id': partner.id,
@@ -52,7 +52,7 @@ def run():
if len(steps) < 3:
raise AssertionError('Need at least 3 steps for the test')
# === Phase A between-step assertion ===
# === Phase A - between-step assertion ===
s1 = steps[0]
s2 = steps[1]
s1.button_start()
@@ -68,9 +68,9 @@ def run():
'Card column should match s2.area_kind=%s, got %s'
% (s2.area_kind, _resolve_card_area(job))
)
_logger.info('[bt_s24] Phase A OK between-step routing correct')
_logger.info('[bt_s24] Phase A OK - between-step routing correct')
# === Phase B paused step assertion ===
# === Phase B - paused step assertion ===
s2.button_start()
s2.button_pause('lunch break')
job.invalidate_recordset(['active_step_id', 'card_state'])
@@ -79,9 +79,9 @@ def run():
'Paused step should remain the live step, got %s'
% job.active_step_id.id
)
_logger.info('[bt_s24] Phase B OK paused step stays live')
_logger.info('[bt_s24] Phase B OK - paused step stays live')
# === Phase C done job filter ===
# === Phase C - done job filter ===
for s in steps:
if s.state != 'done':
if s.state == 'paused':
@@ -104,7 +104,7 @@ def run():
raise AssertionError(
'Done job %s should be filtered off board' % job.id
)
_logger.info('[bt_s24] Phase C OK done jobs filtered off board')
_logger.info('[bt_s24] Phase C OK - done jobs filtered off board')
_logger.info('[bt_s24] ALL ASSERTIONS PASSED')

View File

@@ -1,4 +1,4 @@
# Scenario 9 Carlos starts step, Bob (supervisor) reassigns to Mike.
# Scenario 9 - Carlos starts step, Bob (supervisor) reassigns to Mike.
# Verify chatter audit trail.
from odoo import fields
@@ -36,14 +36,14 @@ print(f' assigned_user_id: {step.assigned_user_id.name}')
before_count = len(job.message_ids)
print(f' Job chatter messages before reassign: {before_count}')
# Bob reassigns to "another user" for the test we just write to ourselves
# Bob reassigns to "another user" - for the test we just write to ourselves
# to simulate, but the field write IS the operation.
print()
print('[Bob] Reassigning step to a different operator...')
# Use a different user if available.
other = env['res.users'].search([('id', '!=', env.user.id), ('share', '=', False)], limit=1)
if not other:
other = env.user # fallback at least the write fires
other = env.user # fallback - at least the write fires
step.assigned_user_id = other.id
step.invalidate_recordset()
job.invalidate_recordset()

View File

@@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
# E2E persona walk order entry from start to finish.
# E2E persona walk - order entry from start to finish.
#
# Personas:
# Sarah Customer Service Rep
# Mike Receiver
# Carlos Plating Operator
# Lisa QC Inspector
# Tom Shipper
# Jane Accounting
# Sarah - Customer Service Rep
# Mike - Receiver
# Carlos - Plating Operator
# Lisa - QC Inspector
# Tom - Shipper
# Jane - Accounting
#
# This script fills every visible-to-operator field per step, walks the
# workflow with no shortcuts, asserts the data is sane after each phase,
@@ -40,7 +40,7 @@ def e2e(env):
findings = []
# ----- pick a real partner with a recipe-able product -----
section('SETUP pick a customer + a part already in the catalog')
section('SETUP - pick a customer + a part already in the catalog')
Partner = env['res.partner']
Part = env['fp.part.catalog']
Coating = env['fp.coating.config']
@@ -59,11 +59,11 @@ def e2e(env):
step('Sarah', f'Part: {part.part_number or part.name} rev {part.revision or "?"} (id={part.id})')
step('Sarah', f'Coating: {coating.display_name if coating else "NONE"} (id={coating.id if coating else 0})')
if not coating:
findings.append('No fp.coating.config found in DB cannot create realistic SO')
findings.append('No fp.coating.config found in DB - cannot create realistic SO')
return findings
# ----- Sarah builds a sale order -----
section('PHASE 1 Sarah (CSR) creates the sale order')
section('PHASE 1 - Sarah (CSR) creates the sale order')
SO = env['sale.order']
SOL = env['sale.order.line']
so_vals = {
@@ -79,13 +79,13 @@ def e2e(env):
'x_fc_delivery_method': 'shipping_partner',
'x_fc_rush_order': False,
'x_fc_is_blanket_order': False,
'x_fc_internal_note': 'E2E test SO full persona walk.',
'x_fc_internal_note': 'E2E test SO - full persona walk.',
'x_fc_external_note': 'Standard plating per spec.',
}
so = SO.create(so_vals)
step('Sarah', f'Created SO {so.name} (id={so.id})')
# add a line fill the part / coating / treatment fields
# add a line - fill the part / coating / treatment fields
product = env['product.product'].search([('sale_ok', '=', True)], limit=1)
if not product:
findings.append('No saleable product available for SO line')
@@ -94,7 +94,7 @@ def e2e(env):
'order_id': so.id,
'product_id': product.id,
'product_uom_qty': 25,
'name': f'{part.part_number or part.name} Plating per coating spec',
'name': f'{part.part_number or part.name} - Plating per coating spec',
'x_fc_part_catalog_id': part.id,
'x_fc_coating_config_id': coating.id,
'x_fc_internal_description': 'Process via standard recipe; bake ASAP.',
@@ -103,7 +103,7 @@ def e2e(env):
line = SOL.create(line_vals)
step('Sarah', f'Added line: {line.product_uom_qty} × {line.name[:40]}')
# confirm does account hold block?
# confirm - does account hold block?
if partner.x_fc_account_hold:
find('Sarah', 'Customer is on account hold; SO confirm should block (or warn)')
try:
@@ -129,7 +129,7 @@ def e2e(env):
find('Sarah', 'NO fp.receiving auto-created on SO confirm! Receiver has nothing to track.')
findings.append('SO confirm did not auto-spawn fp.receiving')
if jobs and not portal_jobs:
find('Sarah', 'fp.job exists but no portal.job mirror customer can\'t track on portal.')
find('Sarah', 'fp.job exists but no portal.job mirror - customer can\'t track on portal.')
findings.append('Portal job mirror missing post-confirm')
# smart-button visibility check
@@ -140,7 +140,7 @@ def e2e(env):
f'NCRs visible? {so.fp_qc_ncr_count_so > 0} (count={so.fp_qc_ncr_count_so})')
# ----- Mike receives parts -----
section('PHASE 2 Mike (Receiver) processes inbound parts')
section('PHASE 2 - Mike (Receiver) processes inbound parts')
receiving = receivings[:1]
if not receiving:
receiving = Receiving.create({
@@ -148,7 +148,7 @@ def e2e(env):
'expected_qty': 25,
})
step('Mike', f'Manually created receiving {receiving.name} (auto-create did not fire)')
find('Mike', 'Had to manually create receiving auto-create from SO confirm is missing')
find('Mike', 'Had to manually create receiving - auto-create from SO confirm is missing')
findings.append('Auto-receiving on SO confirm not wired')
else:
step('Mike', f'Found auto-created receiving {receiving.name} (state={receiving.state})')
@@ -199,17 +199,17 @@ def e2e(env):
racks = Inspection.search([('sale_order_id', '=', so.id)])
step('Mike', f'Racking inspections for this SO: {len(racks)}')
if not racks:
find('Mike', 'Racking inspection NOT auto-created racking crew has nothing to walk.')
find('Mike', 'Racking inspection NOT auto-created - racking crew has nothing to walk.')
findings.append('No racking inspection auto-created post-confirm')
# ----- Carlos works the plating job -----
section('PHASE 3 Carlos (Operator) walks the plating job')
section('PHASE 3 - Carlos (Operator) walks the plating job')
if not jobs:
fail('Carlos', 'No job to work SO confirm did not spawn one. Skipping phase.')
fail('Carlos', 'No job to work - SO confirm did not spawn one. Skipping phase.')
else:
job = jobs[0]
step('Carlos', f'Job {job.name}: state={job.state}, qty={job.qty}, deadline={job.date_deadline}')
step('Carlos', f'Steps: {len(job.step_ids)} recipe={job.recipe_id.name or "(none)"}')
step('Carlos', f'Steps: {len(job.step_ids)} - recipe={job.recipe_id.name or "(none)"}')
if not job.step_ids:
find('Carlos', f'Job has zero steps! Recipe not assigned or not generated. Recipe field: {job.recipe_id}')
findings.append('Job confirmed with zero steps')
@@ -244,7 +244,7 @@ def e2e(env):
done_count = len(job.step_ids.filtered(lambda st: st.state == 'done'))
step('Carlos', f'Walked {done_count}/{len(job.step_ids)} steps to done')
# try to mark job done should hit QC gate if customer requires QC
# try to mark job done - should hit QC gate if customer requires QC
wants_qc = 'x_fc_requires_qc' in partner._fields and partner.x_fc_requires_qc
step('Carlos', f'Customer requires QC? {wants_qc}')
try:
@@ -258,7 +258,7 @@ def e2e(env):
findings.append(f'button_mark_done: {e}')
# ----- Lisa runs QC -----
section('PHASE 4 Lisa (QC) walks the checklist (if any)')
section('PHASE 4 - Lisa (QC) walks the checklist (if any)')
QC = env['fusion.plating.quality.check']
qcs = QC.search([('job_id', 'in', jobs.ids)]) if jobs else QC.browse()
step('Lisa', f'QC checks for this job: {len(qcs)}')
@@ -292,7 +292,7 @@ def e2e(env):
findings.append(f'Job blocked post-QC: {e}')
# ----- Tom ships -----
section('PHASE 5 Tom (Shipper) prepares the delivery')
section('PHASE 5 - Tom (Shipper) prepares the delivery')
Delivery = env['fusion.plating.delivery']
deliveries = Delivery.search([
'|', ('job_ref', 'in', jobs.mapped('name') if jobs else []),
@@ -325,14 +325,14 @@ def e2e(env):
findings.append('Certificate auto-create missing')
# ----- Jane invoices -----
section('PHASE 6 Jane (Accounting) creates and posts invoice')
section('PHASE 6 - Jane (Accounting) creates and posts invoice')
invoices_before = env['account.move'].search_count([
('invoice_origin', '=', so.name),
])
try:
if so.invoice_status == 'to invoice':
inv_action = so._create_invoices()
step('Jane', f'Invoiced {invoices_before}{env["account.move"].search_count([("invoice_origin","=",so.name)])} moves')
step('Jane', f'Invoiced - {invoices_before}{env["account.move"].search_count([("invoice_origin","=",so.name)])} moves')
else:
step('Jane', f'invoice_status={so.invoice_status} (nothing to invoice)')
except Exception as e:
@@ -340,7 +340,7 @@ def e2e(env):
findings.append(f'invoice creation: {e}')
# ----- common-sense edge case sweeps -----
section('PHASE 7 common-sense edge case sweeps')
section('PHASE 7 - common-sense edge case sweeps')
# smart-button results: do they actually return non-empty data?
section_name = ' smart-button result probes'
@@ -381,7 +381,7 @@ def e2e(env):
for i, f in enumerate(findings, 1):
print(f' {i}. {f}')
else:
print(' ✅ No findings workflow is clean end-to-end.')
print(' ✅ No findings - workflow is clean end-to-end.')
env.cr.commit()
return findings

View File

@@ -1,16 +1,16 @@
# Step 1 verification Direct Order wizard onchange + hold guard fixes.
# Step 1 verification - Direct Order wizard onchange + hold guard fixes.
W = env['fp.direct.order.wizard']
ISD = env['fp.invoice.strategy.default']
P = env['res.partner']
target = P.browse(2529) # 2CM INNOVATIVE
print('Test 1 customer with NO invoice strategy default:')
print('Test 1 - customer with NO invoice strategy default:')
ISD.search([('partner_id', '=', target.id)]).unlink()
w = W.new({'partner_id': target.id})
w._onchange_partner_id()
print(f' invoice_strategy={w.invoice_strategy}, payment_term={w.payment_term_id.name if w.payment_term_id else None}')
print('\nTest 2 customer WITH strategy default but NO payment_term:')
print('\nTest 2 - customer WITH strategy default but NO payment_term:')
isd = ISD.create({'partner_id': target.id, 'default_strategy': 'net_terms'})
w = W.new({'partner_id': target.id})
w._onchange_partner_id()
@@ -18,7 +18,7 @@ print(f' invoice_strategy={w.invoice_strategy} (expect: net_terms)')
print(f' payment_term={w.payment_term_id.name if w.payment_term_id else None}')
isd.unlink()
print('\nTest 3 customer with strategy + deposit + payment_term:')
print('\nTest 3 - customer with strategy + deposit + payment_term:')
pt = env['account.payment.term'].search([], limit=1)
isd = ISD.create({
'partner_id': target.id, 'default_strategy': 'deposit',
@@ -31,7 +31,7 @@ print(f' deposit_percent={w.deposit_percent} (expect: 50.0)')
print(f' payment_term={w.payment_term_id.name} (expect: {pt.name})')
isd.unlink()
print('\nTest 4 account-hold warning fires on partner change:')
print('\nTest 4 - account-hold warning fires on partner change:')
target.x_fc_account_hold = True
w = W.new({'partner_id': target.id})
result = w._onchange_partner_id()
@@ -39,7 +39,7 @@ warning = (result or {}).get('warning')
print(f' warning title: {warning.get("title") if warning else None}')
print(f' warning msg: {(warning.get("message") or "")[:100] if warning else None}')
print('\nTest 5 account-hold blocks action_create_order:')
print('\nTest 5 - account-hold blocks action_create_order:')
w = W.create({'partner_id': target.id, 'po_pending': True})
# add one line so the line check passes
part = env['fp.part.catalog'].search([], limit=1)
@@ -53,7 +53,7 @@ env['fp.direct.order.line'].create({
})
try:
w.action_create_order()
print(' ❌ HELD CUSTOMER CREATED ORDER guard failed')
print(' ❌ HELD CUSTOMER CREATED ORDER - guard failed')
except Exception as e:
print(f' ✓ blocked: {str(e)[:120]}')
target.x_fc_account_hold = False

View File

@@ -1,4 +1,4 @@
# Step 2 verification picking a part on the wizard line pre-fills coating + treatments.
# Step 2 verification - picking a part on the wizard line pre-fills coating + treatments.
W = env['fp.direct.order.wizard']
Line = env['fp.direct.order.line']
Part = env['fp.part.catalog']

View File

@@ -1,4 +1,4 @@
# Step 3 Sarah hits "Create Order" in wizard, then confirms SO.
# Step 3 - Sarah hits "Create Order" in wizard, then confirms SO.
# Watch every side-effect: SO state, fp.job auto-spawn, fp.receiving
# auto-spawn, fp.racking.inspection, portal.job mirror, QC check.

View File

@@ -1,4 +1,4 @@
# Step 4 Mike receives parts. Walk the receiving form, fill every
# Step 4 - Mike receives parts. Walk the receiving form, fill every
# visible field, walk the state machine, verify SO status updates at
# every transition.
@@ -54,7 +54,7 @@ try:
print(f' recv.state = {recv.state}')
print(f' SO status: {so.x_fc_receiving_status} (should still be partial)')
assert so.x_fc_receiving_status == 'partial'
print(' ✓ Still partial racking not done yet')
print(' ✓ Still partial - racking not done yet')
except Exception as e:
print(f'{e}')
@@ -71,7 +71,7 @@ try:
except Exception as e:
print(f'{e}')
# At this point Mike's done racking crew takes over.
# At this point Mike's done - racking crew takes over.
# But the receiving stays at "staged" until racking crew finishes
# inspection and someone clicks "Close" on the receiving.
# Let's pretend racking is done and close the receiving.

View File

@@ -1,8 +1,8 @@
# Step 5 Carlos walks the plating job. Test BOTH paths:
# Step 5 - Carlos walks the plating job. Test BOTH paths:
# A) Try to mark_done with steps still ready → must be blocked
# B) Walk every step → mark_done succeeds
# Build a fresh SO + job (don't reuse 423 its job is already done).
# Build a fresh SO + job (don't reuse 423 - its job is already done).
from odoo import fields
W = env['fp.direct.order.wizard']
Line = env['fp.direct.order.line']
@@ -41,7 +41,7 @@ print()
print('[Carlos] Try Mark Done WITHOUT walking any step (compliance test):')
try:
job.button_mark_done()
print(' ❌ JOB CLOSED WITH ZERO STEPS WALKED guard failed')
print(' ❌ JOB CLOSED WITH ZERO STEPS WALKED - guard failed')
except Exception as e:
print(f' ✓ blocked: {str(e)[:200]}')
@@ -58,7 +58,7 @@ print(f' walked {done_count}/{len(job.step_ids)} to done')
try:
job.button_mark_done()
print(f' ✓ Job marked done state={job.state}, finished={job.date_finished}')
print(f' ✓ Job marked done - state={job.state}, finished={job.date_finished}')
except Exception as e:
print(f' ❌ Mark Done failed AFTER walking: {e}')
@@ -90,7 +90,7 @@ job2 = env['fp.job'].search([('sale_order_id', '=', so2.id)], limit=1)
print(f' Created fresh job {job2.name} with {len(job2.step_ids)} unworked steps')
try:
job2.with_context(fp_skip_step_gate=True).button_mark_done()
print(f' ✓ Manager bypass worked job state={job2.state}')
print(f' ✓ Manager bypass worked - job state={job2.state}')
except Exception as e:
print(f' ❌ Bypass failed: {e}')

View File

@@ -1,4 +1,4 @@
# Step 6 Lisa walks the QC checklist for a customer that requires QC.
# Step 6 - Lisa walks the QC checklist for a customer that requires QC.
# Test:
# A) Customer requires QC but no template configured → spawn fails gracefully?
# B) Customer requires QC + template configured → check auto-spawns on confirm
@@ -21,7 +21,7 @@ if not default_tpl:
'name': 'Default QC Template (E2E)',
'active': True,
})
TplLine.create({'template_id': default_tpl.id, 'sequence': 10, 'name': 'Visual inspection appearance'})
TplLine.create({'template_id': default_tpl.id, 'sequence': 10, 'name': 'Visual inspection - appearance'})
TplLine.create({'template_id': default_tpl.id, 'sequence': 20, 'name': 'Thickness measurement (Fischerscope)'})
TplLine.create({'template_id': default_tpl.id, 'sequence': 30, 'name': 'Tape adhesion test'})
print(f'[setup] Created default QC template: {default_tpl.name} ({len(default_tpl.line_ids)} lines)')
@@ -91,7 +91,7 @@ for s in job.step_ids.sorted('sequence'):
s.button_finish()
try:
job.button_mark_done()
print(f' ✓ Job done state={job.state}')
print(f' ✓ Job done - state={job.state}')
except Exception as e:
print(f' ❌ Job mark_done blocked: {e}')

View File

@@ -1,6 +1,6 @@
# Step 7 Tom (Shipper) walks the delivery from draft to delivered.
# Step 7 - Tom (Shipper) walks the delivery from draft to delivered.
# Test:
# A) Delivery exists post-job-done what fields visible? what state?
# A) Delivery exists post-job-done - what fields visible? what state?
# B) Try action_start_route without driver → must block
# C) Assign driver + vehicle + box count, schedule
# D) Try action_mark_delivered without POD → must block

View File

@@ -1,4 +1,4 @@
# Step 8 re-verify fresh SO with net_terms strategy should now get
# Step 8 re-verify - fresh SO with net_terms strategy should now get
# Net-30 payment term auto-filled, and the invoice should post.
from odoo import fields

View File

@@ -1,4 +1,4 @@
# Step 8 Jane creates the invoice for the completed SO and posts it.
# Step 8 - Jane creates the invoice for the completed SO and posts it.
# Test:
# A) SO has invoice_status = 'to invoice' after delivery
# B) Jane creates the invoice
@@ -40,7 +40,7 @@ if so.invoice_status == 'to invoice':
except Exception as e:
print(f'{e}')
else:
print(f' Skipped invoice_status={so.invoice_status} (nothing to invoice)')
print(f' Skipped - invoice_status={so.invoice_status} (nothing to invoice)')
new_invs = env['account.move'].browse()
# Path B: post.

View File

@@ -23,7 +23,7 @@ treats = Treat.search([], limit=2)
# ====================================================================== A
print('='*72)
print('Scenario A Brand-new part (no defaults)')
print('Scenario A - Brand-new part (no defaults)')
print('='*72)
fresh = Part.create({
'partner_id': target.id,
@@ -47,7 +47,7 @@ if result and result.get('warning'):
# ====================================================================== B
print()
print('='*72)
print('Scenario B Existing part WITH defaults already set')
print('Scenario B - Existing part WITH defaults already set')
print('='*72)
existing = Part.search([('x_fc_default_coating_config_id', '!=', False)], limit=1)
print(f' Using part: {existing.display_name} (default coating={existing.x_fc_default_coating_config_id.name})')
@@ -56,14 +56,14 @@ w2._onchange_partner_id()
ln2 = Line.new({'wizard_id': w2.id})
ln2.part_catalog_id = existing
result2 = ln2._onchange_part_clears_variant()
print(f' push_to_defaults after onchange: {ln2.push_to_defaults} (expect False defaults already exist)')
print(f' push_to_defaults after onchange: {ln2.push_to_defaults} (expect False - defaults already exist)')
print(f' pre-filled coating: {ln2.coating_config_id.name if ln2.coating_config_id else "(none)"}')
print(f' warning fired? {bool(result2 and result2.get("warning"))} (expect False)')
# ====================================================================== C
print()
print('='*72)
print('Scenario C Brand-new part flagged is_one_off (don\'t persist)')
print('Scenario C - Brand-new part flagged is_one_off (don\'t persist)')
print('='*72)
fresh3 = Part.create({
'partner_id': target.id,
@@ -75,13 +75,13 @@ w3._onchange_partner_id()
ln3 = Line.new({'wizard_id': w3.id, 'is_one_off': True})
ln3.part_catalog_id = fresh3
result3 = ln3._onchange_part_clears_variant()
print(f' push_to_defaults after onchange: {ln3.push_to_defaults} (expect False is_one_off blocks)')
print(f' push_to_defaults after onchange: {ln3.push_to_defaults} (expect False - is_one_off blocks)')
print(f' warning fired? {bool(result3 and result3.get("warning"))} (expect False)')
# ====================================================================== D
print()
print('='*72)
print('Scenario D End-to-end: order #1 saves defaults, order #2 pre-fills')
print('Scenario D - End-to-end: order #1 saves defaults, order #2 pre-fills')
print('='*72)
fresh_d = Part.create({
'partner_id': target.id,
@@ -96,7 +96,7 @@ w_d._onchange_partner_id()
ln_d = Line.new({'wizard_id': w_d.id})
ln_d.part_catalog_id = fresh_d
ln_d._onchange_part_clears_variant()
print(f' Order #1 line auto-ticked push_to_defaults: {ln_d.push_to_defaults}')
print(f' Order #1 line - auto-ticked push_to_defaults: {ln_d.push_to_defaults}')
# Sarah picks the coating + treatments she wants
saved = Line.create({
'wizard_id': w_d.id, 'part_catalog_id': fresh_d.id,
@@ -117,9 +117,9 @@ print(f' Part defaults after order #1:')
print(f' x_fc_default_coating_config_id: {fresh_d.x_fc_default_coating_config_id.name if fresh_d.x_fc_default_coating_config_id else "(none)"}')
print(f' x_fc_default_treatment_ids: {fresh_d.x_fc_default_treatment_ids.mapped("name") if fresh_d.x_fc_default_treatment_ids else "(none)"}')
# ORDER #2 Sarah picks the same part again
# ORDER #2 - Sarah picks the same part again
print()
print(' Order #2 Sarah picks the same part:')
print(' Order #2 - Sarah picks the same part:')
w_d2 = W.create({'partner_id': target.id, 'po_pending': True, 'invoice_strategy': 'net_terms'})
w_d2._onchange_partner_id()
ln_d2 = Line.new({'wizard_id': w_d2.id})
@@ -127,7 +127,7 @@ ln_d2.part_catalog_id = fresh_d
ln_d2._onchange_part_clears_variant()
print(f' Pre-filled coating: {ln_d2.coating_config_id.name if ln_d2.coating_config_id else "(none)"}')
print(f' Pre-filled treatments: {ln_d2.treatment_ids.mapped("name") if ln_d2.treatment_ids else "(none)"}')
print(f' push_to_defaults: {ln_d2.push_to_defaults} (expect False defaults exist)')
print(f' push_to_defaults: {ln_d2.push_to_defaults} (expect False - defaults exist)')
if ln_d2.coating_config_id == coating:
print(f' ✓ Order #2 correctly auto-filled from order #1\'s saved defaults')
else:

View File

@@ -1,7 +1,7 @@
# Comprehensive internal-process walk.
#
# Phases:
# A) Pause / resume multiple intervals merge into duration_actual
# A) Pause / resume - multiple intervals merge into duration_actual
# B) Skip an opt-in step
# C) Skipped steps don't block job mark-done
# D) Wet plating step finish auto-spawns bake.window with right window_hours
@@ -65,7 +65,7 @@ print(f'[setup] Job {job.name} with {len(job.step_ids)} steps')
# ====================================================================== A
print()
print('='*72)
print('A Pause + resume on a step. Multiple intervals must merge.')
print('A - Pause + resume on a step. Multiple intervals must merge.')
print('='*72)
masking = job.step_ids.sorted('sequence')[0]
masking.button_start()
@@ -90,7 +90,7 @@ else:
# ====================================================================== B
print()
print('='*72)
print('B Skip an opt-in step')
print('B - Skip an opt-in step')
print('='*72)
racking = job.step_ids.sorted('sequence')[1]
print(f' Step: {racking.name} state={racking.state}')
@@ -99,10 +99,10 @@ print(f' After Skip: state={racking.state}')
if racking.state == 'skipped':
print(f' ✓ Skip works')
# ====================================================================== C walk rest, then mark-done
# ====================================================================== C - walk rest, then mark-done
print()
print('='*72)
print('C Walk remaining steps (some will spawn bake-window). Mark job done.')
print('C - Walk remaining steps (some will spawn bake-window). Mark job done.')
print('='*72)
spawn_count_before = env['fusion.plating.bake.window'].search_count([])
for s in job.step_ids.sorted('sequence'):
@@ -121,23 +121,23 @@ bws = env['fusion.plating.bake.window'].search([('part_ref', '=', job.name)])
for bw in bws:
print(f' {bw.name}: state={bw.state}, plate_exit={bw.plate_exit_time}, required_by={bw.bake_required_by}, time_remaining={bw.time_remaining_display}')
# ====================================================================== D try to mark job done
# ====================================================================== D - try to mark job done
print()
print('='*72)
print('D Mark job done (skipped+done steps both count as terminal)')
print('D - Mark job done (skipped+done steps both count as terminal)')
print('='*72)
try:
job.button_mark_done()
print(f' ✓ Job done state={job.state}')
print(f' ✓ Job done - state={job.state}')
except Exception as e:
print(f'{e}')
# ====================================================================== E bake-window lifecycle
# ====================================================================== E - bake-window lifecycle
if bws:
bw = bws[0]
print()
print('='*72)
print('E Bake-window lifecycle: start → end')
print('E - Bake-window lifecycle: start → end')
print('='*72)
print(f' Before start: state={bw.state}, color={bw.status_color}')
bw.action_start_bake()
@@ -146,10 +146,10 @@ if bws:
bw.action_end_bake()
print(f' After end_bake: state={bw.state}, bake_end={bw.bake_end_time}, duration_h={bw.bake_duration_hours:.4f}')
# ====================================================================== F failure: start a done step
# ====================================================================== F - failure: start a done step
print()
print('='*72)
print('F Failure paths')
print('F - Failure paths')
print('='*72)
done_step = job.step_ids.filtered(lambda s: s.state == 'done')[:1]
if done_step:

View File

@@ -1,11 +1,11 @@
# Internal-process walk test time tracking, pause, skip, bake-window
# Internal-process walk - test time tracking, pause, skip, bake-window
# auto-spawn, duration overrun. Persona: Carlos (operator) walking the
# tablet station for a real plating job.
#
# Goals:
# 1) Time tracking captures every start/stop interval correctly
# 2) Multiple intervals (start/finish/start/finish) sum to duration_actual
# 3) Pause / resume flow works (currently NOT implemented gap to fix)
# 3) Pause / resume flow works (currently NOT implemented - gap to fix)
# 4) Skip flow works for opt-in steps (currently NOT implemented)
# 5) Wet plating step finishing auto-spawns a bake.window when the
# coating requires hydrogen embrittlement relief
@@ -46,7 +46,7 @@ print(f'[setup] Fresh job {job.name} with {len(job.step_ids)} steps')
# ====================================================================== STEP 1
print()
print('='*72)
print('STEP 1 Carlos opens the first step on the tablet, clicks Start')
print('STEP 1 - Carlos opens the first step on the tablet, clicks Start')
print('='*72)
first = job.step_ids.sorted('sequence')[0]
print(f' Step: {first.name} (kind={first.kind}, state={first.state})')
@@ -60,7 +60,7 @@ print(f' Open time-log rows: {len(first.time_log_ids.filtered(lambda l: not l.d
# ====================================================================== STEP 2
print()
print('='*72)
print('STEP 2 Carlos works for 6 seconds, then clicks Finish')
print('STEP 2 - Carlos works for 6 seconds, then clicks Finish')
print('='*72)
time.sleep(6)
first.button_finish()
@@ -74,12 +74,12 @@ print(f' ✓ Single interval captured cleanly')
# ====================================================================== STEP 3
print()
print('='*72)
print('STEP 3 Test pause/resume on the next step (currently NotImplementedError)')
print('STEP 3 - Test pause/resume on the next step (currently NotImplementedError)')
print('='*72)
second = job.step_ids.sorted('sequence')[1]
second.button_start()
print(f' Started step: {second.name} (state={second.state})')
print(f' Carlos now needs a smoke break clicks Pause')
print(f' Carlos now needs a smoke break - clicks Pause')
try:
second.button_pause()
print(f' ✓ Paused: state={second.state}, open timelog={len(second.time_log_ids.filtered(lambda l: not l.date_finished))}')
@@ -91,7 +91,7 @@ except Exception as e:
# ====================================================================== STEP 4
print()
print('='*72)
print('STEP 4 Test skip (currently NotImplementedError)')
print('STEP 4 - Test skip (currently NotImplementedError)')
print('='*72)
third = job.step_ids.sorted('sequence')[2]
print(f' Step: {third.name}, state={third.state}')
@@ -107,7 +107,7 @@ except Exception as e:
# ====================================================================== STEP 5
print()
print('='*72)
print('STEP 5 Wet plating step finishes, does a bake.window auto-spawn?')
print('STEP 5 - Wet plating step finishes, does a bake.window auto-spawn?')
print('='*72)
# Find a step with kind='wet' (or use step #4 as plating analog)
wet_step = job.step_ids.filtered(lambda s: 'plating' in (s.name or '').lower())[:1]
@@ -125,7 +125,7 @@ BW = env['fusion.plating.bake.window']
bw_before = BW.search_count([('part_ref', '=', job.name)])
print(f' Bake windows for this job BEFORE finish: {bw_before}')
# Skip if currently in_progress (it is paused step #2 still open)
# Skip if currently in_progress (it is - paused step #2 still open)
if wet_step.state in ('pending', 'ready'):
wet_step.button_start()
if wet_step.state == 'in_progress':
@@ -137,7 +137,7 @@ print(f' Bake windows for this job AFTER finish: {bw_after}')
if coating.requires_bake_relief and bw_after == bw_before:
print(f' ❌ Coating requires bake relief BUT no bake.window was auto-created!')
elif not coating.requires_bake_relief:
print(f' (coating doesn\'t require bake relief auto-spawn would skip anyway)')
print(f' (coating doesn\'t require bake relief - auto-spawn would skip anyway)')
else:
print(f' ✓ Bake window spawned')

View File

@@ -1,7 +1,7 @@
# Walk: Sarah opens Direct Order, creates a brand-new part inline, attaches a process.
#
# Personas:
# Sarah (CSR) driving the wizard
# Sarah (CSR) - driving the wizard
#
# What we're testing:
# 1) Wizard now allows creating a new part (no_quick_create lets the
@@ -30,7 +30,7 @@ print()
# ====================================================================== STEP 2
print('='*72)
print('STEP 2 Sarah opens wizard, hits "Create and edit..." on Part field')
print('STEP 2 - Sarah opens wizard, hits "Create and edit..." on Part field')
print('='*72)
w = W.create({
@@ -56,7 +56,7 @@ print(f'[Sarah] Filled popup → created part: {new_part.display_name}')
print(f' partner_id correctly set: {new_part.partner_id.name}')
print(f' part_number: {new_part.part_number}')
print(f' revision: {new_part.revision}')
print(f' default_process_id: {new_part.default_process_id.name if new_part.default_process_id else "(none no variants composed yet)"}')
print(f' default_process_id: {new_part.default_process_id.name if new_part.default_process_id else "(none - no variants composed yet)"}')
print(f' process_variant_count: {new_part.process_variant_count}')
# Now Sarah adds a line with the new part.
@@ -65,7 +65,7 @@ ln.part_catalog_id = new_part
ln._onchange_part_clears_variant()
print()
print(f'[Sarah] Adds line with new part:')
print(f' Pre-filled coating: {ln.coating_config_id.name if ln.coating_config_id else "(none new part has no defaults)"}')
print(f' Pre-filled coating: {ln.coating_config_id.name if ln.coating_config_id else "(none - new part has no defaults)"}')
print(f' Pre-filled treatments: {ln.treatment_ids.mapped("name") if ln.treatment_ids else "(none)"}')
# Sarah picks coating manually (since new part has no defaults).
@@ -83,13 +83,13 @@ real_line = Line.create({
# Check variant dropdown availability
print()
print(f'[Sarah] Variant dropdown for new part:')
print(f' Available variants: {len(new_part.process_variant_ids)} (expect 0 none composed yet)')
print(f' Available variants: {len(new_part.process_variant_ids)} (expect 0 - none composed yet)')
print(f' → Sarah leaves variant blank; coating.recipe_id will drive job')
# ====================================================================== STEP 3
print()
print('='*72)
print('STEP 3 Sarah confirms order, verify part landed in catalog + job uses coating recipe')
print('STEP 3 - Sarah confirms order, verify part landed in catalog + job uses coating recipe')
print('='*72)
result = w.action_create_order()
so = env['sale.order'].browse(result['res_id'])
@@ -113,7 +113,7 @@ print(f' Part survives in catalog: {fetched.display_name} (id={fetched.id})')
# ====================================================================== STEP 4
print()
print('='*72)
print('STEP 4 Bob attaches a variant to the new part (compose flow)')
print('STEP 4 - Bob attaches a variant to the new part (compose flow)')
print('='*72)
from odoo.addons.fusion_plating_configurator.controllers.fp_part_composer_controller \
import _clone_subtree
@@ -133,10 +133,10 @@ new_part.invalidate_recordset()
print(f' process_variant_count now: {new_part.process_variant_count}')
print(f' default_process_id: {new_part.default_process_id.name}')
# Now Sarah enters a SECOND order this time variant dropdown should show "Standard"
# Now Sarah enters a SECOND order - this time variant dropdown should show "Standard"
print()
print('='*72)
print('STEP 5 Sarah enters a follow-up order; variant dropdown should now show "Standard"')
print('STEP 5 - Sarah enters a follow-up order; variant dropdown should now show "Standard"')
print('='*72)
w2 = W.create({
'partner_id': target.id, 'po_pending': True,

View File

@@ -1,13 +1,13 @@
# Walk part creation + 4 process variants step by step.
# Personas:
# Bob (Estimator) owns the part catalog, designs process variants
# Sarah (CSR) picks a variant on order entry
# Bob (Estimator) - owns the part catalog, designs process variants
# Sarah (CSR) - picks a variant on order entry
#
# Goal: prove that
# 1) Bob can create a part
# 2) Bob can attach 4 distinct process variants via the Composer flow
# 3) One is flagged default; switching default works
# 4) Sarah opens a Direct Order, picks the part variant dropdown lists ALL FOUR
# 4) Sarah opens a Direct Order, picks the part - variant dropdown lists ALL FOUR
# 5) Sarah picks a non-default variant; the SO + job actually use it
from odoo import fields
@@ -23,7 +23,7 @@ Tpl = Node # template recipes are also fp.process.node records
# ====================================================================== STEP 2
print('='*72)
print('STEP 2 Bob creates a brand-new part')
print('STEP 2 - Bob creates a brand-new part')
print('='*72)
target_partner = P.browse(2529) # 2CM INNOVATIVE
default_coating = Coating.search([], limit=1)
@@ -55,14 +55,14 @@ template = Node.search([
('part_catalog_id', '=', False),
], limit=1)
if not template:
print(' ❌ No shared template recipes available cannot continue!')
print(' ❌ No shared template recipes available - cannot continue!')
raise SystemExit
print(f'[Bob] Will clone from shared template: {template.name} ({len(template.child_ids)} root children)')
# ====================================================================== STEP 3
print()
print('='*72)
print('STEP 3 Bob adds variant #1: Standard Production')
print('STEP 3 - Bob adds variant #1: Standard Production')
print('='*72)
v1 = _clone_subtree(env, template, part, parent=False)
v1.variant_label = 'Standard Production'
@@ -75,17 +75,17 @@ print(f' child nodes cloned: {len(v1.child_ids)}')
# ====================================================================== STEP 4
print()
print('='*72)
print('STEP 4 Bob adds variant #2: Aerospace Cert (AS9100)')
print('STEP 4 - Bob adds variant #2: Aerospace Cert (AS9100)')
print('='*72)
v2 = _clone_subtree(env, template, part, parent=False)
v2.variant_label = 'Aerospace Cert (AS9100)'
print(f'[Bob] Created variant: {v2.variant_label} (root id={v2.id})')
print(f' is_default: {v2.is_default_variant} (correct first one stays default)')
print(f' is_default: {v2.is_default_variant} (correct - first one stays default)')
# ====================================================================== STEP 5
print()
print('='*72)
print('STEP 5 Bob adds variant #3: Quick-turn (no bake)')
print('STEP 5 - Bob adds variant #3: Quick-turn (no bake)')
print('='*72)
v3 = _clone_subtree(env, template, part, parent=False)
v3.variant_label = 'Quick-turn (no bake)'
@@ -94,7 +94,7 @@ print(f'[Bob] Created variant: {v3.variant_label} (root id={v3.id})')
# ====================================================================== STEP 6
print()
print('='*72)
print('STEP 6 Bob adds variant #4: Heavy build (wear)')
print('STEP 6 - Bob adds variant #4: Heavy build (wear)')
print('='*72)
v4 = _clone_subtree(env, template, part, parent=False)
v4.variant_label = 'Heavy build (wear)'
@@ -103,18 +103,18 @@ print(f'[Bob] Created variant: {v4.variant_label} (root id={v4.id})')
# Refresh the part and inspect what the form would show.
part.invalidate_recordset()
print()
print(f'[Bob] After 4 adds part {part.display_name}:')
print(f'[Bob] After 4 adds - part {part.display_name}:')
print(f' process_variant_count: {part.process_variant_count}')
print(f' default_process_id: {part.default_process_id.name if part.default_process_id else None}')
print(f' Variants list (per Composer endpoint /fp/part/composer/state):')
for entry in _list_variants(part):
flag = '★ default' if entry['is_default'] else ' '
print(f' {flag} id={entry["id"]:>5} "{entry["label"]}" {entry["node_count"]} nodes')
print(f' {flag} id={entry["id"]:>5} "{entry["label"]}" - {entry["node_count"]} nodes')
# ====================================================================== STEP 7
print()
print('='*72)
print('STEP 7 Sarah enters a Direct Order, picks the part, picks a variant')
print('STEP 7 - Sarah enters a Direct Order, picks the part, picks a variant')
print('='*72)
W = env['fp.direct.order.wizard']
Line = env['fp.direct.order.line']
@@ -163,7 +163,7 @@ print(f' Saved line: process_variant_id={new_line.process_variant_id.variant_la
# ====================================================================== STEP 8
print()
print('='*72)
print('STEP 8 Confirm SO; verify the JOB uses variant #3, not the default')
print('STEP 8 - Confirm SO; verify the JOB uses variant #3, not the default')
print('='*72)
result = w.action_create_order()
so = env['sale.order'].browse(result['res_id'])

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# End-to-end order walkthrough simulates each role on the shop floor.
# End-to-end order walkthrough - simulates each role on the shop floor.
#
# Run via odoo-shell:
# echo 'exec(open("/mnt/extra-addons/custom/fusion_plating_quality/scripts/sub12_e2e_walkthrough.py").read())' \
@@ -24,7 +24,7 @@ def walk():
print('====================== E2E ORDER WALKTHROUGH ======================')
# ------------------------------------------------------------------
# ROLE: Sales / Estimator open Plating > Sales > Quotations
# ROLE: Sales / Estimator - open Plating > Sales > Quotations
# ------------------------------------------------------------------
print('\n[ROLE: Estimator] Plating > Sales > Quotations > New Quote')
@@ -47,7 +47,7 @@ def walk():
part = Part.search([], limit=1)
if not part:
gap('Estimator', 'fp.part.catalog',
'No parts in catalog estimator has nothing to quote against')
'No parts in catalog - estimator has nothing to quote against')
return
print(f' Part chosen: {part.display_name} '
f'(part#={getattr(part, "part_number", "?")} '
@@ -68,7 +68,7 @@ def walk():
coating = e['fp.coating.config'].search([], limit=1)
if not coating:
gap('Estimator', 'fp.coating.config',
'No coating configs defined estimator cannot configure quote')
'No coating configs defined - estimator cannot configure quote')
else:
print(f' Coating chosen: {coating.display_name}')
@@ -91,7 +91,7 @@ def walk():
gap('Estimator', 'fp.quote.configurator.create', str(ex))
return
# 4a. Try the "Create Quotation" path what action confirms the SO?
# 4a. Try the "Create Quotation" path - what action confirms the SO?
so = False
for meth in ('action_create_quotation', 'action_promote_to_direct_order',
'action_create_sale_order', 'action_generate_quote'):
@@ -112,7 +112,7 @@ def walk():
# Fall back: create SO directly and see if the configurator workflow is wired.
gap('Estimator', 'configurator',
'No working "create quote" action found on the configurator '
' estimator has no button to make a quote')
'- estimator has no button to make a quote')
# Manual SO creation for the rest of the walkthrough
SO = e['sale.order']
try:
@@ -155,7 +155,7 @@ def walk():
if so.state == 'draft':
try:
so.action_confirm()
print(f' ✓ SO confirmed state={so.state}')
print(f' ✓ SO confirmed - state={so.state}')
except Exception as ex:
gap('Estimator', 'sale.order.action_confirm', str(ex))
return
@@ -167,7 +167,7 @@ def walk():
jobs = Job.search([('sale_order_id', '=', so.id)])
if not jobs:
gap('Planner', 'fp.job auto-create',
'No fp.job auto-created on SO confirm planner has nothing '
'No fp.job auto-created on SO confirm - planner has nothing '
'to plan against')
else:
print(f'{len(jobs)} fp.job(s) created: '
@@ -178,7 +178,7 @@ def walk():
receivings = Recv.search([('sale_order_id', '=', so.id)])
if not receivings:
gap('Receiver', 'fp.receiving auto-create',
'No fp.receiving auto-created on SO confirm receiver has '
'No fp.receiving auto-created on SO confirm - receiver has '
'nothing to count against')
else:
print(f' ✓ Receiving record(s): {", ".join(receivings.mapped("name"))}')
@@ -188,7 +188,7 @@ def walk():
insps = Insp.search([('sale_order_id', '=', so.id)])
if not insps and jobs:
gap('Racker', 'fp.racking.inspection auto-create',
'jobs exist but no racking inspection racker walks empty')
'jobs exist but no racking inspection - racker walks empty')
elif insps:
print(f' ✓ Racking inspection(s): '
f'{", ".join(insps.mapped("name"))}')
@@ -202,10 +202,10 @@ def walk():
f'{", ".join(pjs.mapped("name"))}')
else:
gap('Portal', 'portal job auto-create',
'No portal.job mirror customer sees nothing on portal')
'No portal.job mirror - customer sees nothing on portal')
# ------------------------------------------------------------------
# ROLE: Receiver Plating > Receiving > All Receiving
# ROLE: Receiver - Plating > Receiving > All Receiving
# ------------------------------------------------------------------
print('\n[ROLE: Receiver] Open the receiving record, count boxes')
if receivings:
@@ -218,7 +218,7 @@ def walk():
if hasattr(r, 'action_mark_counted'):
try:
r.action_mark_counted()
print(f' ✓ Marked counted state={r.state}')
print(f' ✓ Marked counted - state={r.state}')
except Exception as ex:
gap('Receiver', 'action_mark_counted', str(ex))
else:
@@ -227,7 +227,7 @@ def walk():
if hasattr(r, 'action_mark_staged'):
try:
r.action_mark_staged()
print(f' ✓ Marked staged state={r.state}')
print(f' ✓ Marked staged - state={r.state}')
except Exception as ex:
gap('Receiver', 'action_mark_staged', str(ex))
# Smart button to racking inspection?
@@ -239,7 +239,7 @@ def walk():
'no smart button; receiver navigates manually')
# ------------------------------------------------------------------
# ROLE: Racking Crew open the linked racking inspection
# ROLE: Racking Crew - open the linked racking inspection
# ------------------------------------------------------------------
print('\n[ROLE: Racker] Open the racking inspection from receiving smart button')
if insps:
@@ -253,18 +253,18 @@ def walk():
if hasattr(insp, 'action_start'):
try:
insp.action_start()
print(f' ✓ Inspection started state={insp.state}')
print(f' ✓ Inspection started - state={insp.state}')
except Exception as ex:
gap('Racker', 'racking_inspection.action_start', str(ex))
if hasattr(insp, 'action_complete'):
try:
insp.action_complete()
print(f' ✓ Inspection completed state={insp.state}')
print(f' ✓ Inspection completed - state={insp.state}')
except Exception as ex:
gap('Racker', 'racking_inspection.action_complete', str(ex))
# ------------------------------------------------------------------
# ROLE: Operator runs the plating job step-by-step
# ROLE: Operator - runs the plating job step-by-step
# ------------------------------------------------------------------
print('\n[ROLE: Operator] Open the job, run each step')
if jobs:
@@ -272,7 +272,7 @@ def walk():
steps = job.step_ids.sorted('sequence')
if not steps:
gap('Operator', 'fp.job.step_ids',
'job has no steps recipe not generated')
'job has no steps - recipe not generated')
else:
print(f' Job {job.name} has {len(steps)} steps')
ran = 0
@@ -286,17 +286,17 @@ def walk():
gap('Operator', f'step.{step.name}', str(ex))
else:
gap('Operator', f'step.{step.name}',
f"state={step.state} operator can't start it")
f"state={step.state} - operator can't start it")
print(f' ✓ Ran {ran} of 3 first steps')
# ------------------------------------------------------------------
# ROLE: Inspector walk the QC checklist if customer requires QC
# ROLE: Inspector - walk the QC checklist if customer requires QC
# ------------------------------------------------------------------
print('\n[ROLE: Inspector] Look for an open QC check on the job')
QC = e['fusion.plating.quality.check']
if jobs:
job = jobs[0]
# Customer might not be flagged x_fc_requires_qc flip it for the test.
# Customer might not be flagged x_fc_requires_qc - flip it for the test.
wants = ('x_fc_requires_qc' in customer._fields
and customer.x_fc_requires_qc)
print(f' Customer requires QC: {wants}')
@@ -309,7 +309,7 @@ def walk():
print(f' ✓ QC check found: {check.name}')
# ------------------------------------------------------------------
# ROLE: Operator try to mark job done (will hit QC gate if applicable)
# ROLE: Operator - try to mark job done (will hit QC gate if applicable)
# ------------------------------------------------------------------
print('\n[ROLE: Operator] Click Mark Done on the job')
if jobs:
@@ -329,7 +329,7 @@ def walk():
pass
try:
job.with_context(fp_skip_qc_gate=True).button_mark_done()
print(f' ✓ Job marked done (with QC bypass) state={job.state}')
print(f' ✓ Job marked done (with QC bypass) - state={job.state}')
except Exception as ex:
gap('Operator', 'fp.job.button_mark_done', str(ex))
@@ -344,7 +344,7 @@ def walk():
f'{", ".join(deliveries.mapped("name") or ["(none)"])}')
# ------------------------------------------------------------------
# ROLE: Driver picks up the delivery
# ROLE: Driver - picks up the delivery
# ------------------------------------------------------------------
print('\n[ROLE: Driver] Find the linked fusion.plating.delivery')
if Del is not None and jobs:
@@ -355,14 +355,14 @@ def walk():
if hasattr(d, 'action_mark_delivered'):
try:
d.action_mark_delivered()
print(f' ✓ Marked delivered state={d.state}')
print(f' ✓ Marked delivered - state={d.state}')
except Exception as ex:
gap('Driver', 'delivery.action_mark_delivered', str(ex))
else:
print(' No delivery linked to job checking by SO')
print(' No delivery linked to job - checking by SO')
# ------------------------------------------------------------------
# ROLE: Accountant invoice the SO
# ROLE: Accountant - invoice the SO
# ------------------------------------------------------------------
print('\n[ROLE: Accountant] Generate invoice')
print(f' invoice_status={so.invoice_status}')
@@ -376,7 +376,7 @@ def walk():
except Exception as ex:
gap('Accountant', 'sale.order._create_invoices', str(ex))
elif so.invoice_status == 'no':
# qty_delivered is 0 service products invoice on ordered qty by
# qty_delivered is 0 - service products invoice on ordered qty by
# default. If "no" persists, the SO has no invoiceable lines yet
# (e.g. delivered_qty=0 + invoice_policy='delivery').
print(f' Note: SO not yet invoiceable (qty_delivered=0). '
@@ -388,7 +388,7 @@ def walk():
# ------------------------------------------------------------------
print('\n=========================== SUMMARY ===========================')
if not GAPS:
print('NO GAPS FOUND workflow walked end-to-end clean')
print('NO GAPS FOUND - workflow walked end-to-end clean')
else:
print(f'{len(GAPS)} GAP(S) FOUND:')
for role, where, msg in GAPS:

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Sub 12 Phase F end-to-end smoke test.
# Sub 12 Phase F - end-to-end smoke test.
#
# Run via odoo-shell:
# /usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin
@@ -16,7 +16,7 @@ def _resolve_test_partner(env):
"""Pick a partner with at least one sale order so the RMA can bind."""
so = env['sale.order'].search([('state', 'in', ('sale', 'done'))], limit=1)
if not so:
raise RuntimeError('No confirmed sale.order found seed one first.')
raise RuntimeError('No confirmed sale.order found - seed one first.')
return so.partner_id, so
@@ -46,12 +46,12 @@ def smoke():
# 2. Authorise
rma.action_authorise()
assert rma.state == 'authorised'
print(f' ✓ Authorised state={rma.state}, qr_code present={bool(rma.qr_code)}')
print(f' ✓ Authorised - state={rma.state}, qr_code present={bool(rma.qr_code)}')
# 3. Mark shipped
rma.action_mark_shipped_to_us()
assert rma.state == 'shipped_to_us'
print(f' ✓ Customer shipped state={rma.state}')
print(f' ✓ Customer shipped - state={rma.state}')
# 4. Auto-receive via fp.receiving create. The RMA-link create-hook
# walks the receiving from draft → counted → staged → closed in one
@@ -80,12 +80,12 @@ def smoke():
rma.resolution_type = 'rework'
rma.action_triage_complete()
assert rma.state == 'triaged'
print(f' ✓ Triage complete state={rma.state}, resolution=rework')
print(f' ✓ Triage complete - state={rma.state}, resolution=rework')
# 7. Start resolving
rma.action_start_resolving()
assert rma.state == 'resolving'
print(f' ✓ Resolving state={rma.state}')
print(f' ✓ Resolving - state={rma.state}')
# 8. NCR walk: open → containment → root cause → close
ncr.action_open()
@@ -95,7 +95,7 @@ def smoke():
ncr.disposition = 'rework'
ncr.root_cause = '<p>Smoke-test: rack contact loss during transit.</p>'
# 9. Spawn CAPA from NCR (uses severity gate high passes)
# 9. Spawn CAPA from NCR (uses severity gate - high passes)
spawn_action = ncr.action_spawn_capa()
capa = e['fusion.plating.capa'].browse(spawn_action.get('res_id'))
print(f' ✓ Spawned CAPA {capa.name} from NCR')
@@ -109,35 +109,35 @@ def smoke():
capa.action_start_verification()
capa.effectiveness_notes = '<p>Smoke-test: 30 days no recurrence.</p>'
capa.action_mark_effective()
print(f' ✓ CAPA marked effective state={capa.state}')
print(f' ✓ CAPA marked effective - state={capa.state}')
assert capa.state == 'effective'
# 11. Close NCR
ncr.action_close()
assert ncr.state == 'closed'
print(f' ✓ NCR closed state={ncr.state}')
print(f' ✓ NCR closed - state={ncr.state}')
# 11b. Release the auto-spawned Hold (rework path) so the RMA close
# gate doesn't block. action_close on RMA refuses if any Hold is
# still on_hold or under_review.
hold.action_send_to_rework()
print(f' ✓ Hold sent to rework state={hold.state}')
print(f' ✓ Hold sent to rework - state={hold.state}')
# 12. Resolve RMA (will spawn replacement job for rework)
rma.action_resolve()
print(f' ✓ RMA resolved state={rma.state}, replacement_job={rma.replacement_job_id.name if rma.replacement_job_id else None}')
print(f' ✓ RMA resolved - state={rma.state}, replacement_job={rma.replacement_job_id.name if rma.replacement_job_id else None}')
assert rma.state == 'resolved'
# 13. Close RMA
rma.action_close()
assert rma.state == 'closed'
print(f' ✓ RMA closed state={rma.state}')
print(f' ✓ RMA closed - state={rma.state}')
# 14. Stage_id sync sanity
print(f' ✓ NCR stage_id={ncr.stage_id.name if ncr.stage_id else "(none)"}')
print(f' ✓ RMA stage_id={rma.stage_id.name if rma.stage_id else "(none)"}')
# 15. Counts smoke (read directly controller needs http context).
# 15. Counts smoke (read directly - controller needs http context).
open_holds = e['fusion.plating.quality.hold'].search_count([
('state', 'in', ('on_hold', 'under_review')),
])