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

@@ -5,7 +5,7 @@
# Phase 2 of the native plating job model migration. Models are added
# task-by-task in Tasks 2.2 onwards.
from . import fp_job_workflow_state # Sub 14 must load before fp_job (FK target)
from . import fp_job_workflow_state # Sub 14 - must load before fp_job (FK target)
from . import fp_job
from . import fp_job_sticker
from . import fp_job_step
@@ -17,7 +17,7 @@ from . import sale_order
from . import sale_order_line
from . import res_users
# Phase 3 parallel job/step links on dependent modules' models.
# Phase 3 - parallel job/step links on dependent modules' models.
from . import fp_batch
from . import fp_quality_hold
from . import fp_certificate
@@ -26,19 +26,19 @@ from . import fp_delivery
from . import fp_racking_inspection
from . import fp_receiving
# Phase 4 light refactors batch B (notifications, KPI source tag).
# Phase 4 - light refactors batch B (notifications, KPI source tag).
from . import fp_notification_trigger
from . import fusion_plating_kpi_value
# Phase 5 Job Margin report.
# Phase 5 - Job Margin report.
from . import report_fp_job_margin
# Phase 1 of MRP cut-out (Sub 11) relocated from fusion_plating_bridge_mrp.
# Phase 1 of MRP cut-out (Sub 11) - relocated from fusion_plating_bridge_mrp.
# (fp.qc.checklist.template lives in fusion_plating_quality; can't depend
# back on jobs without a cycle.)
from . import fp_job_consumption
# Multi-rack splitting at Racking (Phase 1) jobs-side extension of
# Multi-rack splitting at Racking (Phase 1) - jobs-side extension of
# fp.rack.load (core model in fusion_plating) + fp.job rollups.
from . import fp_job_rack
# fp.work.role, fp.operator.proficiency, fp_process_node inherit, and the

View File

@@ -5,7 +5,7 @@
1. Block direct creation of out_invoice / out_refund for ALL users
including administrators. The only legal entry points are:
* sale.order._create_invoices() sets context fp_from_so_invoice=True
* sale.order._create_invoices() - sets context fp_from_so_invoice=True
* manual create() with invoice_origin matching an existing sale.order.name
2. Once a customer move is created via a legitimate path, derive its
@@ -98,7 +98,7 @@ class AccountMove(models.Model):
# it doesn't survive the copy. Allow reversals through as long
# as the reversed entry is itself a customer-facing move (which
# means it already went through this validator at original
# creation time the audit trail is intact).
# creation time - the audit trail is intact).
reversed_id = vals.get('reversed_entry_id')
if reversed_id:
parent = self.env['account.move'].sudo().browse(reversed_id)
@@ -139,7 +139,7 @@ class AccountMove(models.Model):
portal = job.portal_job_id
if 'invoice_ref' in portal._fields:
portal.invoice_ref = self.name
# Recompute state via the central helper it'll only land on
# Recompute state via the central helper - it'll only land on
# 'complete' if the WO is actually done AND the shipment is
# delivered. Posting an invoice early no longer skips the floor.
if hasattr(portal, '_fp_recompute_portal_state'):

View File

@@ -2,7 +2,7 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
#
# Phase 3 parallel job/step links on fusion.plating.batch.
# Phase 3 - parallel job/step links on fusion.plating.batch.
# The legacy workorder_id link to mrp.workorder stays in place.
from odoo import fields, models

View File

