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

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
# Note: `lib/` is NOT eagerly imported here Python's relative-import
# Note: `lib/` is NOT eagerly imported here - Python's relative-import
# machinery would otherwise re-enter this package mid-init when the
# wizard module does `from ..lib.fischerscope_parser import …`, raising
# "cannot import name X from partially initialized module" on Python

View File

@@ -4,12 +4,12 @@
# Part of the Fusion Plating product family.
{
'name': 'Fusion Plating Certificates',
'name': 'Fusion Plating - Certificates',
'version': '19.0.10.3.0',
'category': 'Manufacturing/Plating',
'summary': 'Certificate registry for CoC, thickness reports, and quality documents.',
'description': """
Fusion Plating Certificates
Fusion Plating - Certificates
===============================
Unified certificate registry tracking all quality documents issued to customers.

View File

@@ -1,3 +1,3 @@
# Parser libraries for fusion_plating_certificates.
# Pure-Python modules, no Odoo imports safe to unit-test in isolation.
# Pure-Python modules, no Odoo imports - safe to unit-test in isolation.
from . import fischerscope_parser # noqa: F401

View File

@@ -20,7 +20,7 @@ _logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Regexes derived from the real Fischerscope XDAL 600 export layout.
# Regexes - derived from the real Fischerscope XDAL 600 export layout.
# Sample line:
# n= 1 NiP 1= 0.6885 mils Ni 1 = 91.323 % P 1 = 8.6771 %
# Spaces vary; allow flexible whitespace + optional channel digit after NiP/Ni/P.
@@ -37,7 +37,7 @@ _READING_RE = re.compile(
re.VERBOSE,
)
# Equipment model first non-blank line that contains "Fischerscope" or
# Equipment model - first non-blank line that contains "Fischerscope" or
# similar gauge identifier. Captures everything up to end of line.
_EQUIPMENT_RE = re.compile(
r'(Fischerscope[^\n\r]*)',
@@ -113,7 +113,7 @@ def parse_fischerscope_file(filename, content_bytes):
'errors': [str], # non-fatal warnings encountered
}
Never raises on parse failure returns success=False with readings=[].
Never raises on parse failure - returns success=False with readings=[].
Raises only on unrecoverable I/O (e.g. corrupted file bytes).
"""
name = (filename or '').lower()
@@ -125,7 +125,7 @@ def parse_fischerscope_file(filename, content_bytes):
return _failed_result(
raw_text='',
error=(
'Legacy .doc format not supported re-export from the '
'Legacy .doc format not supported - re-export from the '
'gauge as .docx or .pdf. (python-docx reads .docx only; '
'old binary .doc needs LibreOffice conversion which '
"isn't installed.)"
@@ -149,7 +149,7 @@ def _parse_docx(content_bytes):
except ImportError:
return _failed_result(
raw_text='',
error='python-docx not installed cannot parse .docx files.',
error='python-docx not installed - cannot parse .docx files.',
)
try:
doc = docx.Document(io.BytesIO(content_bytes))
@@ -157,7 +157,7 @@ def _parse_docx(content_bytes):
return _failed_result(raw_text='', error='Could not open .docx: %s' % e)
# Build the raw text by walking paragraphs AND tables. Fischerscope
# exports vary sometimes the readings are in a table, sometimes
# exports vary - sometimes the readings are in a table, sometimes
# in justified paragraphs. Joining everything gives the regex a
# stable target.
parts = []
@@ -197,7 +197,7 @@ def _parse_pdf(content_bytes):
except ImportError:
return _failed_result(
raw_text='',
error='PyPDF2 not installed cannot parse .pdf files.',
error='PyPDF2 not installed - cannot parse .pdf files.',
)
try:
reader = PdfReader(io.BytesIO(content_bytes))
@@ -214,7 +214,7 @@ def _parse_pdf(content_bytes):
# PDF image extraction is unreliable across PDF producers. Best-
# effort: walk page resources looking for /XObject /Image entries.
# If anything fails, drop image silently the operator still has
# If anything fails, drop image silently - the operator still has
# the original file attached.
image_bytes = None
image_mime = None

View File

@@ -32,7 +32,7 @@ def migrate(cr, version):
""")
if not cr.fetchone():
_logger.warning(
'fp_default_coc_contact_rel missing skipping CoC contact '
'fp_default_coc_contact_rel missing - skipping CoC contact '
'migration (rel table not created yet).')
return
# Copy the single value into the M2m (skip rows already present so a

View File

@@ -30,7 +30,7 @@ def migrate(cr, version):
""")
if not cr.fetchone():
_logger.warning(
'fp_certificate_contact_partner_rel missing skipping cert '
'fp_certificate_contact_partner_rel missing - skipping cert '
'customer-contact migration (rel table not created yet).')
return
cr.execute("""

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Phase 6 (Sub 11) drop legacy MRP columns from certificate tables.
# Phase 6 (Sub 11) - drop legacy MRP columns from certificate tables.
import logging
_logger = logging.getLogger(__name__)

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1
"""Post-migrate for 19.0.9.0.0 Aerospace/Defence cert partner toggles.
"""Post-migrate for 19.0.9.0.0 - Aerospace/Defence cert partner toggles.
Backfills NULL -> FALSE on the three new Boolean columns added to
res.partner (x_fc_send_nadcap_cert, x_fc_send_mill_test,
@@ -11,7 +11,7 @@ flipped on by the admin).
Without this backfill, existing partner rows would read NULL on
the new columns and the resolver's `if p.x_fc_send_nadcap_cert`
check would short-circuit cleanly anyway but explicit FALSE makes
check would short-circuit cleanly anyway - but explicit FALSE makes
SQL queries / reports cleaner and matches the existing partner-flag
storage convention.
"""

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

View File

@@ -58,7 +58,7 @@ class TestActionIssueGates(TransactionCase):
# ---- spec_reference is OPTIONAL (client request 2026-05-28) ----
def test_no_block_on_missing_spec_reference(self):
"""Spec Reference no longer gates issuance the customer-facing
"""Spec Reference no longer gates issuance - the customer-facing
description now serves as the cert's spec/certificate info. A
cert with everything else present must issue even with a blank
spec_reference."""
@@ -124,7 +124,7 @@ class TestActionIssueGates(TransactionCase):
def test_blocks_thickness_report_with_no_data(self):
"""A thickness_report cert with zero readings and no Fischerscope
PDF is empty paper must block at issue."""
PDF is empty paper - must block at issue."""
cert = self._make_cert(certificate_type='thickness_report')
with self.assertRaises(UserError) as exc:
cert.action_issue()
@@ -141,7 +141,7 @@ class TestActionIssueGates(TransactionCase):
def test_coc_does_not_require_thickness_data_by_default(self):
"""Commercial CoC (no strict_thickness flag) should still pass
even without readings only thickness_report type is gated."""
even without readings - only thickness_report type is gated."""
cert = self._make_cert(certificate_type='coc')
cert.action_issue()
self.assertEqual(cert.state, 'issued')

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
#
# Unit tests for the Fischerscope thickness-report parser.
# Pure-Python tests no Odoo DB needed. Builds synthetic .docx files
# Pure-Python tests - no Odoo DB needed. Builds synthetic .docx files
# matching the real XDAL 600 export layout and verifies extraction.
import io
@@ -26,11 +26,11 @@ class TestFischerscopeParser(TransactionCase):
def setUpClass(cls):
super().setUpClass()
try:
import docx # python-docx required for tests
import docx # python-docx - required for tests
cls.docx = docx
except ImportError:
cls.docx = None
# Resolve the parser by absolute path at first use relative
# Resolve the parser by absolute path at first use - relative
# `from ..lib import` at module top trips the test loader's
# partially-initialised-package check.
from odoo.addons.fusion_plating_certificates.lib import (

View File

@@ -39,13 +39,13 @@
<field name="arch" type="xml">
<form>
<header>
<!-- Phase D5 Nadcap-cert restriction enforced at MODEL
<!-- Phase D5 - Nadcap-cert restriction enforced at MODEL
layer via ir.rule (rule_fp_certificate_nadcap_qm_only
in fp_cert_security.xml). Single Issue button visible
to all Manager+ when state=draft. Manager clicking
Issue on a Nadcap cert gets AccessError from the rule.
(Strategy B with user_has_groups() inside invisible=
was rejected by Odoo 19 view validator see CLAUDE.md
was rejected by Odoo 19 view validator - see CLAUDE.md
rule 13f.) -->
<button name="action_issue" string="Issue"
type="object" class="btn-primary"
@@ -91,7 +91,7 @@
<field name="name" readonly="1"/>
</h1>
</div>
<!-- Main info collapsed from 3 separate groups
<!-- Main info - collapsed from 3 separate groups
into 1 to eliminate the dead rows that
appeared when one sub-group ran shorter than
the other. Left column is identity / signer /
@@ -125,11 +125,11 @@
<field name="mean_nip_mils" readonly="1"/>
</group>
</group>
<!-- SPC rebalanced spec/min/max on the left,
<!-- SPC rebalanced - spec/min/max on the left,
derived stats on the right; trend_explanation
spans both columns so the long message doesn't
get cropped. -->
<group string="SPC Statistical Process Control">
<group string="SPC - Statistical Process Control">
<group>
<field name="spec_min_mils"/>
<field name="spec_max_mils"/>
@@ -169,7 +169,7 @@
</page>
<page string="Certificate PDF" name="pdf">
<!-- Manual-attach banner for orphan cert types
(Nadcap / Mill Test / Customer-Specific)
(Nadcap / Mill Test / Customer-Specific) -
spec 2026-05-27. action_issue refuses to
finalize these types without a PDF. -->
<div class="alert alert-warning" role="alert"

View File

@@ -16,7 +16,7 @@
<field name="domain">[('certificate_type', '=', 'thickness_report')]</field>
</record>
<!-- Phase 1 re-parented under Plating → Quality. Certificates are
<!-- Phase 1 - re-parented under Plating → Quality. Certificates are
a quality output, not a separate top-level concern. -->
<menuitem id="menu_fp_certificates"
name="Certificates"

View File

@@ -71,7 +71,7 @@
</field>
</record>
<!-- Sub 6 Per-contact routing flags on the contact's own form
<!-- Sub 6 - Per-contact routing flags on the contact's own form
(opens when editing a child contact row). Applied to every
res.partner form so delivery-location partners and their child
contacts both surface the same flags. -->

View File

@@ -14,7 +14,7 @@ from odoo.exceptions import UserError
class FpCertVoidWizard(models.TransientModel):
_name = 'fp.cert.void.wizard'
_description = 'Fusion Plating Void Certificate Wizard'
_description = 'Fusion Plating - Void Certificate Wizard'
cert_id = fields.Many2one(
'fp.certificate', string='Certificate', required=True, readonly=True,

View File

@@ -31,7 +31,7 @@
</group>
<group>
<field name="void_reason"
placeholder="e.g. Customer rejected lot re-plating required. Replaced by CoC-30041."
placeholder="e.g. Customer rejected lot - re-plating required. Replaced by CoC-30041."
nolabel="1"/>
</group>
</sheet>

View File

@@ -10,7 +10,7 @@
#
# When the parser extracts ≥1 reading, the wizard enters "review" state
# and the editable reading table is shown. When 0 readings are found,
# the wizard enters "manual" state the operator can still save the
# the wizard enters "manual" state - the operator can still save the
# file as-is (attach-only fallback). Either way the file ends up in
# place to satisfy the action_issue thickness gate.
@@ -20,7 +20,7 @@ import logging
from odoo import _, api, fields, models
from odoo.exceptions import UserError
# Lazy parser import `from ..lib.fischerscope_parser import …` at
# Lazy parser import - `from ..lib.fischerscope_parser import …` at
# module top fails on Python 3.11+ because the parent package
# `fusion_plating_certificates` is still mid-init when wizards/__init__
# imports this file (relative traversal into a partially-loaded parent
@@ -44,7 +44,7 @@ class FpThicknessUploadWizard(models.TransientModel):
state = fields.Selection(
[('upload', 'Upload file'),
('review', 'Review parsed readings'),
('manual', 'Parse failed attach only')],
('manual', 'Parse failed - attach only')],
default='upload', required=True,
)
@@ -133,7 +133,7 @@ class FpThicknessUploadWizard(models.TransientModel):
raise UserError(_('Wizard has no certificate to write to.'))
if cert.state != 'draft':
raise UserError(_(
'Cannot attach thickness data certificate %s is in '
'Cannot attach thickness data - certificate %s is in '
'state %s. Only draft certificates can be edited.'
) % (cert.display_name, cert.state))
@@ -157,7 +157,7 @@ class FpThicknessUploadWizard(models.TransientModel):
'mimetype': self.parsed_image_mime or 'image/jpeg',
})
# Write reading rows same metadata copied onto every row
# Write reading rows - same metadata copied onto every row
# (decision confirmed 2026-05-19, so each row is fully self-
# describing for downstream queries / reports).
if self.reading_line_ids:
@@ -189,7 +189,7 @@ class FpThicknessUploadWizard(models.TransientModel):
# Chatter audit
n = len(self.reading_line_ids)
body = (
_('Fischerscope thickness report uploaded %d reading(s) '
_('Fischerscope thickness report uploaded - %d reading(s) '
'parsed from %s.') % (n, self.file_name or 'file')
if n else
_('Fischerscope thickness file attached (parse returned no '
@@ -228,7 +228,7 @@ class FpThicknessUploadWizard(models.TransientModel):
class FpThicknessUploadWizardLine(models.TransientModel):
"""Editable reading row in the upload wizard."""
_name = 'fp.thickness.upload.wizard.line'
_description = 'Thickness Upload Wizard Reading'
_description = 'Thickness Upload Wizard - Reading'
_order = 'reading_number'
wizard_id = fields.Many2one(
@@ -240,5 +240,5 @@ class FpThicknessUploadWizardLine(models.TransientModel):
p_percent = fields.Float(string='P %', digits=(6, 4))
position_label = fields.Char(
string='Position',
help='Optional where on the part this reading was taken.',
help='Optional - where on the part this reading was taken.',
)

View File

@@ -97,7 +97,7 @@
<strong>Couldn't parse readings.</strong>
The file format didn't match what we recognise
(Fischerscope XDAL 600 export). You can still save it
as-is the file will attach to the certificate and
as-is - the file will attach to the certificate and
flow into the CoC PDF as page 2, but the readings
won't appear as queryable rows.
</div>
@@ -122,7 +122,7 @@
</record>
<!-- ================================================================== -->
<!-- Window action opened from the cert form button -->
<!-- Window action - opened from the cert form button -->
<!-- ================================================================== -->
<record id="action_fp_thickness_upload_wizard" model="ir.actions.act_window">
<field name="name">Upload Thickness Report</field>