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,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc. — License OPL-1
|
||||
"""Plan task P2.3 — fp.job.active_step_id compute."""
|
||||
# Copyright 2026 Nexa Systems Inc. - License OPL-1
|
||||
"""Plan task P2.3 - fp.job.active_step_id compute."""
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc. — License OPL-1
|
||||
"""Plan task P2.4 — _cron_autopause_stale_steps method."""
|
||||
# Copyright 2026 Nexa Systems Inc. - License OPL-1
|
||||
"""Plan task P2.4 - _cron_autopause_stale_steps method."""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc. — License OPL-1
|
||||
# Copyright 2026 Nexa Systems Inc. - License OPL-1
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install', 'fp_jobs')
|
||||
class TestBlockerCompute(TransactionCase):
|
||||
"""fp.job.step.blocker_kind / blocker_reason / blocker_jump_target_*
|
||||
— Gate visualizer source of truth for the OWL GateViz component.
|
||||
- Gate visualizer source of truth for the OWL GateViz component.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc. — License OPL-1
|
||||
# Copyright 2026 Nexa Systems Inc. - License OPL-1
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install', 'fp_jobs')
|
||||
class TestDisplayWoName(TransactionCase):
|
||||
"""fp.job.display_wo_name — Tablet/dashboard formatter."""
|
||||
"""fp.job.display_wo_name - Tablet/dashboard formatter."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
@@ -287,7 +287,7 @@ class TestSoConfirmHook(TransactionCase):
|
||||
return so
|
||||
|
||||
def test_so_confirm_creates_job(self):
|
||||
# Need a plating line — add x_fc_part_catalog_id if available
|
||||
# Need a plating line - add x_fc_part_catalog_id if available
|
||||
if 'x_fc_part_catalog_id' in self.env['sale.order.line']._fields:
|
||||
partner_for_part = self.env['res.partner'].create({'name': 'PartOwner'})
|
||||
part = self.env['fp.part.catalog'].create({
|
||||
@@ -325,11 +325,11 @@ class TestSoConfirmHook(TransactionCase):
|
||||
|
||||
def test_so_confirm_splits_by_thickness(self):
|
||||
"""Two lines with same recipe+part+coating but DIFFERENT thicknesses
|
||||
must produce TWO fp.jobs — silent merge was a compliance bug (the
|
||||
must produce TWO fp.jobs - silent merge was a compliance bug (the
|
||||
second thickness's CoC would carry the first thickness).
|
||||
|
||||
The bug only manifests when lines hit the `if recipe:` branch in
|
||||
_fp_auto_create_job — without a resolved recipe, the no_recipe
|
||||
_fp_auto_create_job - without a resolved recipe, the no_recipe
|
||||
branch already splits per line. We seed a recipe via
|
||||
part.default_process_id so both lines resolve to the same recipe
|
||||
and reach the buggy grouping path.
|
||||
@@ -351,7 +351,7 @@ class TestSoConfirmHook(TransactionCase):
|
||||
self.skipTest('need >= 2 fp.coating.thickness records seeded')
|
||||
thick_a, thick_b = thicknesses[0], thicknesses[1]
|
||||
|
||||
# Any existing top-level recipe works — the test only needs both
|
||||
# Any existing top-level recipe works - the test only needs both
|
||||
# lines to resolve to the SAME recipe so they collide on the key.
|
||||
recipe = Node.search([('parent_id', '=', False)], limit=1)
|
||||
if not recipe:
|
||||
@@ -451,7 +451,7 @@ class TestJobLifecycleHooks(TransactionCase):
|
||||
|
||||
|
||||
class TestPhase3Refactors(TransactionCase):
|
||||
"""Phase 3 — verify parallel job/step links exist on the dependent
|
||||
"""Phase 3 - verify parallel job/step links exist on the dependent
|
||||
modules' models. Field-presence is enough; the migration logic is
|
||||
Phase 9's concern."""
|
||||
|
||||
@@ -531,11 +531,11 @@ class TestPhase3Refactors(TransactionCase):
|
||||
|
||||
|
||||
class TestPhase4Refactors(TransactionCase):
|
||||
"""Phase 4 — light refactors batch B (notifications, KPI source tag).
|
||||
"""Phase 4 - light refactors batch B (notifications, KPI source tag).
|
||||
|
||||
Configurator integration is already covered by Task 2.5's SO confirm
|
||||
hook (which reads x_fc_part_catalog_id / x_fc_coating_config_id from
|
||||
sale.order.line — see TestSoConfirmHook above).
|
||||
sale.order.line - see TestSoConfirmHook above).
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
@@ -560,7 +560,7 @@ class TestPhase4Refactors(TransactionCase):
|
||||
self.assertIn('job_complete', triggers)
|
||||
|
||||
def test_action_confirm_calls_fire_notification(self):
|
||||
# Smoke test — creates a job, confirms it, verifies no exception
|
||||
# Smoke test - creates a job, confirms it, verifies no exception
|
||||
# thrown by the notification path even when no templates exist.
|
||||
job = self.env['fp.job'].create({
|
||||
'partner_id': self.partner.id,
|
||||
@@ -643,7 +643,7 @@ class TestPhase6Controllers(TransactionCase):
|
||||
|
||||
|
||||
class TestFpJobSmartButtons(TransactionCase):
|
||||
"""Feature A — verify smart-button count fields and action methods
|
||||
"""Feature A - verify smart-button count fields and action methods
|
||||
are wired on fp.job. Runtime-detect tests confirm the methods exist
|
||||
without requiring downstream models to be installed."""
|
||||
|
||||
@@ -690,7 +690,7 @@ class TestFpJobSmartButtons(TransactionCase):
|
||||
|
||||
|
||||
class TestPhase7Migration(TransactionCase):
|
||||
"""Phase 7 — verify the migration script idempotency-key fields are
|
||||
"""Phase 7 - verify the migration script idempotency-key fields are
|
||||
in place and the script files are present + parse as valid Python.
|
||||
|
||||
We cannot run the migration end-to-end in a unit test (it would need
|
||||
@@ -703,7 +703,7 @@ class TestPhase7Migration(TransactionCase):
|
||||
'legacy_mrp_production_id',
|
||||
self.env['fp.job']._fields,
|
||||
)
|
||||
# Should be Integer (we store the raw db id, not a Many2one — the
|
||||
# Should be Integer (we store the raw db id, not a Many2one - the
|
||||
# source MO may be archived later without breaking the link).
|
||||
self.assertEqual(
|
||||
self.env['fp.job']._fields['legacy_mrp_production_id'].type,
|
||||
@@ -894,7 +894,7 @@ class TestContractReviewStepRouting(TransactionCase):
|
||||
)
|
||||
|
||||
def test_button_start_routes_cr_step_to_qa005(self):
|
||||
"""Sub 12e v4 UX — clicking Start on a contract_review step
|
||||
"""Sub 12e v4 UX - clicking Start on a contract_review step
|
||||
should set state=in_progress AND immediately return the QA-005
|
||||
action so the operator lands on the form without needing to
|
||||
click Finish & Next."""
|
||||
@@ -916,7 +916,7 @@ class TestContractReviewStepRouting(TransactionCase):
|
||||
|
||||
def test_button_start_does_not_route_when_review_complete(self):
|
||||
"""If the QA-005 review is already complete, button_start
|
||||
on the CR step should NOT redirect — operator just starts
|
||||
on the CR step should NOT redirect - operator just starts
|
||||
the step normally and clicks Finish & Next when ready."""
|
||||
review = self.env['fp.contract.review'].create({
|
||||
'part_id': self.part.id,
|
||||
@@ -936,7 +936,7 @@ class TestContractReviewStepRouting(TransactionCase):
|
||||
|
||||
|
||||
class TestSequentialEnforcement(TransactionCase):
|
||||
"""Sub 13 — recipe-level + per-step sequential enforcement.
|
||||
"""Sub 13 - recipe-level + per-step sequential enforcement.
|
||||
|
||||
Decision matrix being verified:
|
||||
recipe.enforce_sequential | step.parallel_start | step.req_pred (legacy) | block?
|
||||
@@ -999,7 +999,7 @@ class TestSequentialEnforcement(TransactionCase):
|
||||
'state': 'ready',
|
||||
}))
|
||||
# job.enforce_sequential is a related from recipe.enforce_sequential
|
||||
# — invalidate to force re-read after the fact-of-life writes above.
|
||||
# - invalidate to force re-read after the fact-of-life writes above.
|
||||
job.invalidate_recordset(['enforce_sequential'])
|
||||
return job, steps
|
||||
|
||||
@@ -1009,7 +1009,7 @@ class TestSequentialEnforcement(TransactionCase):
|
||||
recipe, nodes = self._build_recipe(enforce_sequential=True)
|
||||
job, steps = self._build_job(recipe, nodes)
|
||||
a, b, c = steps
|
||||
# Start A first — should succeed
|
||||
# Start A first - should succeed
|
||||
a.button_start()
|
||||
self.assertEqual(a.state, 'in_progress')
|
||||
# Now try to start C while A is still in_progress
|
||||
@@ -1054,7 +1054,7 @@ class TestSequentialEnforcement(TransactionCase):
|
||||
# B is still blocked (default behaviour)
|
||||
with self.assertRaises(self._UserError):
|
||||
b.button_start()
|
||||
# C is parallel — should start fine while A is in_progress
|
||||
# C is parallel - should start fine while A is in_progress
|
||||
c.button_start()
|
||||
self.assertEqual(c.state, 'in_progress')
|
||||
|
||||
@@ -1064,7 +1064,7 @@ class TestSequentialEnforcement(TransactionCase):
|
||||
recipe, nodes = self._build_recipe(enforce_sequential=False)
|
||||
job, steps = self._build_job(recipe, nodes)
|
||||
a, b, c = steps
|
||||
# All three startable in any order — no enforcement
|
||||
# All three startable in any order - no enforcement
|
||||
c.button_start()
|
||||
a.button_start()
|
||||
b.button_start()
|
||||
@@ -1099,9 +1099,9 @@ class TestSequentialEnforcement(TransactionCase):
|
||||
recipe, nodes = self._build_recipe(enforce_sequential=True)
|
||||
job, steps = self._build_job(recipe, nodes)
|
||||
a, b, c = steps
|
||||
# All ready — only first step can start
|
||||
# All ready - only first step can start
|
||||
steps.invalidate_recordset(['can_start'])
|
||||
self.assertTrue(a.can_start, 'First step has no predecessor — should be startable')
|
||||
self.assertTrue(a.can_start, 'First step has no predecessor - should be startable')
|
||||
self.assertFalse(b.can_start, 'Step B blocked by Step A (ready, not done)')
|
||||
self.assertFalse(c.can_start, 'Step C blocked by Step A')
|
||||
# After A finishes, B becomes startable (C still blocked by B)
|
||||
|
||||
@@ -291,7 +291,7 @@ class TestMilestoneCascade(TransactionCase):
|
||||
def test_mark_delivered_bypass_skips_cert_gate(self):
|
||||
"""With fp_skip_cert_gate=True the gate doesn't raise. Downstream
|
||||
super() chain (notifications, invoicing) may still raise for
|
||||
their own reasons — out of scope for this test."""
|
||||
their own reasons - out of scope for this test."""
|
||||
part = self._make_part(certificate_requirement='coc')
|
||||
job = self._make_job(part_catalog_id=part.id)
|
||||
job.state = 'done'
|
||||
@@ -311,7 +311,7 @@ class TestMilestoneCascade(TransactionCase):
|
||||
|
||||
def test_mark_delivered_passes_when_cert_issued(self):
|
||||
"""Issuing the cert clears the gate. Downstream chain errors
|
||||
are accepted (delivery PDF render etc. — see test above)."""
|
||||
are accepted (delivery PDF render etc. - see test above)."""
|
||||
part = self._make_part(certificate_requirement='coc')
|
||||
job = self._make_job(part_catalog_id=part.id)
|
||||
job.state = 'done'
|
||||
@@ -404,7 +404,7 @@ class TestQtyGate(TransactionCase):
|
||||
self.assertEqual(step1.state, 'done')
|
||||
|
||||
def test_button_finish_allows_last_step_with_qty(self):
|
||||
"""Last runnable step is exempt — parts complete in place."""
|
||||
"""Last runnable step is exempt - parts complete in place."""
|
||||
from odoo import fields
|
||||
job = self._make_job(qty=5)
|
||||
last = self._make_step(
|
||||
@@ -592,7 +592,7 @@ class TestQtyGate(TransactionCase):
|
||||
|
||||
|
||||
class TestCertCreationAndGates(TransactionCase):
|
||||
"""2026-05-18 — cert creation bug fix + gate hardening.
|
||||
"""2026-05-18 - cert creation bug fix + gate hardening.
|
||||
|
||||
Covers the fixes for the WO-30040 incident where
|
||||
_fp_create_certificates raised NameError on `coating` and the cert
|
||||
@@ -767,7 +767,7 @@ class TestCertCreationAndGates(TransactionCase):
|
||||
|
||||
|
||||
class TestReceivingGate(TransactionCase):
|
||||
"""2026-05-18 — Hard gate on button_start / button_finish blocking
|
||||
"""2026-05-18 - Hard gate on button_start / button_finish blocking
|
||||
step transitions until SO receiving status = 'received'. Contract
|
||||
Review steps are exempt; manager bypass via context flag
|
||||
`fp_skip_receiving_gate=True`. See
|
||||
@@ -829,7 +829,7 @@ class TestReceivingGate(TransactionCase):
|
||||
job, step = self._make_job_with_step(
|
||||
recv_status='not_received', is_cr=True,
|
||||
)
|
||||
# button_start may return an action (CR auto-open) — must not raise.
|
||||
# button_start may return an action (CR auto-open) - must not raise.
|
||||
try:
|
||||
step.button_start()
|
||||
except Exception as e:
|
||||
@@ -837,7 +837,7 @@ class TestReceivingGate(TransactionCase):
|
||||
if isinstance(e, UserError) and 'parts not received' in str(e).lower():
|
||||
self.fail('CR step should be exempt from receiving gate')
|
||||
# Other failures (e.g. CR auto-open quirks in test env) are
|
||||
# not the gate — accept them.
|
||||
# not the gate - accept them.
|
||||
|
||||
def test_start_bypass_via_context(self):
|
||||
job, step = self._make_job_with_step(recv_status='not_received')
|
||||
@@ -883,7 +883,7 @@ class TestReceivingGate(TransactionCase):
|
||||
|
||||
|
||||
class TestCreateDeliveryShippingMirror(TransactionCase):
|
||||
"""Phase A — _fp_create_delivery mirrors shipping fields from the
|
||||
"""Phase A - _fp_create_delivery mirrors shipping fields from the
|
||||
linked receiving onto the auto-created fp.delivery."""
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc. — License OPL-1
|
||||
"""Plan task P2.2 — fp.job.late_risk_ratio compute."""
|
||||
# Copyright 2026 Nexa Systems Inc. - License OPL-1
|
||||
"""Plan task P2.2 - fp.job.late_risk_ratio compute."""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
"""Order-level ship-readiness gate (spec D4 — ship together).
|
||||
"""Order-level ship-readiness gate (spec D4 - ship together).
|
||||
|
||||
Spec: docs/superpowers/specs/2026-05-29-technician-receiving-shipping-tablet-design.md
|
||||
"""
|
||||
|
||||
@@ -23,7 +23,7 @@ class TestPostShopAdvance(TransactionCase):
|
||||
vals.update(kw)
|
||||
return self.env['fp.job'].create(vals)
|
||||
|
||||
# ===== Task 2 — _fp_check_advance_post_shop helper ==================
|
||||
# ===== Task 2 - _fp_check_advance_post_shop helper ==================
|
||||
|
||||
def test_advance_helper_exists(self):
|
||||
job = self._make_job()
|
||||
@@ -36,13 +36,13 @@ class TestPostShopAdvance(TransactionCase):
|
||||
self.assertEqual(job.state, 'confirmed')
|
||||
|
||||
def test_advance_noop_when_no_steps(self):
|
||||
# job with zero steps stays put — nothing to evaluate
|
||||
# job with zero steps stays put - nothing to evaluate
|
||||
job = self._make_job(state='in_progress')
|
||||
self.assertFalse(job.step_ids)
|
||||
job._fp_check_advance_post_shop()
|
||||
self.assertEqual(job.state, 'in_progress')
|
||||
|
||||
# ===== Task 3 — cert-issue + cert-void helpers =====================
|
||||
# ===== Task 3 - cert-issue + cert-void helpers =====================
|
||||
|
||||
def test_advance_after_cert_issue_helper_exists(self):
|
||||
job = self._make_job()
|
||||
@@ -58,7 +58,7 @@ class TestPostShopAdvance(TransactionCase):
|
||||
job._fp_check_advance_after_cert_issue()
|
||||
self.assertEqual(job.state, 'draft')
|
||||
|
||||
# ===== Task 4 — button_finish gates + auto-advance =================
|
||||
# ===== Task 4 - button_finish gates + auto-advance =================
|
||||
|
||||
def test_button_finish_on_last_step_triggers_advance(self):
|
||||
"""Finishing the only step of an in_progress job flips state
|
||||
@@ -75,7 +75,7 @@ class TestPostShopAdvance(TransactionCase):
|
||||
step.button_finish()
|
||||
self.assertEqual(job.state, 'awaiting_ship')
|
||||
|
||||
# ===== Task 5 — button_mark_shipped ================================
|
||||
# ===== Task 5 - button_mark_shipped ================================
|
||||
|
||||
def test_button_mark_shipped_requires_awaiting_ship(self):
|
||||
from odoo.exceptions import UserError
|
||||
@@ -89,7 +89,7 @@ class TestPostShopAdvance(TransactionCase):
|
||||
self.assertEqual(job.state, 'done')
|
||||
self.assertTrue(job.date_finished)
|
||||
|
||||
# ===== Task 20 — activity helpers ==================================
|
||||
# ===== Task 20 - activity helpers ==================================
|
||||
|
||||
def test_schedule_cert_activity_helper_exists(self):
|
||||
job = self._make_job()
|
||||
|
||||
@@ -61,7 +61,7 @@ class TestQtyReceivedPropagation(TransactionCase):
|
||||
# after the 2026-05-20 `staged` retirement).
|
||||
recv.action_mark_counted()
|
||||
recv.action_close()
|
||||
# Reload — the hook fires inside _update_so_receiving_status.
|
||||
# Reload - the hook fires inside _update_so_receiving_status.
|
||||
job.invalidate_recordset(['qty_received'])
|
||||
self.assertEqual(job.qty_received, 5)
|
||||
|
||||
@@ -75,7 +75,7 @@ class TestQtyReceivedPropagation(TransactionCase):
|
||||
|
||||
def test_no_job_match_is_silent(self):
|
||||
"""If the receiving line's part doesn't match any job, skip
|
||||
without raising — common for receivings without spawned jobs."""
|
||||
without raising - common for receivings without spawned jobs."""
|
||||
# Build a receiving with a part that no job uses.
|
||||
other_part = self.env['fp.part.catalog'].create({
|
||||
'name': 'Orphan',
|
||||
@@ -143,7 +143,7 @@ class TestQtyReceivedPropagation(TransactionCase):
|
||||
self.assertEqual(job_b.qty_received, 7)
|
||||
|
||||
def test_idempotent_under_repeated_writes(self):
|
||||
"""Hook is safe to call multiple times — value just settles."""
|
||||
"""Hook is safe to call multiple times - value just settles."""
|
||||
so, job = self._make_so_with_job()
|
||||
recv = self._make_receiving(so, received_qty=5)
|
||||
recv.action_mark_counted()
|
||||
|
||||
@@ -145,7 +145,7 @@ class TestRecipeCertSuppression(TransactionCase):
|
||||
# ---- Test 7: passivation recipe also suppresses the ISSUE-TIME gate ----
|
||||
def test_passivation_recipe_suppresses_thickness_issue_gate(self):
|
||||
"""A passivation recipe (requires_thickness_report=False) must drop
|
||||
the thickness-data requirement at issue time too — even for a
|
||||
the thickness-data requirement at issue time too - even for a
|
||||
strict-thickness customer. Regression: the recipe-suppression
|
||||
feature updated _resolve_required_cert_types but NOT the
|
||||
action_issue / wizard thickness gate, so passivation CoCs could
|
||||
|
||||
Reference in New Issue
Block a user