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

@@ -19,7 +19,7 @@ class FpCertificate(models.Model):
formats. Auto-created when reports are generated.
"""
_name = 'fp.certificate'
_description = 'Fusion Plating Certificate'
_description = 'Fusion Plating - Certificate'
_inherit = ['mail.thread', 'mail.activity.mixin', 'fp.parent.numbered.mixin']
_order = 'issue_date desc, id desc'
@@ -39,7 +39,7 @@ class FpCertificate(models.Model):
domain="[('customer_rank', '>', 0)]",
)
sale_order_id = fields.Many2one('sale.order', string='Sale Order')
# Phase 6 (Sub 11) production_id retired (MRP module gone).
# Phase 6 (Sub 11) - production_id retired (MRP module gone).
# Certificates link via sale_order_id + portal_job_id natively.
portal_job_id = fields.Many2one('fusion.plating.portal.job', string='Portal Job')
part_number = fields.Char(string='Part Number', help='Denormalized for fast search.')
@@ -52,7 +52,7 @@ class FpCertificate(models.Model):
quantity_shipped = fields.Integer(string='Qty Shipped')
nc_quantity = fields.Integer(
string='NC Qty',
help='Non-conforming quantity parts that failed inspection / rework.',
help='Non-conforming quantity - parts that failed inspection / rework.',
)
customer_job_no = fields.Char(
string='Customer Job No.',
@@ -69,7 +69,7 @@ class FpCertificate(models.Model):
'job ships. The first is the primary (its name, email and '
'phone print on the CoC); ALL of them are emailed when the '
'cert is sent to the customer. (Renamed from the single '
'contact_partner_id see migration 19.0.10.3.0.)',
'contact_partner_id - see migration 19.0.10.3.0.)',
)
issued_by_id = fields.Many2one(
'res.users', string='Issued By', default=lambda self: self.env.user,
@@ -78,7 +78,7 @@ class FpCertificate(models.Model):
'res.users', string='Certified By', help='Signing authority (e.g. Quality Manager).',
)
# ===== Sub 12c chronological CoC opt-in ===============================
# ===== Sub 12c - chronological CoC opt-in ===============================
body_style = fields.Selection(
[
('classic', 'Classic (recipe-order)'),
@@ -113,7 +113,7 @@ class FpCertificate(models.Model):
string='Fischerscope PDF filename',
)
# Non-PDF Fischerscope uploads (.doc / .docx / .xlsx / images) the
# Non-PDF Fischerscope uploads (.doc / .docx / .xlsx / images) - the
# Issue Certs wizard stashes them here so the thickness-required gate
# can still pass. Unlike `x_fc_local_thickness_pdf`, this attachment
# is NOT merged into the CoC PDF as page 2 (we can't rasterize .doc
@@ -129,7 +129,7 @@ class FpCertificate(models.Model):
'the CoC PDF.',
)
# Report-level Fischerscope metadata populated by the Issue Certs
# Report-level Fischerscope metadata - populated by the Issue Certs
# wizard when parsing an RTF/.docx upload. Rendered on the CoC so
# the printed cert shows the same context an auditor would see on
# the original XDAL 600 export (equipment, operator, calibration,
@@ -171,11 +171,11 @@ class FpCertificate(models.Model):
'parsed from.',
)
# Two paths populate this field, with operator upload winning:
# 1. RTF auto-extraction Issue Certs wizard runs libwmf
# 1. RTF auto-extraction - Issue Certs wizard runs libwmf
# (wmf2svg) on the embedded WMF blocks and picks the
# largest raster (header banners filtered by area threshold).
# 2. Manual PNG/JPEG upload via the wizard's "Measurement
# Image" field operator override path when the
# Image" field - operator override path when the
# auto-extracted image is wrong, missing, or low-quality.
# See _apply_to_cert and _apply_image_to_cert in the wizard.
x_fc_thickness_image_id = fields.Many2one(
@@ -203,7 +203,7 @@ class FpCertificate(models.Model):
@api.depends('sale_order_id')
def _compute_batch_ids(self):
# Phase 6 (Sub 11) walks fp.job via SO instead of mrp.production.
# Phase 6 (Sub 11) - walks fp.job via SO instead of mrp.production.
Batch = self.env.get('fusion.plating.batch')
Bath = self.env['fusion.plating.bath']
Job = self.env.get('fp.job')
@@ -235,7 +235,7 @@ class FpCertificate(models.Model):
string='Age (hours)',
compute='_compute_x_fc_age_hours',
help='Hours since the cert was created. Drives the Quality '
'Dashboard age chip and overdue filter. Non-stored '
'Dashboard age chip and overdue filter. Non-stored - '
'always fresh on read. Spec 2026-05-25.',
)
void_reason = fields.Text(string='Void Reason')
@@ -340,7 +340,7 @@ class FpCertificate(models.Model):
sigma = 0.0
rec.std_dev_mils = round(sigma, 4)
# Cpk requires spec limits + non-zero sigma
# Cpk - requires spec limits + non-zero sigma
usl = rec.spec_max_mils or 0.0
lsl = rec.spec_min_mils or 0.0
if n < 5 or sigma == 0 or (usl == 0 and lsl == 0):
@@ -404,7 +404,7 @@ class FpCertificate(models.Model):
def create(self, vals_list):
SaleOrder = self.env['sale.order']
for vals in vals_list:
# Spec-limit auto-fill sources thickness range from the
# Spec-limit auto-fill - sources thickness range from the
# recipe (Phase A moved the thickness fields onto the
# recipe root). Falls back gracefully when the SO has no
# recipe-bearing line.
@@ -449,12 +449,12 @@ class FpCertificate(models.Model):
Single source of truth shared by action_issue (hard gate) and the
Issue-Certs wizard (readiness hint) so the two can never drift.
Partner side the ceiling: a CoC needs thickness when the customer
Partner side - the ceiling: a CoC needs thickness when the customer
is strict-thickness (aerospace / Nadcap) or opts into the
thickness-on-CoC bundle; a thickness_report cert always needs it.
Recipe side suppress-only: a recipe whose requires_thickness_report
is False (passivation, chemical conversion, anodize seal-only no
Recipe side - suppress-only: a recipe whose requires_thickness_report
is False (passivation, chemical conversion, anodize seal-only - no
plating thickness physically exists) REMOVES the requirement even
when the customer asked. This mirrors Step 2 of
fp.job._resolve_required_cert_types so the cert-type resolver and
@@ -543,7 +543,7 @@ class FpCertificate(models.Model):
# the common case and this fill is what locks the signer at
# issue. fp.certificate doesn't declare company_id directly,
# so resolve the company from the SO (then env.company) the
# same way the CoC report does otherwise this fill is
# same way the CoC report does - otherwise this fill is
# skipped and the "Certified By is not set" gate below would
# block issuance.
if not rec.certified_by_id:
@@ -566,33 +566,33 @@ class FpCertificate(models.Model):
# when an estimator chooses to fill it, but it no longer
# blocks issuance.
# Process description (what was done to the parts). Without
# it the cert PDF just shows blank process text customer
# it the cert PDF just shows blank process text - customer
# has no idea what they paid for. Auto-filled from the
# recipe at create time; manager can override before issuing.
if not rec.process_description:
raise UserError(_(
'Cannot issue certificate "%(name)s" Process '
'Cannot issue certificate "%(name)s" - Process '
'Description is blank.\n\nFill it manually (e.g. '
'"ELECTROLESS NICKEL PLATING PER AMS 2404") or '
'assign a recipe to the job so it auto-fills.'
) % {'name': rec.name or rec.display_name})
# Signing authority the human who attests the work. Auto-
# Signing authority - the human who attests the work. Auto-
# filled from per-spec signer_user_id, falling back to
# company.x_fc_owner_user_id. If neither is configured, the
# manager must pick before issuing.
if not rec.certified_by_id:
raise UserError(_(
'Cannot issue certificate "%(name)s" Certified By '
'Cannot issue certificate "%(name)s" - Certified By '
'is not set.\n\nPick the signing authority, or have '
'an admin configure the company\'s Certificate Owner '
'(Settings > Fusion Plating).'
) % {'name': rec.name or rec.display_name})
# Customer contact the named recipient printed on the
# Customer contact - the named recipient printed on the
# cert and emailed when it ships. Auto-filled from the FIRST
# of partner.x_fc_default_coc_contact_ids when set.
if not rec.contact_partner_ids:
raise UserError(_(
'Cannot issue certificate "%(name)s" Customer '
'Cannot issue certificate "%(name)s" - Customer '
'Contact is not set.\n\nPick the recipient contact(s), '
'or configure Default CoC Contacts on customer '
'"%(cust)s".'
@@ -602,7 +602,7 @@ class FpCertificate(models.Model):
})
if not (rec.contact_partner_ids[:1].email or '').strip():
raise UserError(_(
'Cannot issue certificate "%(name)s" primary contact '
'Cannot issue certificate "%(name)s" - primary contact '
'"%(c)s" has no email address.\n\nAdd an email to the '
'contact before issuing (the cert is sent by email '
'post-issue).'
@@ -611,7 +611,7 @@ class FpCertificate(models.Model):
'c': rec.contact_partner_ids[:1].name,
})
# Orphan cert types (Nadcap / Mill Test / Customer-Specific)
# are manual-attach only operator uploads supplier doc /
# are manual-attach only - operator uploads supplier doc /
# regulator-issued cert / filled customer template. Block
# issuance until an attachment is present so the customer
# doesn't receive an empty PDF. See spec 2026-05-27.
@@ -622,7 +622,7 @@ class FpCertificate(models.Model):
rec._fields['certificate_type'].selection
).get(rec.certificate_type, rec.certificate_type)
raise UserError(_(
'Cannot issue %(type)s "%(name)s" no PDF attached.'
'Cannot issue %(type)s "%(name)s" - no PDF attached.'
'\n\nThis certificate type expects a PDF you upload '
'from disk (supplier doc / regulator-issued cert / '
'filled customer template). Upload the PDF to the '
@@ -631,14 +631,14 @@ class FpCertificate(models.Model):
'type': type_label,
'name': rec.name or rec.display_name,
})
# Thickness data requirement _fp_needs_thickness_data is the
# Thickness data requirement - _fp_needs_thickness_data is the
# single source of truth (shared with the Issue-Certs wizard).
# A customer needs thickness data when the cert is a
# thickness_report, or it's a CoC and the partner is
# strict-thickness / opts into the thickness-on-CoC bundle
# strict-thickness / opts into the thickness-on-CoC bundle -
# UNLESS the job's recipe suppresses thickness
# (requires_thickness_report=False: passivation, chemical
# conversion, anodize seal-only no plating thickness exists).
# conversion, anodize seal-only - no plating thickness exists).
# Acceptable data: logged readings on the cert OR a Fischerscope
# PDF on the linked QC OR a cert-local Fischerscope upload.
partner = rec.partner_id
@@ -660,7 +660,7 @@ class FpCertificate(models.Model):
else _('CoC')
)
raise UserError(_(
'Cannot issue %(type)s "%(name)s" customer '
'Cannot issue %(type)s "%(name)s" - customer '
'"%(cust)s" requires thickness data on every '
'%(type)s. No readings, no Fischerscope PDF on '
'the linked QC, and no local Fischerscope upload '
@@ -674,10 +674,10 @@ class FpCertificate(models.Model):
'cust': partner.name if partner else '?',
'so': rec.sale_order_id.name if rec.sale_order_id else '?',
})
# Defensive qty reconciliation should already be guaranteed
# Defensive qty reconciliation - should already be guaranteed
# by fp.job.button_mark_done's gate, but re-checked here so
# certs created outside the job flow (manual, scripts) still
# can't issue with a mismatched job. No bypass qty integrity
# can't issue with a mismatched job. No bypass - qty integrity
# is non-negotiable at issue.
job = (rec.x_fc_job_id
if 'x_fc_job_id' in rec._fields else False)
@@ -690,7 +690,7 @@ class FpCertificate(models.Model):
)
if abs(job.qty_received - accounted) > 0.0001:
raise UserError(_(
'Cannot issue certificate "%(name)s" job '
'Cannot issue certificate "%(name)s" - job '
'%(job)s qty mismatch (received %(r)g vs '
'accounted-out %(a)g). Reconcile job '
'quantities before issuing.'
@@ -752,7 +752,7 @@ class FpCertificate(models.Model):
if was_issued:
for rec in self:
if not was_issued.get(rec.id):
continue # wasn't issued no regress
continue # wasn't issued - no regress
if ('x_fc_job_id' in rec._fields and rec.x_fc_job_id
and hasattr(rec.x_fc_job_id,
'_fp_check_regress_after_cert_void')):
@@ -788,8 +788,8 @@ class FpCertificate(models.Model):
mandatory; the customer-facing description (what the estimator
actually typed on the order) now carries the descriptive text.
Resolution first non-empty wins:
1. The order line matching this cert's job/part cleaned via
Resolution - first non-empty wins:
1. The order line matching this cert's job/part - cleaned via
sale.order.line.fp_customer_description() (strips the
"[code] product" prefix Odoo re-prepends).
2. Any product line on the linked sale order.
@@ -880,7 +880,7 @@ class FpCertificate(models.Model):
the supporting evidence inline with the cert.
Tries the EN-language CoC report first, falls back to the
generic action_report_coc. Idempotent skips if attachment_id
generic action_report_coc. Idempotent - skips if attachment_id
is already set. PDF merge is best-effort: corrupt Fischerscope
upload or missing pypdf falls back to CoC-only with a warning.
"""
@@ -888,7 +888,7 @@ class FpCertificate(models.Model):
import io
self.ensure_one()
# Orphan cert types (Nadcap / Mill Test / Customer-Specific) are
# manual-attach only never render a CoC QWeb template for them.
# manual-attach only - never render a CoC QWeb template for them.
# action_issue's precondition gate already enforces the
# attachment requirement, so by the time we get here the operator
# has uploaded the right PDF. See spec 2026-05-27 §4.
@@ -950,9 +950,9 @@ class FpCertificate(models.Model):
if self.certificate_type != 'coc':
return None
# Resolution order for the source of the Fischerscope bytes:
# 1. Cert-local upload (x_fc_local_thickness_pdf) manager
# 1. Cert-local upload (x_fc_local_thickness_pdf) - manager
# dropped it directly on the cert form
# 2. Linked QC's thickness_report_pdf_id operator uploaded
# 2. Linked QC's thickness_report_pdf_id - operator uploaded
# via the tablet during inspection
# Either path yields the same merged-PDF outcome.
fischer_bytes = b''
@@ -993,7 +993,7 @@ class FpCertificate(models.Model):
)
if not fischer_bytes:
return None
# Merge pypdf is the modern name; PyPDF2 still works on older
# Merge - pypdf is the modern name; PyPDF2 still works on older
# Odoo bundles. Either is fine.
try:
from pypdf import PdfWriter
@@ -1013,7 +1013,7 @@ class FpCertificate(models.Model):
return None
try:
if use_append:
# pypdf 3.x PdfWriter.append() handles bytes/streams
# pypdf 3.x - PdfWriter.append() handles bytes/streams
writer = writer_cls()
writer.append(io.BytesIO(coc_pdf_bytes))
writer.append(io.BytesIO(fischer_bytes))
@@ -1021,7 +1021,7 @@ class FpCertificate(models.Model):
writer.write(out)
merged = out.getvalue()
else:
# PyPDF2 PdfMerger.append + write
# PyPDF2 - PdfMerger.append + write
merger = writer_cls()
merger.append(io.BytesIO(coc_pdf_bytes))
merger.append(io.BytesIO(fischer_bytes))
@@ -1031,7 +1031,7 @@ class FpCertificate(models.Model):
merged = out.getvalue()
except Exception:
_logger.exception(
'PDF merge failed for cert %s Fischerscope PDF may '
'PDF merge failed for cert %s - Fischerscope PDF may '
'be corrupt / encrypted / malformed. Falling back to '
'CoC-only.', self.name,
)
@@ -1048,7 +1048,7 @@ class FpCertificate(models.Model):
def action_reset_to_draft(self):
"""Move an issued/voided cert back to draft so the manager can
correct typos in the thickness metadata, swap the microscope
image, re-pick the void reason, etc. then re-Issue.
image, re-pick the void reason, etc. - then re-Issue.
Wipes the existing `attachment_id` so the next render picks up
whatever was changed. The original PDF stays around as a
@@ -1109,11 +1109,11 @@ class FpCertificate(models.Model):
def action_view_traceability(self):
"""Show the batches (and their chemistry logs) that produced
these parts auditor's dream, customer's RMA friend."""
these parts - auditor's dream, customer's RMA friend."""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _('Traceability %s') % self.name,
'name': _('Traceability - %s') % self.name,
'res_model': 'fusion.plating.batch',
'view_mode': 'list,form',
'domain': [('id', 'in', self.batch_ids.ids)],