@@ -2,13 +2,13 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
#
# Phase 3 parallel job link on fp.certificate.
# Phase 3 - parallel job link on fp.certificate.
# Coexists with bridge_mrp's production_id link.
#
# v19.0.6.20.0 surface the Fischerscope PDF on the cert form so
# v19.0.6.20.0 - surface the Fischerscope PDF on the cert form so
# operators can SEE that the thickness report will be (or has been)
# merged into the CoC. The merge logic itself lives in
# fusion_plating_certificates/models/fp_certificate.py this file
# fusion_plating_certificates/models/fp_certificate.py - this file
# only adds the human-readable indicators.
from odoo import api, fields, models
@@ -74,7 +74,7 @@ class FpCertificate(models.Model):
else:
status = 'pending'
elif QC is not None and rec.x_fc_job_id:
# Same lookup the merge method uses passed-first,
# Same lookup the merge method uses - passed-first,
# then any QC with a PDF.
qc = QC.sudo().search([
('job_id', '=', rec.x_fc_job_id.id),
@@ -97,7 +97,7 @@ class FpCertificate(models.Model):
rec.x_fc_thickness_status = status
def action_view_thickness_qc(self):
"""Smart-button target open the linked QC for inspection."""
"""Smart-button target - open the linked QC for inspection."""
self.ensure_one()
if not self.x_fc_thickness_qc_id:
return False
@@ -111,7 +111,7 @@ class FpCertificate(models.Model):
}
def action_open_job(self):
"""Smart-button target open the linked plating job."""
"""Smart-button target - open the linked plating job."""
self.ensure_one()
if not self.x_fc_job_id:
return False
@@ -127,7 +127,7 @@ class FpCertificate(models.Model):
# ---- Parse-on-upload for the cert-form Fischerscope field (2026-05-28)
# The Issue Certs wizard parses .doc/.docx/RTF Fischerscope exports into
# readings + metadata + microscope image. Dropping the same file straight
# onto the cert form's x_fc_local_thickness_pdf field did nothing it
# onto the cert form's x_fc_local_thickness_pdf field did nothing - it
# just stored the bytes. These hooks give the form the SAME behaviour as
# the wizard: on save, a non-PDF upload is parsed and relocated to the
# evidence field (a real PDF is left in place to merge as page 2).
@@ -157,7 +157,7 @@ class FpCertificate(models.Model):
then relocate the non-PDF source to x_fc_local_thickness_evidence_id
and clear the PDF field (so the page-2 merge doesn't choke on it).
A real PDF is left in place it merges as page 2 of the CoC on
A real PDF is left in place - it merges as page 2 of the CoC on
Issue and carries no parseable readings. Unknown non-PDF types are
left untouched.
"""
@@ -178,7 +178,7 @@ class FpCertificate(models.Model):
is_rtf = raw[:5] == b'{\\rtf'
is_docx = name.endswith('.docx')
if not (is_rtf or is_docx):
return # unknown non-PDF don't guess
return # unknown non-PDF - don't guess
from ..wizards.fp_cert_issue_wizard import (
_fp_parse_fischerscope_rtf, _fp_parse_fischerscope_docx,
@@ -219,7 +219,7 @@ class FpCertificate(models.Model):
except ValueError:
continue
# Readings replace any existing set with the freshly-parsed rows
# Readings - replace any existing set with the freshly-parsed rows
# (the uploaded report is authoritative for this cert).
readings = parsed.get('readings') or []
Reading = self.env.get('fp.thickness.reading')
@@ -249,7 +249,7 @@ class FpCertificate(models.Model):
vals['x_fc_local_thickness_pdf'] = False
vals['x_fc_local_thickness_pdf_filename'] = False
# Microscope image (RTF only .docx images need a different path).
# Microscope image (RTF only - .docx images need a different path).
if is_rtf and 'x_fc_thickness_image_id' in self._fields:
try:
pngs = _fp_extract_rtf_images(raw)

View File

@@ -2,7 +2,7 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
#
# Phase 3 parallel job link on fusion.plating.delivery.
# Phase 3 - parallel job link on fusion.plating.delivery.
# Coexists with the legacy job_ref Char.
from odoo import fields, models

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
#
# Phase 1 (Sub 11) relocated from fusion_plating_bridge_mrp.
# Phase 1 (Sub 11) - relocated from fusion_plating_bridge_mrp.
# MRP-flavoured fields (production_id, workorder_id) replaced by their
# native fp.job / fp.job.step equivalents.
@@ -14,14 +14,14 @@ class FpJobConsumption(models.Model):
"""A single consumable drawdown charged to a plating job.
Sources include bath replenishment applied against a job, masking tape
rolls, PPE, nickel salts anything that has a cost and should roll
rolls, PPE, nickel salts - anything that has a cost and should roll
into job costing.
Kept deliberately lightweight: one row per event, cost derived from
`product.standard_price` at log time (snapshot, not reactive).
"""
_name = 'fp.job.consumption'
_description = 'Fusion Plating Job Consumption'
_description = 'Fusion Plating - Job Consumption'
_order = 'logged_date desc, id desc'
job_id = fields.Many2one(

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
"""Masking reference attachments captured at Express order entry, surfaced
"""Masking reference attachments - captured at Express order entry, surfaced
on the job's masking step (operator workstation) and rolled up to the job
form (office). Populated by sale.order.line._fp_apply_express_overrides_to_job.
"""

View File

@@ -2,7 +2,7 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
#
# fp.job.node.override per-job opt-in/out decisions for opt_in/opt_out
# fp.job.node.override - per-job opt-in/out decisions for opt_in/opt_out
# recipe nodes. Mirrors fusion.plating.job.node.override from bridge_mrp,
# but bound to fp.job instead of mrp.production.
#

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
#
# Multi-rack splitting at Racking Phase 1 jobs-module extension.
# Multi-rack splitting at Racking - Phase 1 jobs-module extension.
# Core models live in fusion_plating/models/fp_rack_load.py. This file owns
# everything that touches jobs-module fields (fp.job.step.area_kind,
# fp.job.part_catalog_id) and the racking-step detection (_fp_is_racking_step).
@@ -30,7 +30,7 @@ class FpRackLoad(models.Model):
@api.model
def _fp_racking_step_for(self, job):
# Detect the racking step by area_kind == 'racking' (the corrected
# classification), NOT _fp_is_racking_step() the latter keys off the
# classification), NOT _fp_is_racking_step() - the latter keys off the
# step's kind, and de-racking steps are frequently mis-tagged
# kind='racking' in the data, which would wrongly match De-Racking.
return job.step_ids.filtered(lambda s: s.area_kind == 'racking')[:1]

View File

@@ -17,7 +17,7 @@ from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
# 2026-05-24 Shop Floor live-step fix (19.0.10.24.0):
# 2026-05-24 - Shop Floor live-step fix (19.0.10.24.0):
# The legacy `_STEP_KIND_TO_AREA` dict that lived here was removed.
# fp.step.kind now self-declares its area_kind, so the kind taxonomy
# IS the source of truth for Shop Floor column routing.
@@ -27,7 +27,7 @@ _logger = logging.getLogger(__name__)
class FpJobStep(models.Model):
_inherit = 'fp.job.step'
# ===== Sub 13 sequential enforcement (recipe + per-step) =============
# ===== Sub 13 - sequential enforcement (recipe + per-step) =============
# Decision matrix for whether button_start must verify predecessors:
#
# recipe.enforce_sequential | step.parallel_start | step.req_pred (legacy) | block?
@@ -56,7 +56,7 @@ class FpJobStep(models.Model):
return True
# Partial-flow short-circuit (2026-06-02 partial-order handling).
# Once REAL parts have physically arrived at this step (a move
# parked them here), the predecessor lock is moot the parts are
# parked them here), the predecessor lock is moot - the parts are
# on the floor at this station, so the step is startable
# regardless of whether upstream steps are fully done. This is
# what lets a partial group "light up" the next stage while the
@@ -68,12 +68,12 @@ class FpJobStep(models.Model):
recipe_seq = self.job_id.enforce_sequential
if recipe_seq:
return not self.parallel_start
# Free-flow recipe only the legacy per-step flag still gates.
# Free-flow recipe - only the legacy per-step flag still gates.
return bool(self.requires_predecessor_done)
def _fp_has_real_incoming(self):
"""True when real parts have physically arrived at this step via
a move an incoming move from a DIFFERENT step with qty_moved > 0.
a move - an incoming move from a DIFFERENT step with qty_moved > 0.
Distinct from the qty_at_step first-step seed (a notional UI hint
with no backing move) and from self-loop measurement moves
@@ -131,7 +131,7 @@ class FpJobStep(models.Model):
)
step.can_start = not bool(blocking)
# ===== 2026-05-23 plant-view redesign area_kind + activity =========
# ===== 2026-05-23 plant-view redesign - area_kind + activity =========
area_kind = fields.Selection(
[
('receiving', 'Receiving'),
@@ -171,22 +171,22 @@ class FpJobStep(models.Model):
Priority chain (non-gating steps):
1. step-NAME override for unambiguous de-rack / de-mask / bake
steps (2026-06-03) their recipe kind and/or work-centre is
steps (2026-06-03) - their recipe kind and/or work-centre is
frequently wrong (tagged 'racking'/'mask', a shared station, or
left blank), scattering cards across the Racking / Masking /
Plating columns. The operator-facing NAME is unambiguous, so it
wins OUTRIGHT even over an explicit work-centre. Bake/oven
wins OUTRIGHT - even over an explicit work-centre. Bake/oven
steps that merely mention "de-rack" stay in Baking. See spec
2026-05-24-shopfloor-live-step-fix-design.md Change 6.
2. work_centre.area_kind (explicit operator setup)
3. recipe_node.kind_id.area_kind (kind taxonomy authoritative)
4. catch-all 'plating' (data integrity issue if we land here)
Gating/marker steps (kind `code == 'gating'` the "Ready for X"
Gating/marker steps (kind `code == 'gating'` - the "Ready for X"
steps) have NO physical location; the taxonomy maps them to
'receiving', which made a mid-recipe gate snap the job's card back
to the first column (Racking -> "Ready for processing" jumped to
Receiving, so the job looked like it vanished 2026-06-02). A
Receiving, so the job looked like it vanished - 2026-06-02). A
gating step FALLS FORWARD to the next non-gating step's column
(it's "ready for [that stage]"), keeping the card moving
left->right. If nothing real follows, it falls back to the last
@@ -196,7 +196,7 @@ class FpJobStep(models.Model):
step.area_kind = step._fp_resolve_area_kind()
def _fp_raw_area_kind(self):
"""Area from this step's OWN name / work_centre / kind only no
"""Area from this step's OWN name / work_centre / kind only - no
look-ahead and no dependence on the computed `area_kind` field (so
the gating fall-forward below can't recurse).
@@ -261,7 +261,7 @@ class FpJobStep(models.Model):
x = (name or '').strip().lower()
if not x:
return None
# bake / oven first a "post de-rack" oven bake IS a bake
# bake / oven first - a "post de-rack" oven bake IS a bake
if 'oven' in x or 'bake' in x:
if any(w in x for w in (
'processing', 'inspect', 'check', 'qc',
@@ -306,7 +306,7 @@ class FpJobStep(models.Model):
_logger.debug("last_activity_at stamp on message_post failed: %s", exc)
return res
# Gate visualizer drives the OWL GateViz component on the tablet.
# Gate visualizer - drives the OWL GateViz component on the tablet.
# Returns kind of blocker + human reason + optional (model, id) jump
# target. Reuses _fp_should_block_predecessors so this stays in sync
# with can_start as a single source of truth.
@@ -349,7 +349,7 @@ class FpJobStep(models.Model):
step.blocker_jump_target_id = 0
continue
# Predecessor gate same policy as _compute_can_start
# Predecessor gate - same policy as _compute_can_start
if step._fp_should_block_predecessors():
earlier_open = step.job_id.step_ids.filtered(lambda x: (
x.id != step.id
@@ -376,7 +376,7 @@ class FpJobStep(models.Model):
step.blocker_jump_target_id = 0
# ==================================================================
# Shop-Floor auto-pause cron (Phase 2 tablet redesign)
# Shop-Floor auto-pause cron (Phase 2 - tablet redesign)
# ==================================================================
@api.model
def _cron_autopause_stale_steps(self):
@@ -386,7 +386,7 @@ class FpJobStep(models.Model):
fp.shopfloor.autopause_threshold_hours (default 8.0)
Recipes can opt out per node via
fusion.plating.process.node.long_running (Phase 2 P2.1)
fusion.plating.process.node.long_running (Phase 2 - P2.1)
Fixes the 411-hour ghost timer that bit us on the original tablet
when an operator started a step and never tapped Finish. Posts an
@@ -422,7 +422,7 @@ class FpJobStep(models.Model):
paused += 1
except Exception:
_logger.exception(
"Auto-pause failed for step %s skipping", step.id,
"Auto-pause failed for step %s - skipping", step.id,
)
if paused:
_logger.info(
@@ -448,7 +448,7 @@ class FpJobStep(models.Model):
for step in self:
if step.state != 'in_progress':
raise UserError(_(
"Step '%s' is in state '%s' only in-progress steps can pause."
"Step '%s' is in state '%s' - only in-progress steps can pause."
) % (step.name, step.state))
now = fields.Datetime.now()
open_log = step.time_log_ids.filtered(lambda l: not l.date_finished)
@@ -465,7 +465,7 @@ class FpJobStep(models.Model):
for step in self:
if step.state not in ('pending', 'ready'):
raise UserError(_(
"Step '%s' is in state '%s' only pending/ready steps can be skipped."
"Step '%s' is in state '%s' - only pending/ready steps can be skipped."
) % (step.name, step.state))
step.state = 'skipped'
return True
@@ -477,7 +477,7 @@ class FpJobStep(models.Model):
for step in self:
if step.state == 'done':
raise UserError(_(
"Step '%s' is done cannot cancel."
"Step '%s' is done - cannot cancel."
) % step.name)
if step.state == 'cancelled':
raise UserError(_(
@@ -487,7 +487,7 @@ class FpJobStep(models.Model):
return True
def button_reset(self):
"""Reset a step back to 'ready' so it can be redone operator
"""Reset a step back to 'ready' so it can be redone - operator
self-serve for a mistake, an accidental skip, or a customer redo
request. Clears the finish + sign-off stamps and closes any open
timelog so the redo re-captures them; KEEPS the first-start audit
@@ -529,7 +529,7 @@ class FpJobStep(models.Model):
"""Post a chatter trail on the parent JOB whenever an active
step gets reassigned. The step itself already tracks
assigned_user_id (tracking=True) but supervisors don't open
each step's chatter they read the job. Without a job-level
each step's chatter - they read the job. Without a job-level
post the takeover is invisible.
Only fires for steps in active states (in_progress / paused)
@@ -593,7 +593,7 @@ class FpJobStep(models.Model):
@api.model
def _cron_nudge_stale_in_progress(self, threshold_hours=8):
"""Cron nudge for steps stuck in `in_progress` longer than
threshold. Default 8 hours operator started, walked away,
threshold. Default 8 hours - operator started, walked away,
timelog accumulating phantom hours.
"""
return self._cron_nudge_stale_steps(
@@ -610,7 +610,7 @@ class FpJobStep(models.Model):
Finds every fp.job.step in any of `states` with date_started
older than N hours. Schedules a 'todo' mail.activity on the
parent job for the job's manager_id (falls back to the user
who started the step). Idempotent won't double-schedule if
who started the step). Idempotent - won't double-schedule if
an open activity with the same summary already exists.
"""
from datetime import timedelta as _td
@@ -689,7 +689,7 @@ class FpJobStep(models.Model):
for step in self:
if step.state not in ('in_progress', 'paused'):
raise UserError(_(
"Step '%s' is in state '%s' only in_progress / "
"Step '%s' is in state '%s' - only in_progress / "
"paused steps can be aborted for retry."
) % (step.name, step.state))
old_tank = step.tank_id.display_name or '(no tank set)'
@@ -715,7 +715,7 @@ class FpJobStep(models.Model):
'Reason: <em>%s</em><br/>'
'Equipment: tank=%s, bath=%s%s<br/>'
'Partial work captured: %.2f min in %d timelog(s). '
'Step is back in <b>ready</b> state operator can '
'Step is back in <b>ready</b> state - operator can '
'restart when the issue is resolved.'
)) % (
step.name, self.env.user.name, reason,
@@ -725,7 +725,7 @@ class FpJobStep(models.Model):
return True
def action_recompute_duration_from_timelogs(self):
"""Manual button re-sum duration_actual + post to chatter
"""Manual button - re-sum duration_actual + post to chatter
for audit. Use case: supervisor adjusts a timelog row and
wants an explicit audit trail of the recompute. The
automatic version called from timelog hooks is
@@ -743,7 +743,7 @@ class FpJobStep(models.Model):
return True
def _fp_resum_duration_actual(self):
"""Quiet re-sum used by automatic triggers (timelog
"""Quiet re-sum - used by automatic triggers (timelog
create/write/unlink hooks). No chatter post. Skips no-op
updates so writes are minimised."""
for step in self:
@@ -753,7 +753,7 @@ class FpJobStep(models.Model):
return True
def action_finish_and_advance(self):
"""Steelhead-style "Finish & Next" finish this step then auto-
"""Steelhead-style "Finish & Next" - finish this step then auto-
start the next pending/ready step in sequence. Single click
replaces the prior Finish-then-Move-wizard dance.
@@ -765,10 +765,10 @@ class FpJobStep(models.Model):
self.ensure_one()
if self.state != 'in_progress':
raise UserError(_(
"Step '%s' is in state '%s' start it before clicking Finish."
"Step '%s' is in state '%s' - start it before clicking Finish."
) % (self.name, self.state))
# Contract Review (QA-005) routing when the recipe step is
# Contract Review (QA-005) routing - when the recipe step is
# flagged as a contract-review step, the operator should land on
# the part's QA-005 form rather than the generic measurement
# wizard. Once the review is complete or dismissed we fall
@@ -798,7 +798,7 @@ class FpJobStep(models.Model):
fp_skip_predecessor_check=True,
).button_start()
self.job_id.message_post(body=_(
'Step "%(prev)s" finished auto-started next step "%(next)s".'
'Step "%(prev)s" finished - auto-started next step "%(next)s".'
) % {'prev': self.name, 'next': next_step.name})
return True
@@ -817,31 +817,31 @@ class FpJobStep(models.Model):
parts (2026-06-02 partial-order handling).
Called by the Move controller after a bulk move commits. When the
last parts leave an in_progress step it should close itself one
last parts leave an in_progress step it should close itself - one
fewer tap for the operator. But finishing runs the full gate chain
(required inputs, sign-off, contract review, receiving, and the
post-shop close gates on the last step). If any gate isn't
satisfied we must NOT fail the move that already succeeded so we
satisfied we must NOT fail the move that already succeeded - so we
swallow the UserError and leave the step in_progress for the
operator to finish manually (the board will show it "running, 0
here", which reads as "finish me").
Fires for any step that actually moved parts OUT and drained to
zero INCLUDING the first/seeded stage (its qty comes from the
zero - INCLUDING the first/seeded stage (its qty comes from the
qty_at_step seed, not a real incoming move). Returns True if the
step finished.
"""
self.ensure_one()
if self.state != 'in_progress':
return False
# qty_at_step is a non-stored compute off the move rows force a
# qty_at_step is a non-stored compute off the move rows - force a
# re-read so we see the just-committed outgoing move.
self.invalidate_recordset(['qty_at_step'])
if self.qty_at_step != 0:
return False
# Guard: only auto-finish a step that genuinely moved parts OUT (a
# real outgoing move, excluding self-loop measurement moves). The
# earlier guard checked _fp_has_real_incoming() the WRONG
# earlier guard checked _fp_has_real_incoming() - the WRONG
# direction: the first/seeded stage (e.g. Racking) is fed by the
# qty_at_step seed, not an incoming move, so it never auto-finished
# when all its parts were sent forward. Checking for a real
@@ -853,7 +853,7 @@ class FpJobStep(models.Model):
self.button_finish()
return True
except UserError:
# Gates still pending (missing prompts / sign-off / etc.)
# Gates still pending (missing prompts / sign-off / etc.) -
# leave the step in_progress for a manual finish. The move
# itself stands.
return False
@@ -863,7 +863,7 @@ class FpJobStep(models.Model):
whose values haven't been recorded yet.
Previously this checked "any move with input values exists since
date_started" too coarse. Operator clicked Save on the dialog
date_started" - too coarse. Operator clicked Save on the dialog
after filling ONE prompt and the helper went quiet, letting
action_finish_and_advance bypass the dialog re-open even when
4 of 5 required prompts were still empty (WO-30051 / Riya 2026-05-23).
@@ -876,7 +876,7 @@ class FpJobStep(models.Model):
def _fp_missing_required_step_inputs(self):
"""Return the recordset of REQUIRED step_input prompts on this
step's recipe node that have NO value recorded across any move
from this step. Centralised helper used by both
from this step. Centralised helper - used by both
_fp_has_uncaptured_step_inputs (re-open dialog) and
_fp_check_step_inputs_complete (raise UserError on finish).
"""
@@ -888,9 +888,9 @@ class FpJobStep(models.Model):
# Master switch (Sub 12d): when the recipe node opts OUT of
# measurement collection, the Record-Inputs wizard returns ZERO
# rows (fp_job_step_input_wizard.default_get). The finish gate MUST
# agree otherwise required prompts are demanded with no way to
# agree - otherwise required prompts are demanded with no way to
# enter them and the step is permanently stuck (bake nodes with
# collect_measurements=False but required prompts WO-30098 + 63
# collect_measurements=False but required prompts - WO-30098 + 63
# others on entech). Honour the switch here so gate <=> wizard.
if ('collect_measurements' in node._fields
and not node.collect_measurements):
@@ -920,10 +920,10 @@ class FpJobStep(models.Model):
WHO finished the step as the signer-of-record. For shops that
need separate operator+supervisor sign-off, call action_signoff()
explicitly from a supervisor session BEFORE the operator clicks
Finish that pre-sets signoff_user_id and this helper becomes a
Finish - that pre-sets signoff_user_id and this helper becomes a
no-op.
Idempotent never overwrites an existing signoff_user_id, so a
Idempotent - never overwrites an existing signoff_user_id, so a
manager pre-signing via action_signoff is preserved through the
operator's Finish click.
"""
@@ -960,7 +960,7 @@ class FpJobStep(models.Model):
continue
step.job_id.message_post(body=Markup(_(
'Sign-off gate bypassed on step "<b>%s</b>" by %s. '
'Documented deviation no signer recorded.'
'Documented deviation - no signer recorded.'
)) % (step.name, self.env.user.name))
return
for step in self:
@@ -969,7 +969,7 @@ class FpJobStep(models.Model):
if step.signoff_user_id:
continue
raise UserError(_(
'Step "%(step)s" cannot be finished sign-off required '
'Step "%(step)s" cannot be finished - sign-off required '
'but no signer recorded. Click "Sign Off" on the step '
'(or have your supervisor sign before you finish). '
'Managers can override via context flag '
@@ -977,13 +977,13 @@ class FpJobStep(models.Model):
) % {'step': step.name})
def action_signoff(self):
"""Explicit sign-off action sets signoff_user_id = env.user.id
"""Explicit sign-off action - sets signoff_user_id = env.user.id
for the calling user. Use case: a supervisor reviews an operator's
work and signs off BEFORE the operator clicks Finish. Once signed,
the operator's Finish click passes the signoff gate without auto-
assigning a different signer.
Idempotent re-clicking by the same user is a no-op. A DIFFERENT
Idempotent - re-clicking by the same user is a no-op. A DIFFERENT
user re-signing overwrites the prior signer (and chatters the change)
so a senior supervisor can override a junior's premature sign-off
without leaving the audit trail mute.
@@ -991,7 +991,7 @@ class FpJobStep(models.Model):
for step in self:
if not step.requires_signoff:
raise UserError(_(
'Step "%s" does not require sign-off nothing to sign.'
'Step "%s" does not require sign-off - nothing to sign.'
) % step.name)
prior = step.signoff_user_id
if prior and prior.id == self.env.user.id:
@@ -1013,7 +1013,7 @@ class FpJobStep(models.Model):
per-step data trail; finishing a step with missing prompts breaks
the audit chain.
2026-05-24: also blocks orphaned steps (recipe_node_id NULL
2026-05-24: also blocks orphaned steps (recipe_node_id NULL -
happens when the source recipe was deleted, e.g. a per-part clone
cleanup). Without a recipe link there's no way to verify required
prompts; defaulting to "let it through" was a silent compliance
@@ -1028,19 +1028,19 @@ class FpJobStep(models.Model):
for step in self:
step.job_id.message_post(body=Markup(_(
'Required-inputs gate bypassed on step "<b>%s</b>" by %s. '
'Documented deviation review the step\'s prompts.'
'Documented deviation - review the step\'s prompts.'
)) % (step.name, self.env.user.name))
return
for step in self:
# Orphan-step block NULL recipe_node means we can't list
# Orphan-step block - NULL recipe_node means we can't list
# required prompts, so we conservatively refuse to finish.
if not step.recipe_node_id:
raise UserError(_(
'Step "%(step)s" cannot be finished this step has '
'Step "%(step)s" cannot be finished - this step has '
'no recipe link (the source recipe was deleted or the '
'job was created before recipes were assigned). '
'Required-input verification is impossible without '
'the recipe. Escalate to a manager they can bypass '
'the recipe. Escalate to a manager - they can bypass '
'with an audit-chatter entry.'
) % {'step': step.name})
missing = step._fp_missing_required_step_inputs()
@@ -1048,7 +1048,7 @@ class FpJobStep(models.Model):
continue
names = ', '.join('"%s"' % (p.name or '').strip() for p in missing)
raise UserError(_(
'Step "%(step)s" cannot be finished %(n)s required '
'Step "%(step)s" cannot be finished - %(n)s required '
'input(s) not recorded yet: %(names)s. '
'Click "Record Inputs" on the step row to enter the '
'missing values, then finish. '
@@ -1066,7 +1066,7 @@ class FpJobStep(models.Model):
Replaces the form-view-based wizard with a custom OWL Dialog
component (fp_record_inputs_dialog.js). The dialog renders
each prompt as a proper card with semantic HTML no more
each prompt as a proper card with semantic HTML - no more
list-cell-as-card CSS hacks.
When advance_after is True, the dialog's Save button commits
@@ -1084,11 +1084,11 @@ class FpJobStep(models.Model):
}
# NB: action_open_input_wizard is defined further down (line ~829)
# that one stays as the per-row "Record" button entry-point.
# - that one stays as the per-row "Record" button entry-point.
# _fp_open_input_wizard above adds the advance_after pathway used
# only by action_finish_and_advance.
# NOTE the earlier duplicate `button_finish` definition that held
# NOTE - the earlier duplicate `button_finish` definition that held
# the duration-overrun + bake.window auto-spawn logic has been merged
# into the canonical button_finish further down (line ~1130). Python
# was silently keeping only the LAST definition in this class body,
@@ -1096,16 +1096,16 @@ class FpJobStep(models.Model):
# era. Don't re-introduce a second button_finish here.
# ==================================================================
# Phase 2 multi-serial auto-promote serials on step transitions
# Phase 2 multi-serial - auto-promote serials on step transitions
# ==================================================================
def _fp_promote_serials_on_start(self):
"""When this step transitions to in_progress, lift any serial
attached to the parent SO line out of `received` / `racked` and
into `in_process`. Idempotent already-promoted serials are
into `in_process`. Idempotent - already-promoted serials are
skipped.
"""
for step in self:
# sudo() technicians lack sale.order ACL (Rule 13m).
# sudo() - technicians lack sale.order ACL (Rule 13m).
job = step.sudo().job_id
if not job.sale_order_line_ids:
continue
@@ -1124,9 +1124,9 @@ class FpJobStep(models.Model):
"""When the LAST step of this step's job finishes (sequenced
terminal step OR an explicit inspect/final-inspect kind), bump
in-flight serials to `inspected` so the shipper sees them ready
for packing. Conservative only promotes from `in_process`."""
for packing. Conservative - only promotes from `in_process`."""
for step in self:
# sudo() technicians lack sale.order ACL (Rule 13m).
# sudo() - technicians lack sale.order ACL (Rule 13m).
job = step.sudo().job_id
if not job.sale_order_line_ids:
continue
@@ -1150,7 +1150,7 @@ class FpJobStep(models.Model):
) % (step.name, self.env.user.name))
# ==================================================================
# Policy B (2026-04-28) Contract Review enforcement
# Policy B (2026-04-28) - Contract Review enforcement
# ==================================================================
# When a recipe author drops a "Contract Review" step into a recipe,
# button_start opens the QA-005 audit form for the linked part (auto-
@@ -1158,7 +1158,7 @@ class FpJobStep(models.Model):
# the form is `complete` AND the current user is on the recipe's
# contract_review_user_ids approver list (when configured).
#
# Detection case-insensitive match on the step name OR
# Detection - case-insensitive match on the step name OR
# recipe_node_id mapped from a step.template with default_kind ==
# 'contract_review' (the simple-editor library entry).
def _fp_is_contract_review_step(self):
@@ -1182,7 +1182,7 @@ class FpJobStep(models.Model):
Falls through to None when no part can be resolved (no SO line,
SO line without x_fc_part_catalog_id, etc.)."""
self.ensure_one()
# sudo() technicians lack sale.order ACL (Rule 13m).
# sudo() - technicians lack sale.order ACL (Rule 13m).
for so_line in self.sudo().job_id.sale_order_line_ids:
if (so_line.x_fc_part_catalog_id
and 'fp.contract.review' in self.env):
@@ -1199,7 +1199,7 @@ class FpJobStep(models.Model):
return None
Review = self.env.get('fp.contract.review')
if Review is None:
return None # quality module not installed skip
return None # quality module not installed - skip
review = part.x_fc_contract_review_id
if not review:
review = Review.sudo().create({
@@ -1223,7 +1223,7 @@ class FpJobStep(models.Model):
'res_id': review.id,
'view_mode': 'form',
'target': 'current',
'name': _('Contract Review %s') % (
'name': _('Contract Review - %s') % (
part.display_name or part.part_number or ''
),
}
@@ -1247,7 +1247,7 @@ class FpJobStep(models.Model):
review.state if review else _('not started')
)
raise UserError(_(
'Contract Review for %(part)s is %(state)s must be '
'Contract Review for %(part)s is %(state)s - must be '
'"complete" before this step can finish. Open the '
'QA-005 form (smart button on the part), get both '
'sections signed off, then retry. Manager bypass: '
@@ -1272,7 +1272,7 @@ class FpJobStep(models.Model):
) % ', '.join(approvers.mapped('name')))
# ==================================================================
# Sub 8 follow-up (2026-04-28) Racking Inspection enforcement
# Sub 8 follow-up (2026-04-28) - Racking Inspection enforcement
# ==================================================================
# When the recipe-side "Racking" step starts, auto-promote the linked
# fp.racking.inspection from draft → inspecting and route the operator
@@ -1325,13 +1325,13 @@ class FpJobStep(models.Model):
'res_id': ri.id,
'view_mode': 'form',
'target': 'current',
'name': _('Racking Inspection %s') % self.job_id.name,
'name': _('Racking Inspection - %s') % self.job_id.name,
}
def _fp_check_racking_inspection_complete(self):
"""Soft gate block button_finish on a Racking step until the
"""Soft gate - block button_finish on a Racking step until the
linked inspection is in a terminal state. discrepancy_flagged
counts as complete (the operator finished but flagged issues
counts as complete (the operator finished but flagged issues -
the discrepancy activity will route to the manager separately)."""
if self.env.context.get('fp_skip_racking_inspection_gate'):
return
@@ -1340,7 +1340,7 @@ class FpJobStep(models.Model):
continue
ri = step.job_id.racking_inspection_id
if not ri:
# No inspection at all still let it finish, but log a
# No inspection at all - still let it finish, but log a
# chatter warning so the manager sees the gap.
step.job_id.message_post(body=_(
'⚠️ Racking step "%s" finished without a racking '
@@ -1352,7 +1352,7 @@ class FpJobStep(models.Model):
state_label = dict(ri._fields['state'].selection).get(
ri.state, ri.state)
raise UserError(_(
'Racking inspection for %(job)s is "%(state)s" must '
'Racking inspection for %(job)s is "%(state)s" - must '
'be Done or Discrepancy Flagged before this step can '
'finish. Click the Racking Insp. smart button on the '
'job, complete the line check-off, then retry. '
@@ -1365,7 +1365,7 @@ class FpJobStep(models.Model):
def _fp_check_receiving_gate(self):
"""Block step transitions until parts are physically received.
Applied to every step EXCEPT Contract Review (paperwork doesn't
Applied to every step EXCEPT Contract Review (paperwork - doesn't
need parts on the floor). Fires from both button_start and
button_finish so an operator can't begin OR complete physical
work before the receiving record is closed.
@@ -1384,13 +1384,13 @@ class FpJobStep(models.Model):
for step in self:
if step._fp_is_contract_review_step():
continue
# sudo() technicians don't have sale.order ACL but the
# sudo() - technicians don't have sale.order ACL but the
# gate's purpose is checking a denormalized state field.
# Rule 13m: cross-module reads in tablet/floor controllers
# must sudo() the source recordset.
so = step.sudo().job_id.sale_order_id
if not so:
# Internal rework / no SO gate doesn't apply.
# Internal rework / no SO - gate doesn't apply.
continue
if 'x_fc_receiving_status' not in so._fields:
# Defensive: configurator module not installed.
@@ -1403,7 +1403,7 @@ class FpJobStep(models.Model):
so.x_fc_receiving_status or 'unknown',
)
raise UserError(_(
'Step "%(step)s" cannot proceed parts not received '
'Step "%(step)s" cannot proceed - parts not received '
'yet (SO %(so)s receiving status: %(status)s).\n\n'
'Close the receiving record (Sales > %(so)s > '
'Receiving) before starting or finishing work on '
@@ -1491,7 +1491,7 @@ class FpJobStep(models.Model):
return result
def button_finish(self):
"""Canonical button_finish gates first, then super(), then
"""Canonical button_finish - gates first, then super(), then
post-finish side effects.
Gates (raise UserError, blocking finish):
@@ -1502,8 +1502,8 @@ class FpJobStep(models.Model):
no supervisor has pre-signed. Manager bypass:
fp_skip_signoff_gate=True.
- Contract Review (QA-005) complete when customer requires it.
- Receiving gate parts physically on site for this WO.
(Racking-inspection gate removed racking is a recipe step
- Receiving gate - parts physically on site for this WO.
(Racking-inspection gate removed - racking is a recipe step
now, not a separate workflow. _fp_check_racking_inspection_
complete() is kept as a helper for diagnostics.)
@@ -1530,7 +1530,7 @@ class FpJobStep(models.Model):
# ----- Post-shop gate (spec 2026-05-25 D12) ---------------------
# When finishing the LAST open step on an in_progress job, run
# the bake/qty/QC gates that used to live in button_mark_done.
# Failure raises UserError on THIS click operator fixes
# Failure raises UserError on THIS click - operator fixes
# (qty, bake, QC) and retries the finish. Without this the
# auto-advance helper would silently fail with no error path.
for step in self:
@@ -1545,7 +1545,7 @@ class FpJobStep(models.Model):
and s.state not in ('done', 'skipped', 'cancelled')
)
if siblings_open:
continue # not the last open step skip the gates
continue # not the last open step - skip the gates
job._fp_check_finish_gates()
result = super().button_finish()
@@ -1569,13 +1569,13 @@ class FpJobStep(models.Model):
ratio = step.duration_actual / step.duration_expected
if ratio >= 1.5:
step.job_id.message_post(body=Markup(_(
'⚠️ <b>Step "%s" ran %.1fx expected</b> '
'⚠️ <b>Step "%s" ran %.1fx expected</b> - '
'expected %.0f min, actual %.0f min. Investigate: '
'equipment issue, training gap, or recipe time '
'estimate too tight.'
)) % (step.name, ratio, step.duration_expected,
step.duration_actual))
# Bake-window auto-spawn wet plating step + recipe flagged
# Bake-window auto-spawn - wet plating step + recipe flagged
# requires_bake_relief. Heuristic identifies the actual
# plate-out step (kind=wet OR "plating" as a word in name),
# excluding inspection/bake/mask/rack steps that mention
@@ -1608,7 +1608,7 @@ class FpJobStep(models.Model):
bath = Bath.sudo().search([], limit=1)
if not bath:
_logger.warning(
'Step %s: bake-window auto-spawn skipped no bath '
'Step %s: bake-window auto-spawn skipped - no bath '
'configured.', step.name,
)
continue
@@ -1622,7 +1622,7 @@ class FpJobStep(models.Model):
'quantity': int(step.job_id.qty or 0),
})
step.job_id.message_post(body=Markup(_(
'Bake window <b>%s</b> auto-created %.1fh window from '
'Bake window <b>%s</b> auto-created - %.1fh window from '
'plate exit. Required by %s.'
)) % (bw.name, window_hrs, bw.bake_required_by))
return result
@@ -1675,11 +1675,11 @@ class FpJobStep(models.Model):
Only valid for kind='gating' steps in state in (ready, pending,
paused). NOOPs on already-terminal steps for idempotency. Raises
UserError if called on a non-gating step (defensive UI dispatcher
UserError if called on a non-gating step (defensive - UI dispatcher
only renders Mark Passed for gating kinds).
Bypasses the S21 required-inputs gate (gating steps have no
required inputs by design they're admin gates).
required inputs by design - they're admin gates).
Spec: 2026-05-24-workspace-step-actions-design.md Change 5.
"""
@@ -1735,7 +1735,7 @@ class FpJobStep(models.Model):
if not part:
_logger.warning(
"Contract-review step '%s' on job %s has no part_catalog_id "
" cannot redirect to QA-005 form, falling through to "
"- cannot redirect to QA-005 form, falling through to "
"standard wizard.",
self.name, self.job_id.name,
)
@@ -1746,7 +1746,7 @@ class FpJobStep(models.Model):
return part.action_start_contract_review()
# ------------------------------------------------------------------
# Live duration helper view binds to a non-stored compute that
# Live duration helper - view binds to a non-stored compute that
# ticks each time the form re-reads. For a true live ticking clock
# we'd need an OWL widget; this gives "minutes since start" that's
# accurate at every record refresh, which is good enough for a
@@ -1781,13 +1781,13 @@ class FpJobStep(models.Model):
step.duration_running_minutes = step.duration_actual or 0.0
# ------------------------------------------------------------------
# Sub 12d Step Details Quick-Look modal
# Sub 12d - Step Details Quick-Look modal
# ------------------------------------------------------------------
# Three computed/related fields that power the read-only manager
# quick-look modal. The modal is bound via context= on the parent
# job form's <field name="step_ids"/> no TransientModel needed.
# job form's <field name="step_ids"/> - no TransientModel needed.
# Job-level context for the quick-look modal restored after commit
# Job-level context for the quick-look modal - restored after commit
# b0070afc accidentally removed these while still referencing them in
# fp_job_step_quick_look_views.xml (entech caught the mismatch during
# the 2026-05-22 Phase 1-4 deploy).
@@ -1871,7 +1871,7 @@ class FpJobStep(models.Model):
def action_open_quick_look(self):
"""Open the read-only Step Details quick-look modal.
Bound to the row-button on the embedded step list explicit
Bound to the row-button on the embedded step list - explicit
trigger needed because editable="bottom" intercepts row clicks
for inline editing rather than opening the form view.
"""
@@ -1900,7 +1900,7 @@ class FpJobStep(models.Model):
step in one move. Paperwork / first steps don't physically
hold parts per-piece.
- real qty == 1 + downstream: record move(1).
- real qty > 1 + downstream: raise operator must use
- real qty > 1 + downstream: raise - operator must use
Complete 1 → Next (streaming) or Move… (batched).
- real qty > 1 + last step: allow (qty_done auto-tick Phase 2).
Called from action_finish_and_advance just before button_finish.
@@ -1921,7 +1921,7 @@ class FpJobStep(models.Model):
# - real incoming, qty == 1 (streaming flow last part):
# same as Complete 1 → Next's tail call
# - real incoming, qty > 1 (batched flow): one click moves
# everything forward operators with small parts don't
# everything forward - operators with small parts don't
# have to click Complete 1 → Next repeatedly
# Complete 1 → Next is still available for one-by-one flow.
self.env['fp.job.step.move'].create({

View File

@@ -16,13 +16,13 @@ def _clean(text):
if not text:
return ''
t = str(text)
for a, b in ((u'', '-'), (u'', '-'), (u'', "'"),
for a, b in ((u'\u2014', '-'), (u'\u2013', '-'), (u'', "'"),
(u'', "'"), (u'', '"'), (u'', '"'),
(u'', '...'),
# Degree symbols: the masculine-ordinal 'º' (U+00BA) operators
# type for "375ºF", the real degree '°' (U+00B0), and the ring
# '˚' ALL mojibake to "°"/"º" through this sticker's lightweight
# html_container path (no .article UTF-8 wrapper and adding one
# html_container path (no .article UTF-8 wrapper - and adding one
# blows up the dpi=96 mm layout). Strip to clean ASCII: "375F".
(u'º', ''), (u'°', ''), (u'˚', '')):
t = t.replace(a, b)
@@ -94,7 +94,7 @@ class FpJob(models.Model):
'qty': qty,
'due': due_s,
'thk': thk,
# Real thickness present (has a digit) drives the prominent
# Real thickness present (has a digit) - drives the prominent
# THICKNESS banner; skips empty / 'N/A' / '-' placeholders.
'has_thk': bool(thk and any(c.isdigit() for c in thk)),
'mask': bool(line and 'x_fc_masking_enabled' in line._fields and line.x_fc_masking_enabled),
@@ -105,7 +105,7 @@ class FpJob(models.Model):
def _fp_sticker_boxes(self):
"""The job's tracked boxes (External sticker prints one label each).
Empty recordset when none yet the template falls back to 1/1."""
Empty recordset when none yet - the template falls back to 1/1."""
self.ensure_one()
if self.sale_order_id and 'fp.box' in self.env:
return self.env['fp.box'].sudo().search(

View File

@@ -2,11 +2,11 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
"""Sub 14 Configurable workflow status bar.
"""Sub 14 - Configurable workflow status bar.
Each job carries a workflow_state_id that auto-advances along a
shop-configurable sequence of milestones (Draft → Confirmed → Received
→ In Progress → Inspected → Shipped → Done by default).
→ In Progress → Inspected → Shipped → Done - by default).
Recipe authors tag specific recipe steps as "this step's completion
triggers workflow state X" via process_node.triggers_workflow_state_id.
@@ -27,7 +27,7 @@ from odoo import _, api, fields, models
class FpJobWorkflowState(models.Model):
_name = 'fp.job.workflow.state'
_description = 'Fusion Plating Job Workflow State (status bar milestone)'
_description = 'Fusion Plating - Job Workflow State (status bar milestone)'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'sequence, id'
_rec_name = 'name'
@@ -44,7 +44,7 @@ class FpJobWorkflowState(models.Model):
string='Code',
required=True,
tracking=True,
help='Stable identifier used by code/migrations to reference '
help='Stable identifier - used by code/migrations to reference '
'this state without depending on the (translatable) name. '
'Lowercase snake_case (e.g. "received", "inspected").',
)
@@ -98,8 +98,8 @@ class FpJobWorkflowState(models.Model):
# A state is "passed" when ALL recipe steps matching its trigger
# conditions are in done/skipped/cancelled. Two ways to define
# which steps trigger:
# 1. trigger_default_kinds match on recipe step's default_kind
# Selection. Easiest path covers standard recipes that use
# 1. trigger_default_kinds - match on recipe step's default_kind
# Selection. Easiest path - covers standard recipes that use
# the curated kind values (receiving, final_inspect, ship, etc.)
# 2. Per-recipe-node override via
# fusion.plating.process.node.triggers_workflow_state_id
@@ -117,7 +117,7 @@ class FpJobWorkflowState(models.Model):
trigger_first_step_started = fields.Boolean(
string='Trigger on First Step Started',
default=False,
help='Special trigger passes as soon as the first wet step '
help='Special trigger - passes as soon as the first wet step '
'(or any step with kind not in inspection/admin) starts. '
'Used for the "In Progress" milestone.',
)
@@ -125,18 +125,18 @@ class FpJobWorkflowState(models.Model):
trigger_all_steps_done = fields.Boolean(
string='Trigger on All Steps Done',
default=False,
help='Special trigger passes when every non-cancelled step '
help='Special trigger - passes when every non-cancelled step '
'is in done/skipped state. Used for the "Done" milestone.',
)
trigger_on_delivery_state = fields.Boolean(
string='Trigger on Delivery Delivered',
default=False,
help='Special trigger passes once the fusion.plating.delivery '
help='Special trigger - passes once the fusion.plating.delivery '
'linked to the job (job.delivery_id) reaches state="delivered". '
'Used for the Shipped milestone in lieu of recipe-side '
'default_kind="ship" tagging. Shipping is logistics, not '
'manufacturing keeping the trigger off the recipe lets us '
'manufacturing - keeping the trigger off the recipe lets us '
'route deliveries (split shipments, RMA reverse-flow, '
'customer pickup) independently from plating steps.',
)
@@ -160,12 +160,12 @@ class FpJobWorkflowState(models.Model):
trigger_on_parts_received = fields.Boolean(
string='Trigger on Parts Received',
default=False,
help='Special trigger passes once the job\'s sale order has '
help='Special trigger - passes once the job\'s sale order has '
'a receiving status of "partial" or "received" (set by '
'the fp.receiving inbound logistics flow). Used by the '
'Received milestone in lieu of recipe-side '
'default_kind="receiving" tagging. Receiving is a pre-'
'recipe activity (parts physically arrive on the dock) '
'recipe activity (parts physically arrive on the dock) - '
'keeping the trigger off recipe steps means shops without '
'a "Receiving" step in their recipe still see the bar '
'advance correctly.',
@@ -176,7 +176,7 @@ class FpJobWorkflowState(models.Model):
default=False,
help='If True, this state will NOT pass while there is an open '
'quality hold on the job. Used for the "Inspected" '
'milestone you can finish the inspection step but the '
'milestone - you can finish the inspection step but the '
'state stays at the previous milestone until the NCR clears.',
)
@@ -208,7 +208,7 @@ class FpJobWorkflowState(models.Model):
the given fp.job. Called from the job's compute method.
"""
self.ensure_one()
# Initial state always passed (every job starts here)
# Initial state - always passed (every job starts here)
if self.is_initial:
return True
@@ -265,7 +265,7 @@ class FpJobWorkflowState(models.Model):
for s in steps
)
else:
# Untagged recipe any started step counts as
# Untagged recipe - any started step counts as
# "production has started".
production_started = any(
s.state in ('in_progress', 'paused', 'done')
@@ -290,7 +290,7 @@ class FpJobWorkflowState(models.Model):
)
)
if not matching_steps:
# Nothing matches this state can't pass for this recipe.
# Nothing matches - this state can't pass for this recipe.
# Treat as not-passed so the bar stays at the previous state.
return False

View File

@@ -2,7 +2,7 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
#
# Phase 3 parallel job/step links on fusion.plating.quality.hold.
# Phase 3 - parallel job/step links on fusion.plating.quality.hold.
# Coexists with bridge_mrp's existing production_id link.
from odoo import fields, models

View File

@@ -2,7 +2,7 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
#
# Phase 3 / Phase 9 native-job link on fp.racking.inspection.
# Phase 3 / Phase 9 - native-job link on fp.racking.inspection.
# Coexists with the legacy production_id (mrp.production) link; either
# (or both) may be set.
@@ -20,7 +20,7 @@ class FpRackingInspection(models.Model):
def _compute_name(self):
for rec in self:
if rec.x_fc_job_id:
rec.name = _('Inspection %s') % rec.x_fc_job_id.name
rec.name = _('Inspection - %s') % rec.x_fc_job_id.name
else:
rec.name = _('Racking Inspection')

View File

@@ -84,7 +84,7 @@ class FpReceiving(models.Model):
jobs = self._fp_sticker_jobs()
if not jobs:
raise UserError(_(
'No work order exists for this receiving yet create the '
'No work order exists for this receiving yet - create the '
'Work Order before printing stickers.'))
return self.env.ref(xmlid).report_action(jobs)
@@ -94,6 +94,6 @@ class FpReceiving(models.Model):
'fusion_plating_jobs.action_report_fp_job_sticker')
def action_print_internal_sticker(self):
"""Shop (internal) box sticker(s) same layout, internal notes."""
"""Shop (internal) box sticker(s) - same layout, internal notes."""
return self._fp_print_sticker(
'fusion_plating_jobs.action_report_fp_job_sticker_internal')

View File

@@ -2,7 +2,7 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
#
# Phase 3 parallel job/step links on fp.thickness.reading.
# Phase 3 - parallel job/step links on fp.thickness.reading.
from odoo import fields, models

View File

@@ -2,7 +2,7 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
#
# Native fp.job margin report replaces report_wo_margin which binds
# Native fp.job margin report - replaces report_wo_margin which binds
# to mrp.production. Uses fp.job.step.cost_total (already computed in
# Phase 1: duration_actual / 60 * cost_per_hour).

View File

@@ -13,7 +13,7 @@ class ResUsers(models.Model):
string='Plating Initials',
help='Operator / inspector initials used to pre-fill signature '
'and "Reviewer Initials" style prompts in the Record Inputs '
'dialog. Editable in the dialog itself when the user types '
'dialog. Editable in the dialog itself - when the user types '
'a different value and saves, it persists here for every '
'future job and step.',
)

View File

@@ -2,7 +2,7 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
#
# sale.order.action_confirm hook creates fp.job records on confirm.
# sale.order.action_confirm hook - creates fp.job records on confirm.
# Sub 11 (2026-04-26) removed MRP entirely; fp.job is the only fulfilment
# path. The former x_fc_use_native_jobs migration toggle was dropped in
# 19.0.8.19.0 once the legacy bridge_mrp fallback became unreachable.
@@ -59,11 +59,11 @@ class SaleOrder(models.Model):
help='The quote-stage name (e.g. Q202605-200). Preserved when '
'the SO is renamed on confirm.',
)
# Per-model counters monotonic, never decrement. Source of truth
# Per-model counters - monotonic, never decrement. Source of truth
# for the next sibling's x_fc_doc_index. Updated via row-locked SQL
# in fp.parent.numbered.mixin so concurrent creates can't drift.
#
# Naming: `x_fc_pn_*_count` the `pn_` infix distinguishes our
# Naming: `x_fc_pn_*_count` - the `pn_` infix distinguishes our
# storage counters from pre-existing compute fields (e.g. the
# `x_fc_delivery_count` compute in bridge_mrp, `x_fc_ncr_count`
# in configurator, `x_fc_receiving_count` in fp_receiving) which
@@ -82,7 +82,7 @@ class SaleOrder(models.Model):
x_fc_pn_rma_count = fields.Integer(string='Parent: RMA Count', readonly=True, copy=False, default=0)
# ------------------------------------------------------------------
# Phase 4 (Sub 11) workflow-stage field + assigned-manager field
# Phase 4 (Sub 11) - workflow-stage field + assigned-manager field
# relocated from fusion_plating_bridge_mrp. Field re-declared with
# the same selection + compute pointer; jobs is now the source of
# truth so Phase 5 can delete bridge_mrp without losing the field.
@@ -236,7 +236,7 @@ class SaleOrder(models.Model):
return action
def action_view_fp_certificates(self):
"""Smart-button target open the certificate(s) linked to this
"""Smart-button target - open the certificate(s) linked to this
SO. One cert → form view; many → list view filtered to this SO."""
self.ensure_one()
certs = self.env['fp.certificate'].search([
@@ -258,7 +258,7 @@ class SaleOrder(models.Model):
return action
# ------------------------------------------------------------------
# Parent-number hierarchy quote naming on create
# Parent-number hierarchy - quote naming on create
# ------------------------------------------------------------------
@api.model_create_multi
def create(self, vals_list):
@@ -287,7 +287,7 @@ class SaleOrder(models.Model):
Parent number is drawn from fp.parent.number; the quote name
was already saved to x_fc_quote_ref on create() so it survives
the rename. Idempotent if x_fc_parent_number is already set,
the rename. Idempotent - if x_fc_parent_number is already set,
the rename is skipped (re-confirm scenarios)."""
Seq = self.env['ir.sequence']
for so in self:
@@ -344,7 +344,7 @@ class SaleOrder(models.Model):
))._create_invoices(grouped=grouped, final=final, date=date)
def unlink(self):
"""Spec §6.2 confirmed SOs are part of the compliance audit
"""Spec §6.2 - confirmed SOs are part of the compliance audit
trail and cannot be deleted. Cancellation must go through the
state machine instead. Draft SOs (no parent_number assigned
yet) remain freely deletable per Odoo standard. Applies to
@@ -352,7 +352,7 @@ class SaleOrder(models.Model):
for so in self:
if so.x_fc_parent_number:
raise UserError(_(
'Sale Order "%(name)s" cannot be deleted it has '
'Sale Order "%(name)s" cannot be deleted - it has '
'been confirmed (parent number %(parent)s issued) '
'and is part of the compliance audit trail. Cancel '
'it instead. This rule applies to all users '
@@ -364,14 +364,14 @@ class SaleOrder(models.Model):
"""Recipe resolution with Express-Orders SO header fallback.
Priority (most-specific first):
1. line.x_fc_process_variant_id explicit per-line variant
1. line.x_fc_process_variant_id - explicit per-line variant
(always wins; this is where G3 propagation lands a value).
2. self.x_fc_material_process Express Orders order-level
2. self.x_fc_material_process - Express Orders order-level
recipe. Catches the case where G3 propagation failed to
reach the line but the header has the recipe.
3. part.default_process_id part's flagged default
3. part.default_process_id - part's flagged default
variant. Customer-and-part-tuned recipe.
4. part.recipe_id legacy fallback.
4. part.recipe_id - legacy fallback.
Returns the recipe record or an empty recordset.
"""
Node = self.env['fusion.plating.process.node']
@@ -428,7 +428,7 @@ class SaleOrder(models.Model):
'x_fc_part_catalog_id' in self._fields and self.x_fc_part_catalog_id
):
_logger.info(
'SO %s: no line-level part but header carries one '
'SO %s: no line-level part but header carries one - '
'treating all lines as a single plating job.', self.name,
)
plating_lines = self.order_line
@@ -439,7 +439,7 @@ class SaleOrder(models.Model):
# Group by (recipe, part, spec, thickness, serial). Lines that
# share ALL FIVE collapse into one WO. Bundling lines with
# different specs / thicknesses / serials under one WO would
# carry the first line's values onto the cert + sticker
# carry the first line's values onto the cert + sticker -
# silent mis-attestation. No-recipe lines still get their own
# group each.
groups = {}
@@ -513,7 +513,7 @@ class SaleOrder(models.Model):
if recipe:
vals['recipe_id'] = recipe.id
# Customer references mirror onto the job so the shop floor
# Customer references - mirror onto the job so the shop floor
# has them without round-tripping to the SO.
if 'x_fc_customer_job_number' in self._fields \
and self.x_fc_customer_job_number:
@@ -523,7 +523,7 @@ class SaleOrder(models.Model):
if 'x_fc_rush_order' in self._fields:
vals['x_fc_rush_order'] = bool(self.x_fc_rush_order)
# Scheduling targets mirror the SO's customer-facing dates.
# Scheduling targets - mirror the SO's customer-facing dates.
if 'x_fc_internal_deadline' in self._fields \
and self.x_fc_internal_deadline:
vals['x_fc_internal_deadline'] = self.x_fc_internal_deadline
@@ -531,7 +531,7 @@ class SaleOrder(models.Model):
and self.x_fc_planned_start_date:
vals['x_fc_planned_start_date'] = self.x_fc_planned_start_date
# Operational notes mirror so the shop has them on the WO.
# Operational notes - mirror so the shop has them on the WO.
if 'x_fc_internal_note' in self._fields \
and self.x_fc_internal_note:
vals['x_fc_internal_note'] = self.x_fc_internal_note
@@ -539,7 +539,7 @@ class SaleOrder(models.Model):
and self.x_fc_external_note:
vals['x_fc_external_note'] = self.x_fc_external_note
# Customer spec / facility / manager copy from SO if present
# Customer spec / facility / manager - copy from SO if present
if 'x_fc_customer_spec_id' in self._fields and self.x_fc_customer_spec_id:
vals['customer_spec_id'] = self.x_fc_customer_spec_id.id
if 'x_fc_facility_id' in self._fields and self.x_fc_facility_id:
@@ -568,12 +568,12 @@ class SaleOrder(models.Model):
self.name, job.name, qty, (recipe.name if recipe else '-'),
)
# Express Orders (2026-05-26) apply per-line masking + bake
# Express Orders (2026-05-26) - apply per-line masking + bake
# overrides to the new job. This runs BEFORE step generation
# (which happens in fp.job.action_confirm) so the override rows
# are in place when _generate_steps_from_recipe reads override_map.
# Step.instructions writes are deferred to a second pass after
# step gen see fp.job.action_confirm override.
# step gen - see fp.job.action_confirm override.
if job.recipe_id and 'x_fc_masking_enabled' in self.env['sale.order.line']._fields:
for sol in lines:
if hasattr(sol, '_fp_apply_express_overrides_to_job'):
@@ -590,7 +590,7 @@ class SaleOrder(models.Model):
return True
# ------------------------------------------------------------------
# Phase 4 (Sub 11) workflow stage action buttons.
# Phase 4 (Sub 11) - workflow stage action buttons.
# Native versions of bridge_mrp's action_fp_* methods. Drop the
# mrp.production lookups; talk to fp.job and fp.receiving directly.
# ------------------------------------------------------------------
@@ -619,7 +619,7 @@ class SaleOrder(models.Model):
if Recv is None:
return False
for rec in Recv.search([('sale_order_id', '=', self.id)]):
# Push receiving to its terminal state 'closed' is the
# Push receiving to its terminal state - 'closed' is the
# post-Sub-8 terminal; 'accepted' kept as a legacy fallback
# only for old records still in pre-Sub-8 states.
if rec.state in ('draft', 'counted', 'staged'):
@@ -628,7 +628,7 @@ class SaleOrder(models.Model):
rec.state = 'accepted'
if 'x_fc_receiving_status' in self._fields:
self.x_fc_receiving_status = 'received'
self.message_post(body=_('Parts accepted ready to assign manager.'))
self.message_post(body=_('Parts accepted - ready to assign manager.'))
return True
def action_fp_assign_to_me(self):
@@ -687,6 +687,6 @@ class SaleOrder(models.Model):
return {
'type': 'ir.actions.client',
'tag': 'fp_plant_overview',
'name': _('Shop Floor %s') % self.name,
'name': _('Shop Floor - %s') % self.name,
'target': 'current',
}

View File

@@ -4,7 +4,7 @@
#
# Mid-job qty drift guard. When Sarah edits an SO line's qty after a
# fp.job has been spawned and started, the job's qty does NOT auto-
# update (intentionally Carlos may already be plating). But without
# update (intentionally - Carlos may already be plating). But without
# a warning the qty drift is silent and bills go out wrong. This
# write-override posts chatter on every active linked job so operators
# see the change immediately, AND offers a "Sync qty from SO" action
@@ -41,7 +41,7 @@ class SaleOrderLine(models.Model):
job.message_post(body=Markup(_(
'⚠️ <b>SO qty changed mid-job</b> by %(user)s. '
'SO line %(name)s went from %(old)g to %(new)g. '
'Job qty is still <b>%(jobqty)g</b> operator '
'Job qty is still <b>%(jobqty)g</b> - operator '
'must manually adjust scope (start more racks or '
'stop early) and the supervisor should hit '
'<b>Sync qty from SO</b> on the job header to '