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:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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.',
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user