View File

@@ -43,7 +43,7 @@ class FusionPlatingDelivery(models.Model):
draft_certs = Cert.search(dom)
if draft_certs:
raise UserError(_(
'Cannot mark delivery %(d)s shipped job '
'Cannot mark delivery %(d)s shipped - job '
'%(j)s still has %(n)d draft '
'certificate(s) (%(types)s). Issue them '
'first, or pass fp_skip_cert_gate=True '

View File

@@ -14,13 +14,13 @@ class FpThicknessReading(models.Model):
Data is manually entered for now; future: CSV import from equipment.
"""
_name = 'fp.thickness.reading'
_description = 'Fusion Plating Thickness Reading'
_description = 'Fusion Plating - Thickness Reading'
_order = 'reading_number'
certificate_id = fields.Many2one(
'fp.certificate', string='Certificate', ondelete='cascade',
)
# Phase 6 (Sub 11) production_id retired (MRP module gone).
# Phase 6 (Sub 11) - production_id retired (MRP module gone).
# Thickness readings link via certificate_id and quality_check_id.
reading_number = fields.Integer(
string='Reading #', default=1, help='Sequence number (n=1, n=2, n=3).',
@@ -73,5 +73,5 @@ class FpThicknessReading(models.Model):
if rec.nip_mils:
label = '%s (%.4f mils)' % (label, rec.nip_mils)
if ctx:
label = '%s %s' % (label, ctx)
label = '%s - %s' % (label, ctx)
rec.display_name = label

View File

@@ -15,7 +15,7 @@ class ResConfigSettings(models.TransientModel):
x_fc_owner_user_id = fields.Many2one(
related='company_id.x_fc_owner_user_id', readonly=False,
)
# x_fc_coc_signature_override was retired 2026-05-17 cert
# x_fc_coc_signature_override was retired 2026-05-17 - cert
# signatures now come from the certifier user's Plating Signature
# only. Field stays on res.company (no migration) but is no longer
# exposed in settings.

View File

@@ -49,9 +49,9 @@ class ResPartner(models.Model):
'for commercial customers.',
)
# Aerospace / Defence cert toggles (2026-05-27 sub
# Aerospace / Defence cert toggles (2026-05-27 - sub
# docs/superpowers/specs/2026-05-27-recipe-cert-toggles-design.md).
# Default False opt-in for aerospace/defence customers only.
# Default False - opt-in for aerospace/defence customers only.
# Resolver _resolve_required_cert_types reads these alongside the
# existing x_fc_send_coc / x_fc_send_thickness_report. The three
# cert types (nadcap_cert / mill_test / customer_specific) are
@@ -61,7 +61,7 @@ class ResPartner(models.Model):
default=False, tracking=True,
help='Auto-spawn a Nadcap-type fp.certificate when a job for '
'this customer reaches awaiting_cert. Operator attaches '
'the supplier/PRI-issued PDF before clicking Issue '
'the supplier/PRI-issued PDF before clicking Issue - '
'there is no QWeb auto-render for this type.',
)
x_fc_send_mill_test = fields.Boolean(
@@ -78,10 +78,10 @@ class ResPartner(models.Model):
'issuing.',
)
# ---- Sub 6 Per-contact communication routing -----------------------
# ---- Sub 6 - Per-contact communication routing -----------------------
# These five flags live on CHILD contacts under a company partner.
# When every contact under a company leaves them blank, the resolver
# falls back to the company's own `email` matching the pre-Sub-6
# falls back to the company's own `email` - matching the pre-Sub-6
# behaviour exactly. Admins opt in to per-contact routing by ticking
# the relevant flag on each contact row.
x_fc_receives_certs = fields.Boolean(
@@ -117,7 +117,7 @@ class ResPartner(models.Model):
'visibility into everything the shop sends out.',
)
# ---- Sub 12c+ Per-customer cert statement override ----------------
# ---- Sub 12c+ - Per-customer cert statement override ----------------
x_fc_cert_statement = fields.Text(
string='Cert Statement Override',
help='Override boilerplate text printed in the Certificate of '
@@ -134,7 +134,7 @@ class ResPartner(models.Model):
# when a job ships); the rest are CC'd when the CoC is emailed. Sales
# sets the list once per customer. Falls back to manual selection at
# action_issue time if blank. Self-referential M2m (renamed from the old
# single Many2one x_fc_default_coc_contact_id see migration 19.0.10.2.0).
# single Many2one x_fc_default_coc_contact_id - see migration 19.0.10.2.0).
x_fc_default_coc_contact_ids = fields.Many2many(
'res.partner',
relation='fp_default_coc_contact_rel',