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,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

View File

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

View File

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

View File

@@ -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()

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()

View File

@@ -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()

View File

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