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

@@ -15,7 +15,7 @@ readings before confirming. On confirm:
and the parsed readings are written as fp.thickness.reading rows.
- cert.action_issue() is called for each cert.
The wizard is a convenience layer it does NOT replace the per-cert
The wizard is a convenience layer - it does NOT replace the per-cert
Issue button on the cert form, which stays as the fallback path.
"""
import base64
@@ -53,7 +53,7 @@ _FISCHER_READING_RE = re.compile(
)
# Capture every {\pict ... \wmetafile8 ...hex...} group in an RTF, in
# document order. The hex blob can be interspersed with whitespace
# (RTF wraps to 80 cols) the consumer strips it.
# (RTF wraps to 80 cols) - the consumer strips it.
_RTF_PICT_WMF_RE = re.compile(
r'\{\\pict'
r'(?:\\[a-zA-Z]+-?\d*\s?)*?'
@@ -71,7 +71,7 @@ def _fp_extract_rtf_images(raw_bytes):
XDAL 600 RTF exports embed each picture as a WMF metafile wrapping
the actual raster. ImageMagick on Debian Bookworm doesn't carry a
WMF delegate, so we shell out to `wmf2svg` (from libwmf-bin) it
WMF delegate, so we shell out to `wmf2svg` (from libwmf-bin) - it
writes a thin SVG and a side-file `*-N.png` per raster block. We
keep the PNGs, drop the SVG/WMF temp files.
@@ -108,7 +108,7 @@ def _fp_extract_rtf_images(raw_bytes):
)
except (FileNotFoundError, subprocess.TimeoutExpired) as e:
_logger.warning(
'wmf2svg unavailable or timed out (%s) skipping '
'wmf2svg unavailable or timed out (%s) - skipping '
'RTF image extraction.', e,
)
return []
@@ -125,7 +125,7 @@ def _fp_extract_rtf_images(raw_bytes):
def _fp_pick_microscope_image(png_bytes_list):
"""Pick the largest-area PNG (by pixel count, not file size) from
the list that's almost always the microscope photo. Header
the list - that's almost always the microscope photo. Header
banners are wide-but-thin so their pixel area falls below the
threshold. Returns (png_bytes, width, height) or (None, 0, 0)
when no PNG meets the threshold.
@@ -153,7 +153,7 @@ _FISCHER_CALIB_RE = re.compile(r'Calibr\.\s*Std\.\s*Set\s+(.+?)(?:\s{2,}|$)',
_FISCHER_OPERATOR_RE = re.compile(r'Operator:\s*(\S+)', re.IGNORECASE)
_FISCHER_DATE_RE = re.compile(r'Date:\s*([\d/]+)', re.IGNORECASE)
_FISCHER_TIME_RE = re.compile(r'Time:\s*([\d:]+\s*[APMapm]*)')
# XDAL 600 header lines only present on full RTF reports (not on
# XDAL 600 header lines - only present on full RTF reports (not on
# the .docx body the upstream parser already handled).
_FISCHER_PRODUCT_RE = re.compile(r'Product:\s*([^\r\n]+?)(?:\s{2,}|$)', re.IGNORECASE)
_FISCHER_DIRECTORY_RE = re.compile(r'Directory:\s*([^\r\n]+?)(?:\s{2,}|$)', re.IGNORECASE)
@@ -168,14 +168,14 @@ def _fp_strip_rtf(raw_bytes):
all of those plus the hex-encoded image data so the Fischerscope
reading regex hits clean text.
Not a full parser meant for the narrow case of XRF/XDAL reports
Not a full parser - meant for the narrow case of XRF/XDAL reports
that have a simple body wrapped around an embedded WMF image.
"""
if not raw_bytes:
return ''
# RTF is ASCII-safe; latin-1 round-trips every byte.
text = raw_bytes.decode('latin-1', errors='replace')
# Drop destination groups entirely these are the image data,
# Drop destination groups entirely - these are the image data,
# font tables, color tables, etc. The pattern `{\* ...}` and other
# nested destinations carry binary-ish hex strings we never want.
text = re.sub(r'\{\\\*[^{}]*\}', ' ', text)
@@ -185,7 +185,7 @@ def _fp_strip_rtf(raw_bytes):
# part between `\pict...goal\d+` and the closing brace of the group.
# Easier: nuke anything matching the picture marker through the
# next closing brace at the same depth (single-level approximation
# works for FedEx/XRF docs that have one image per pict block).
# - works for FedEx/XRF docs that have one image per pict block).
text = re.sub(r'\{\\pict[^{}]*\}', ' ', text)
# Remove control words like \rtf1, \ansicpg1252, \par, \tab,
# \tx2840, etc. (`\` + letters + optional digits + optional space)
@@ -204,7 +204,7 @@ def _fp_strip_rtf(raw_bytes):
def _fp_parse_fischerscope_rtf(raw_bytes):
"""Fischerscope XDAL 600 RTF export → same dict shape as the
.docx parser. RTF detection is by magic bytes (`{\\rtf`) the
.docx parser. RTF detection is by magic bytes (`{\\rtf`) - the
XRF software names the file `.doc` for legacy reasons, but the
contents are RTF.
"""
@@ -267,7 +267,7 @@ def _fp_parse_fischerscope_docx(raw_bytes):
}
Soft-fails to an empty dict-like result when python-docx isn't
installed or the bytes don't parse the wizard still works, the
installed or the bytes don't parse - the wizard still works, the
operator just has to type readings manually.
"""
empty = {
@@ -280,7 +280,7 @@ def _fp_parse_fischerscope_docx(raw_bytes):
import docx # python-docx
except ImportError:
_logger.info(
'python-docx not installed Fischerscope auto-parse '
'python-docx not installed - Fischerscope auto-parse '
'skipped. Operator will enter readings manually.'
)
return empty
@@ -335,7 +335,7 @@ def _fp_parse_fischerscope_docx(raw_bytes):
class FpCertIssueWizard(models.TransientModel):
_name = 'fp.cert.issue.wizard'
_description = 'Fusion Plating Issue Certs Wizard'
_description = 'Fusion Plating - Issue Certs Wizard'
job_id = fields.Many2one(
'fp.job', string='Job', required=True, readonly=True,
@@ -357,7 +357,7 @@ class FpCertIssueWizard(models.TransientModel):
@api.model
def open_for_job(self, job):
"""Factory create a wizard pre-populated with one line per
"""Factory - create a wizard pre-populated with one line per
draft cert on the job. Returns an action dict that opens the
wizard form."""
Cert = self.env['fp.certificate'].sudo()
@@ -375,7 +375,7 @@ class FpCertIssueWizard(models.TransientModel):
})
return {
'type': 'ir.actions.act_window',
'name': _('Issue Certs %s') % job.name,
'name': _('Issue Certs - %s') % job.name,
'res_model': self._name,
'res_id': wiz.id,
'view_mode': 'form',
@@ -416,7 +416,7 @@ class FpCertIssueWizard(models.TransientModel):
class FpCertIssueWizardLine(models.TransientModel):
_name = 'fp.cert.issue.wizard.line'
_description = 'Fusion Plating Issue Certs Wizard Line'
_description = 'Fusion Plating - Issue Certs Wizard Line'
wizard_id = fields.Many2one(
'fp.cert.issue.wizard', required=True, ondelete='cascade',
@@ -438,7 +438,7 @@ class FpCertIssueWizardLine(models.TransientModel):
fischer_filename = fields.Char(string='Filename')
# Optional: microscope/coupon image exported separately from the
# XDAL 600. The RTF carries an embedded WMF that the entech host
# can't rasterize (no imagemagick/libwmf see CLAUDE.md "entech
# can't rasterize (no imagemagick/libwmf - see CLAUDE.md "entech
# apt is in a broken-deps state"), so the operator exports a PNG
# from the XDAL software and uploads it here. Rendered inline on
# the CoC's thickness section when present.
@@ -464,8 +464,8 @@ class FpCertIssueWizardLine(models.TransientModel):
'cert_id.partner_id.x_fc_strict_thickness_required',
'cert_id.x_fc_job_id.recipe_id.requires_thickness_report')
def _compute_needs_thickness(self):
# Delegate to fp.certificate._fp_needs_thickness_data the single
# source of truth shared with the action_issue hard gate so the
# Delegate to fp.certificate._fp_needs_thickness_data - the single
# source of truth shared with the action_issue hard gate - so the
# wizard's readiness hint and the gate can never drift. Honours
# recipe-level thickness suppression (passivation = no thickness
# even if the customer asked).
@@ -493,7 +493,7 @@ class FpCertIssueWizardLine(models.TransientModel):
@api.onchange('fischer_file', 'fischer_filename')
def _onchange_fischer_file(self):
"""Parse .docx OR RTF on upload (XDAL 600 names RTF files
`.doc` detected by magic bytes; see CLAUDE.md "Fischerscope
`.doc` - detected by magic bytes; see CLAUDE.md "Fischerscope
XDAL 600 `.doc` files are actually RTF"). Prefill the readings
+ summary so the operator can verify before issuing."""
if not self.fischer_file:
@@ -511,7 +511,7 @@ class FpCertIssueWizardLine(models.TransientModel):
parsed = _fp_parse_fischerscope_docx(raw)
else:
self.parsed_summary = _(
'Non-parseable upload (%s) file will be attached as '
'Non-parseable upload (%s) - file will be attached as '
'evidence. Type readings manually below if needed.'
) % (self.fischer_filename or 'unnamed')
return
@@ -531,9 +531,9 @@ class FpCertIssueWizardLine(models.TransientModel):
'Operator: %(o)s · Date: %(d)s %(t)s'
) % {
'n': len(readings),
'c': parsed.get('calibration') or '',
'o': parsed.get('operator') or '',
'd': parsed.get('date_str') or '',
'c': parsed.get('calibration') or '-',
'o': parsed.get('operator') or '-',
'd': parsed.get('date_str') or '-',
't': parsed.get('time_str') or '',
}
@@ -559,7 +559,7 @@ class FpCertIssueWizardLine(models.TransientModel):
for fname, fval in field_map:
if fname in cert._fields and fval:
vals[fname] = fval
# Combine the gauge's date+time and parse to Datetime try a
# Combine the gauge's date+time and parse to Datetime - try a
# few formats since XDAL exports vary (12h vs 24h, with/without
# seconds). Best-effort: leave the field blank if no format
# matches rather than crashing the cert issue.
@@ -589,7 +589,7 @@ class FpCertIssueWizardLine(models.TransientModel):
Order matters: operator-uploaded PNG must run LAST so it wins
over any image the RTF auto-extraction picked. Reverse order
(PNG first, then RTF) lets the WMF blow away the explicit
operator choice exactly the bug we just hit.
operator choice - exactly the bug we just hit.
"""
self.ensure_one()
cert = self.cert_id.sudo()
@@ -602,13 +602,13 @@ class FpCertIssueWizardLine(models.TransientModel):
name = (self.fischer_filename or 'fischerscope').lower()
calibration = '' # backfilled below if the parser hits
if name.endswith('.pdf'):
# Drop the PDF into the cert-local field merges into page 2.
# Drop the PDF into the cert-local field - merges into page 2.
cert.write({
'x_fc_local_thickness_pdf': self.fischer_file,
'x_fc_local_thickness_pdf_filename': self.fischer_filename,
})
else:
# .doc / .docx / anything else attach as evidence AND
# .doc / .docx / anything else - attach as evidence AND
# link the attachment to the cert's evidence slot so the
# thickness-required gate recognises it. Without the link,
# the gate would still raise (it checks specific fields,
@@ -675,7 +675,7 @@ class FpCertIssueWizardLine(models.TransientModel):
'Fischerscope file <b>%s</b> attached via Issue wizard.'
)) % (self.fischer_filename or 'unnamed'))
self._push_readings_to_cert(calibration=calibration)
# Operator's PNG upload wins over auto-extracted WMF runs
# Operator's PNG upload wins over auto-extracted WMF - runs
# last so it overwrites x_fc_thickness_image_id if both paths
# supplied an image.
self._apply_image_to_cert(cert)
@@ -704,7 +704,7 @@ class FpCertIssueWizardLine(models.TransientModel):
def _push_readings_to_cert(self, calibration=''):
"""Create fp.thickness.reading rows on the cert from wizard rows.
Skips when no rows. Does not deduplicate against existing
readings the manager has just told us this is the new data.
readings - the manager has just told us this is the new data.
Per-reading calibration_std_ref is stamped from the optional
`calibration` arg so the printed CoC's calibration line stays
accurate even when readings are re-pushed from a fresh upload.
@@ -729,7 +729,7 @@ class FpCertIssueWizardLine(models.TransientModel):
class FpCertIssueWizardReading(models.TransientModel):
_name = 'fp.cert.issue.wizard.reading'
_description = 'Fusion Plating Issue Certs Wizard Reading Row'
_description = 'Fusion Plating - Issue Certs Wizard Reading Row'
_order = 'sequence, id'
line_id = fields.Many2one(

View File

@@ -15,7 +15,7 @@
<sheet>
<div class="oe_title">
<h2>
Issue Certs
Issue Certs -
<field name="job_id" readonly="1" nolabel="1"/>
</h2>
</div>
@@ -30,7 +30,7 @@
</div>
<!-- 2026-05-20: surface the file upload INLINE in the
list instead of behind a row-click into a sub-form.
Operators kept missing the upload affordance the
Operators kept missing the upload affordance - the
list looked like a status display, not an action
surface. Adding the binary field as a column lets
them drop the Fischerscope file right where they

View File

@@ -18,7 +18,7 @@ the job form.
Captured values land on a synthetic `fp.job.step.move` row with
transfer_type='step' (an in-place move, no destination change) so the
existing CoC chronological QWeb template renders them in the same
format as the tablet-captured values single source of truth for
format as the tablet-captured values - single source of truth for
report rendering.
"""
@@ -51,7 +51,7 @@ _FP_INPUT_TYPE_SELECTION = [
class FpJobStepInputWizard(models.TransientModel):
_name = 'fp.job.step.input.wizard'
_description = 'Fusion Plating Step Input Recording (Backend)'
_description = 'Fusion Plating - Step Input Recording (Backend)'
step_id = fields.Many2one(
'fp.job.step', string='Step', required=True, readonly=True,
@@ -76,11 +76,11 @@ class FpJobStepInputWizard(models.TransientModel):
return defaults
defaults['step_id'] = step.id
node = step.recipe_node_id
# Sub 12d master switch when off, return no input rows.
# Sub 12d - master switch - when off, return no input rows.
if hasattr(node, 'collect_measurements') and not node.collect_measurements:
defaults['line_ids'] = []
return defaults
# Filter to step_input prompts only transition inputs go on the
# Filter to step_input prompts only - transition inputs go on the
# Move wizard, not here. Also filter to collect=True (per-recipe
# opt-out, default True).
inputs = node.input_ids
@@ -106,7 +106,7 @@ class FpJobStepInputWizard(models.TransientModel):
'Click "Add a line" in the table above to enter an '
'ad-hoc measurement.'
))
# Ad-hoc rows must have a prompt name otherwise we can't tell
# Ad-hoc rows must have a prompt name - otherwise we can't tell
# what was being measured on the audit trail.
unnamed = self.line_ids.filtered(
lambda l: not l.node_input_id and not (l.name or '').strip()
@@ -143,7 +143,7 @@ class FpJobStepInputWizard(models.TransientModel):
'value_boolean': line.value_boolean,
'value_date': line.value_date or False,
}
# Sub 12d composite + photo input types serialise differently.
# Sub 12d - composite + photo input types serialise differently.
if line.is_photo_type and line.photo_value:
att = Attachment.create({
'name': line.photo_filename or 'photo.jpg',
@@ -206,12 +206,12 @@ class FpJobStepInputWizard(models.TransientModel):
class FpJobStepInputWizardLine(models.TransientModel):
_name = 'fp.job.step.input.wizard.line'
_description = 'Fusion Plating Step Input Wizard Line'
_description = 'Fusion Plating - Step Input Wizard Line'
wizard_id = fields.Many2one(
'fp.job.step.input.wizard', required=True, ondelete='cascade',
)
# 2026-04-28 fix node_input_id is optional now so operators can
# 2026-04-28 fix - node_input_id is optional now so operators can
# record ad-hoc measurements when the recipe has no authored prompts
# (the screenshot case: a step with zero step_input definitions
# rendered an empty wizard with no way to add anything). Authored
@@ -221,7 +221,7 @@ class FpJobStepInputWizardLine(models.TransientModel):
'fusion.plating.process.node.input', ondelete='set null',
)
name = fields.Char(string='Prompt')
# 2026-04-28 convert input_type + target_unit from Char → Selection
# 2026-04-28 - convert input_type + target_unit from Char → Selection
# so operators pick from the curated dropdown. Free-text led to "kg"
# vs "kgs" vs "kilo" inconsistencies on the audit trail.
input_type = fields.Selection(
@@ -233,7 +233,7 @@ class FpJobStepInputWizardLine(models.TransientModel):
target_unit = fields.Selection(
FP_UOM_SELECTION,
string='Unit',
help='Pick from the curated list keeps every step\'s readings '
help='Pick from the curated list - keeps every step\'s readings '
'in the same vocabulary across the shop.',
)
@@ -242,7 +242,7 @@ class FpJobStepInputWizardLine(models.TransientModel):
value_boolean = fields.Boolean(string='Yes/No')
value_date = fields.Datetime(string='Date / Time')
# Sub 12d composite + photo input types
# Sub 12d - composite + photo input types
photo_value = fields.Binary(string='Photo', attachment=True)
photo_filename = fields.Char(string='Photo Filename')
point_1 = fields.Float(string='R1')
@@ -274,7 +274,7 @@ class FpJobStepInputWizardLine(models.TransientModel):
is_authored = fields.Boolean(
compute='_compute_is_authored',
help='True when this row originated from an authored recipe input. '
'Drives field readonly state authored prompts are locked, '
'Drives field readonly state - authored prompts are locked, '
'ad-hoc rows are fully editable.',
)
@@ -285,7 +285,7 @@ class FpJobStepInputWizardLine(models.TransientModel):
# ---- Single-column value editor -----------------------------------------
# The previous wizard exposed FOUR value columns (text / number /
# yes-no / date) operators saw 9 columns wide and got lost. We
# yes-no / date) - operators saw 9 columns wide and got lost. We
# collapse them into one "Value" column whose widget routes to the
# right typed field based on input_type. Booleans and dates get
# their own dedicated field (still per-row) so the widget behaves

View File

@@ -94,7 +94,7 @@
</record>
<!-- ================================================================== -->
<!-- v3 Card-based stacked layout (light + dark mode aware) -->
<!-- v3 - Card-based stacked layout (light + dark mode aware) -->
<!-- -->
<!-- Replaces v2's wide editable table. Each measurement renders as a -->
<!-- card with: prompt name (header), type/unit pills (meta), and ONLY -->
@@ -133,7 +133,7 @@
<field name="line_ids" class="o_fp_input_card_list" nolabel="1">
<list editable="bottom" create="true" delete="true">
<!-- Hidden flag fields drive value-cell visibility -->
<!-- Hidden flag fields - drive value-cell visibility -->
<field name="is_authored" column_invisible="1"/>
<field name="is_boolean_type" column_invisible="1"/>
<field name="is_date_type" column_invisible="1"/>
@@ -144,14 +144,14 @@
<field name="point_avg" column_invisible="1"/>
<field name="photo_filename" column_invisible="1"/>
<!-- Card header prompt name (large, bold via SCSS) -->
<!-- Card header - prompt name (large, bold via SCSS) -->
<field name="name"
string="Measurement"
readonly="is_authored"
placeholder="e.g. Oven Temp, Bath Reading, Operator Initials"
class="o_fp_iw_prompt"/>
<!-- Meta type + unit rendered as pills (top-right).
<!-- Meta - type + unit rendered as pills (top-right).
Distinct classes so each pill lands in its own
grid column (otherwise they stack on top of
each other and the labels overlap). -->
@@ -165,12 +165,12 @@
class="o_fp_iw_meta o_fp_iw_meta_unit"
optional="show"/>
<!-- Hidden by default operator can opt in via the cog menu
<!-- Hidden by default - operator can opt in via the cog menu
if they want to see/edit target ranges per row -->
<field name="target_min" optional="hide"/>
<field name="target_max" optional="hide"/>
<!-- Mutually exclusive value widgets only the one
<!-- Mutually exclusive value widgets - only the one
matching the row's input_type renders -->
<field name="value_number"
string="Value"
@@ -196,7 +196,7 @@
invisible="not is_photo_type"
class="o_fp_iw_value"/>
<!-- Composite type 1: Multi-Point Thickness 5 readings -->
<!-- Composite type 1: Multi-Point Thickness - 5 readings -->
<field name="point_1" string="R1"
invisible="not is_multi_point_type"
class="o_fp_iw_extra" optional="show"/>

View File

@@ -8,7 +8,7 @@
Mirrors the tablet's Move Parts dialog (`fusion_plating_shopfloor`'s
`move_parts_dialog.js`) so a manager running the whole job from the
backend form on a low-staffing day captures the same chain-of-custody
record the operator would create from the tablet same `fp.job.step.move`
record the operator would create from the tablet - same `fp.job.step.move`
row + same `transition_input_value_ids` snapshot, same chatter trail,
same downstream report rendering.
@@ -40,7 +40,7 @@ _FP_INPUT_TYPE_SELECTION = [
class FpJobStepMoveWizard(models.TransientModel):
_name = 'fp.job.step.move.wizard'
_description = 'Fusion Plating Move Step Wizard (Backend)'
_description = 'Fusion Plating - Move Step Wizard (Backend)'
job_id = fields.Many2one('fp.job', string='Job', required=True, readonly=True)
from_step_id = fields.Many2one(
@@ -97,7 +97,7 @@ class FpJobStepMoveWizard(models.TransientModel):
'wizard_id',
string='Compliance Prompts',
help='Authored transition inputs from the to-step\'s recipe node. '
'Capture the operator\'s answers they snapshot to '
'Capture the operator\'s answers - they snapshot to '
'fp.job.step.move.input.value when the wizard commits.',
)
@@ -108,7 +108,7 @@ class FpJobStepMoveWizard(models.TransientModel):
ctx = self.env.context
from_step_id = ctx.get('default_from_step_id') or ctx.get('active_id')
if from_step_id and self.env.context.get('active_model') != 'fp.job.step':
# Came from job form button active_id is the job, not the step
# Came from job form button - active_id is the job, not the step
from_step_id = ctx.get('default_from_step_id')
if from_step_id:
from_step = self.env['fp.job.step'].browse(from_step_id)
@@ -133,7 +133,7 @@ class FpJobStepMoveWizard(models.TransientModel):
defaults['to_step_id'] = next_step.id
# Pre-seed input_value_ids from authored prompts on
# both ends of the move so programmatic creators
# (script tests, RPC clients) get them too
# (script tests, RPC clients) get them too -
# @api.onchange only fires in interactive UI.
seen = set()
rows = []
@@ -173,14 +173,14 @@ class FpJobStepMoveWizard(models.TransientModel):
def _onchange_to_step_seed_inputs(self):
"""Seed prompt rows from BOTH
* the to-step's recipe node `transition_input` prompts
* the to-step's recipe node `transition_input` prompts -
authored compliance fields fired on move-in.
* the from-step's recipe node `step_input` prompts
* the from-step's recipe node `step_input` prompts -
measurements that should be captured BEFORE leaving the
from-step (operator answers "what did you actually run?"
while the data is fresh).
2026-04-28 fix previously only transition_input was pulled,
2026-04-28 fix - previously only transition_input was pulled,
which left the section empty for steps where the author only
defined step_input prompts. Operators tried to record inputs
at move time and got an unfillable form.
@@ -189,7 +189,7 @@ class FpJobStepMoveWizard(models.TransientModel):
wiz.input_value_ids = [(5, 0, 0)]
seen = set()
rows = []
# 1. From-step's step_input prompts measurements captured
# 1. From-step's step_input prompts - measurements captured
# while finalising the step.
if wiz.from_step_id and wiz.from_step_id.recipe_node_id:
from_node = wiz.from_step_id.recipe_node_id
@@ -205,7 +205,7 @@ class FpJobStepMoveWizard(models.TransientModel):
'name': '%s (Step Input)' % inp.name,
'input_type': inp.input_type,
}))
# 2. To-step's transition_input prompts compliance gates
# 2. To-step's transition_input prompts - compliance gates
# fired on entry to the next step.
if wiz.to_step_id and wiz.to_step_id.recipe_node_id:
to_node = wiz.to_step_id.recipe_node_id
@@ -243,7 +243,7 @@ class FpJobStepMoveWizard(models.TransientModel):
qty_here = int(self.from_step_id.qty_at_step or 0)
if qty_here > 0 and self.qty_moved > qty_here:
raise UserError(_(
'Cannot move %(req)s parts only %(here)s currently '
'Cannot move %(req)s parts - only %(here)s currently '
'parked at "%(step)s". Adjust Qty Moved or split '
'across multiple moves.'
) % {
@@ -277,7 +277,7 @@ class FpJobStepMoveWizard(models.TransientModel):
'value_boolean': line.value_boolean,
'value_date': line.value_date or False,
}
# Ad-hoc rows (no node_input_id) preserve the operator's typed
# Ad-hoc rows (no node_input_id) - preserve the operator's typed
# prompt label in value_text so the chronological CoC report
# still shows what was measured.
if not line.node_input_id and line.name:
@@ -325,13 +325,13 @@ class FpJobStepMoveWizardInput(models.TransientModel):
a transient mirror means the wizard form can be filled, cancelled,
and reopened without polluting the chain-of-custody audit log.
2026-04-28 `node_input_id` is now optional so operators can add
2026-04-28 - `node_input_id` is now optional so operators can add
ad-hoc input rows directly from the Move dialog (operator types
the prompt label + value). Authored prompts still pre-fill
name + type as readonly; ad-hoc rows are fully editable. Same
pattern as the standalone Record Inputs wizard."""
_name = 'fp.job.step.move.wizard.input'
_description = 'Fusion Plating Move Wizard Input Row'
_description = 'Fusion Plating - Move Wizard Input Row'
wizard_id = fields.Many2one(
'fp.job.step.move.wizard',
@@ -355,7 +355,7 @@ class FpJobStepMoveWizardInput(models.TransientModel):
is_authored = fields.Boolean(
compute='_compute_is_authored',
help='True when this row originated from an authored recipe input. '
'Drives field readonly state authored prompts are locked, '
'Drives field readonly state - authored prompts are locked, '
'ad-hoc rows are fully editable.',
)

View File

@@ -48,7 +48,7 @@
</field>
<separator string="Notes"/>
<field name="notes" nolabel="1"
placeholder="Optional context why this move, what to watch for next..."/>
placeholder="Optional context - why this move, what to watch for next..."/>
</sheet>
<footer>
<button name="action_commit" type="object"