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

@@ -26,27 +26,27 @@ from . import res_config_settings
from . import res_partner
from . import fp_part_catalog
# Phase 1 of MRP cut-out (Sub 11) relocated from fusion_plating_bridge_mrp.
# Phase 1 of MRP cut-out (Sub 11) - relocated from fusion_plating_bridge_mrp.
from . import fp_qc_template
from . import fp_thickness_reading
from . import fp_quality_check
# Sub 12 Phase A native RMA + the inverse fields it hangs off existing
# Sub 12 Phase A - native RMA + the inverse fields it hangs off existing
# quality and receiving models.
from . import fp_rma
from . import fp_rma_links
# Sub 12 Phase B categorisation primitives + cross-model link fields.
# Sub 12 Phase B - categorisation primitives + cross-model link fields.
from . import fp_quality_tag
from . import fp_quality_reason
from . import fp_quality_team
from . import fp_quality_alert_stage
from . import fp_quality_categorisation_links
# Sub 12 Phase C trigger-based quality points.
# Sub 12 Phase C - trigger-based quality points.
from . import fp_quality_point
from . import fp_quality_point_hooks
# Sub 12 Phase D smart-button counts + cross-creation actions.
# Sub 12 Phase D - smart-button counts + cross-creation actions.
from . import fp_quality_smart_buttons
from . import fp_quality_cross_creation

View File

@@ -14,7 +14,7 @@ class FpAudit(models.Model):
relationship.
"""
_name = 'fusion.plating.audit'
_description = 'Fusion Plating Audit'
_description = 'Fusion Plating - Audit'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'audit_date desc, id desc'

View File

@@ -15,7 +15,7 @@ class FpAvl(models.Model):
in the list for traceability.
"""
_name = 'fusion.plating.avl'
_description = 'Fusion Plating Approved Vendor List'
_description = 'Fusion Plating - Approved Vendor List'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'state, name'
_rec_name = 'name'
@@ -72,7 +72,7 @@ class FpAvl(models.Model):
)
approved_for = fields.Char(
string='Approved For',
help='Free text what processes / products / services this vendor '
help='Free text - what processes / products / services this vendor '
'is approved to supply.',
)
notes = fields.Html(

View File

@@ -17,7 +17,7 @@ class FpCalibrationEquipment(models.Model):
fusion.plating.calibration.event.
"""
_name = 'fusion.plating.calibration.equipment'
_description = 'Fusion Plating Calibrated Equipment'
_description = 'Fusion Plating - Calibrated Equipment'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'next_cal_date asc, name'

View File

@@ -15,7 +15,7 @@ class FpCalibrationEvent(models.Model):
which jobs may have been measured by an out-of-tolerance instrument.
"""
_name = 'fusion.plating.calibration.event'
_description = 'Fusion Plating Calibration Event'
_description = 'Fusion Plating - Calibration Event'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'cal_date desc, id desc'
_rec_name = 'display_name'
@@ -88,6 +88,6 @@ class FpCalibrationEvent(models.Model):
def _compute_display_name(self):
for rec in self:
if rec.equipment_id and rec.cal_date:
rec.display_name = f'{rec.equipment_id.code} {rec.cal_date}'
rec.display_name = f'{rec.equipment_id.code} - {rec.cal_date}'
else:
rec.display_name = 'Calibration Event'

View File

@@ -15,7 +15,7 @@ class FpCapa(models.Model):
action plan, and an effectiveness verification step.
"""
_name = 'fusion.plating.capa'
_description = 'Fusion Plating Corrective / Preventive Action'
_description = 'Fusion Plating - Corrective / Preventive Action'
_inherit = ['mail.thread', 'mail.activity.mixin', 'fp.parent.numbered.mixin']
_order = 'due_date asc, id desc'
@@ -108,7 +108,7 @@ class FpCapa(models.Model):
def _fp_parent_sale_order(self):
# CAPA usually flows from an NCR. If the NCR has a job-back-link
# (added by future modules), we can reach SO through it. For now
# there's no link in core falls back to legacy seq.
# there's no link in core - falls back to legacy seq.
return self.env['sale.order']
def _fp_name_prefix(self):
@@ -191,7 +191,7 @@ class FpCapa(models.Model):
def action_close(self):
"""Block close unless root_cause + action_plan + verification are set.
A CAPA without these is just an open ticket the AS9100 §10.2
A CAPA without these is just an open ticket - the AS9100 §10.2
/ Nadcap loop requires evidence of the root cause analysis,
the corrective/preventive action plan, AND that effectiveness
was verified before the loop is closed.
@@ -214,11 +214,11 @@ class FpCapa(models.Model):
missing.append(_('Verification (date + verifier)'))
if rec.is_effective is False and is_empty_html(rec.effectiveness_notes):
# If marked not-effective, demand a note explaining the
# follow-up plan otherwise the loop never actually closes.
# follow-up plan - otherwise the loop never actually closes.
missing.append(_('Effectiveness Notes (required when "Not Effective")'))
if missing:
raise UserError(_(
'Cannot close CAPA "%(name)s" these fields must be '
'Cannot close CAPA "%(name)s" - these fields must be '
'filled in first:\n%(fields)s\n\n'
'A CAPA without root cause + action plan + verified '
'effectiveness fails AS9100 §10.2 / Nadcap on audit.'

View File

@@ -82,7 +82,7 @@ class FpContractReview(models.Model):
qty = fields.Integer(string='Qty')
due_date = fields.Date(string='Due')
# ---- Section 2.0 Planning / Production Review (Planning Review) ------
# ---- Section 2.0 - Planning / Production Review (Planning Review) ------
s20_acceptable_lead_time = fields.Boolean(string='Acceptable Lead Time')
s20_capacity_to_process = fields.Boolean(string='Capacity to Process')
@@ -121,7 +121,7 @@ class FpContractReview(models.Model):
copy=False,
)
# ---- Section 3.0 Quality Review (QA Review) --------------------------
# ---- Section 3.0 - Quality Review (QA Review) --------------------------
s30_source_control_docs = fields.Boolean(string="Source Control Documents (Customer Spec's)")
s30_quality_clauses_supplied = fields.Boolean(string='Quality Clause(s) supplied')
@@ -137,19 +137,19 @@ class FpContractReview(models.Model):
s30_accepted = fields.Boolean(string='Accepted (Section 3.0)')
s30_evaluate_risk = fields.Boolean(string='Evaluate Risk (Section 3.0)')
s30_risk_consequence = fields.Selection(
[('1', '1 Minimal'),
('2', '2 Moderate'),
('3', '3 Mod. / Applicable'),
('4', '4 Major / Changes'),
('5', '5 Unacceptable')],
[('1', '1 - Minimal'),
('2', '2 - Moderate'),
('3', '3 - Mod. / Applicable'),
('4', '4 - Major / Changes'),
('5', '5 - Unacceptable')],
string='Consequence',
)
s30_risk_likelihood = fields.Selection(
[('1', '1 Not Likely'),
('2', '2 Low Likelihood'),
('3', '3 Likely'),
('4', '4 Highly Likely'),
('5', '5 Near Certainty')],
[('1', '1 - Not Likely'),
('2', '2 - Low Likelihood'),
('3', '3 - Likely'),
('4', '4 - Highly Likely'),
('5', '5 - Near Certainty')],
string='Likelihood',
)
s30_risk_band = fields.Selection(
@@ -193,14 +193,14 @@ class FpContractReview(models.Model):
tracking=True,
)
# ---- "Failed QA Awaiting Client Info" workflow ------------------------
# ---- "Failed QA - Awaiting Client Info" workflow ------------------------
# When a QA Signer (Brett or whoever the company has rostered) finds a
# client requirement that fails during the QA Review, they mark the
# review failed. The state moves to `awaiting_info`, an activity is
# scheduled for every QA Signer to follow up, and a smart button on
# the form gives them a one-click email composer to ping the client.
# When the client replies, the QA Signer captures notes in
# `special_instructions` and marks complete the notes print on the
# `special_instructions` and marks complete - the notes print on the
# final QA-005 PDF for the audit trail.
qa_failure_reason = fields.Html(
string='QA Failure Reason',
@@ -232,7 +232,7 @@ class FpContractReview(models.Model):
)
client_email_count = fields.Integer(
compute='_compute_client_email_count',
help='Smart-button counter number of emails posted to chatter '
help='Smart-button counter - number of emails posted to chatter '
'against this review. Always non-zero after the first send.',
)
@@ -257,7 +257,7 @@ class FpContractReview(models.Model):
def _compute_name(self):
for rec in self:
if rec.part_id:
rec.name = _('QA-005 %(pn)s Rev %(rev)s') % {
rec.name = _('QA-005 - %(pn)s Rev %(rev)s') % {
'pn': rec.part_id.part_number or _('(no part#)'),
'rev': rec.part_id.revision or _('(no rev)'),
}
@@ -313,7 +313,7 @@ class FpContractReview(models.Model):
return True
# Checklist fields per section, for the "Check All" / "Clear All"
# bulk-toggle buttons. Only the checklist boxes are flipped
# bulk-toggle buttons. Only the checklist boxes are flipped -
# outcome fields (Accepted, Evaluate Risk, Risk Level / Matrix,
# Mitigation Plan Required) remain under the user's explicit
# decision so they don't get accidentally ticked.
@@ -347,7 +347,7 @@ class FpContractReview(models.Model):
self.ensure_one()
if self[locked_field]:
raise UserError(_(
'Section is already signed checklist is locked.'
'Section is already signed - checklist is locked.'
))
self.write({f: value for f in fields_tuple})
return True
@@ -405,7 +405,7 @@ class FpContractReview(models.Model):
'fusion_plating_quality.action_report_contract_review'
).report_action(self)
# ---- "Failed QA Awaiting Client Info" workflow ------------------------
# ---- "Failed QA - Awaiting Client Info" workflow ------------------------
def action_mark_qa_failed(self):
"""QA Signer marks the review failed because a client requirement
is missing or unclear. Captures the reason, flips state to
@@ -418,13 +418,13 @@ class FpContractReview(models.Model):
'Only a review at the QA Review (or Planning Review) stage '
'can be flagged as failed. Current state: %s.'
) % dict(self._fields['state'].selection).get(self.state, self.state))
# Reuse the section-30 signer roster the same group of people
# Reuse the section-30 signer roster - the same group of people
# who can sign QA can flag a QA failure.
self._check_signer(30)
if not self.qa_failure_reason or not self.qa_failure_reason.strip():
raise UserError(_(
'Capture the QA Failure Reason before flagging the '
'review failed the reason pre-fills the client email '
'review failed - the reason pre-fills the client email '
'and is required for the audit trail.'
))
self.write({'state': 'awaiting_info'})
@@ -449,7 +449,7 @@ class FpContractReview(models.Model):
for user in signers:
self.activity_schedule(
activity_type_id=activity_type.id if activity_type else False,
summary=_('Follow up on QA-005 client info required'),
summary=_('Follow up on QA-005 - client info required'),
note=self.qa_failure_reason or '',
user_id=user.id,
date_deadline=fields.Date.context_today(self) +
@@ -458,14 +458,14 @@ class FpContractReview(models.Model):
return True
def action_open_client_email_wizard(self):
"""Smart-button target opens the email composer wizard pre-filled
"""Smart-button target - opens the email composer wizard pre-filled
with the customer's contact email + a body templated from the
QA failure reason. The wizard handles the actual mail.mail send
and stamps `info_requested_date` on this review."""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _('Email Client Request Info'),
'name': _('Email Client - Request Info'),
'res_model': 'fp.contract.review.client.email.wizard',
'view_mode': 'form',
'target': 'new',
@@ -479,12 +479,12 @@ class FpContractReview(models.Model):
}
def action_view_client_emails(self):
"""Drill-down behind the smart button counter shows the chatter
"""Drill-down behind the smart button counter - shows the chatter
messages of type=email for this review."""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _('Client Emails %s') % self.name,
'name': _('Client Emails - %s') % self.name,
'res_model': 'mail.message',
'view_mode': 'list,form',
'domain': [
@@ -520,7 +520,7 @@ class FpContractReview(models.Model):
# everyone's inbox once the case is closed.
self.activity_feedback(
['mail.mail_activity_data_todo'],
feedback=_('Client info received review closed.'),
feedback=_('Client info received - review closed.'),
)
self.message_post(body=Markup(_(
'<b>QA Review completed</b> by %(user)s after receiving '

View File

@@ -11,11 +11,11 @@ class FpCustomerSpec(models.Model):
Holds the metadata about a specification (industry, customer, or
internal) so jobs and process types can reference it. The actual
document lives at document_url could be a SharePoint link, a
document lives at document_url - could be a SharePoint link, a
Google Drive URL, or any other location the shop already uses.
"""
_name = 'fusion.plating.customer.spec'
_description = 'Fusion Plating Customer Specification'
_description = 'Fusion Plating - Customer Specification'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'spec_type, code, revision desc'
_rec_name = 'display_name'
@@ -81,7 +81,7 @@ class FpCustomerSpec(models.Model):
domain="[('node_type', '=', 'recipe'), ('parent_id', '=', False)]",
string='Applicable Recipes',
help='Recipes that can produce work to this specification. '
'Many-to-many one spec can cover multiple processes; '
'Many-to-many - one spec can cover multiple processes; '
'one recipe can satisfy multiple specs.',
)
print_on_cert = fields.Boolean(
@@ -112,5 +112,5 @@ class FpCustomerSpec(models.Model):
if rec.revision:
parts.append(f'Rev {rec.revision}')
if rec.name:
parts.append(f' {rec.name}')
parts.append(f'- {rec.name}')
rec.display_name = ' '.join(p for p in parts if p)

View File

@@ -28,10 +28,10 @@ class FpDirectOrderLine(models.Model):
Priority (first non-empty result wins, matches the same
"remember last entered" pattern used by process / unit_price /
tax / thickness in the base wizard line):
1. What the operator already typed on this line never clobber
1. What the operator already typed on this line - never clobber
2. Most recent SO line for (part, customer) where a spec
was set the "repeat order" carry-over
3. Part's stored default x_fc_default_customer_spec_id
was set - the "repeat order" carry-over
3. Part's stored default - x_fc_default_customer_spec_id
"""
for rec in self:
if not rec.part_catalog_id or rec.customer_spec_id:
@@ -67,7 +67,7 @@ class FpDirectOrderWizard(models.Model):
pair wizard lines to SO lines by sequence and patch.
The push-to-defaults block mirrors the base wizard's thickness
push (action_create_order's "6. Push-to-defaults" loop) spec
push (action_create_order's "6. Push-to-defaults" loop) - spec
lives here in the quality module so the back-write lives here
too. Only fills when the part default is currently empty so we
never clobber an existing default that the part-form user set
@@ -84,7 +84,7 @@ class FpDirectOrderWizard(models.Model):
for wiz_line, so_line in zip(wiz_lines, so_lines):
if wiz_line.customer_spec_id and not so_line.x_fc_customer_spec_id:
so_line.x_fc_customer_spec_id = wiz_line.customer_spec_id.id
# Spec push-to-default only on first-time parts that
# Spec push-to-default - only on first-time parts that
# had the toggle auto-ticked (or manually ticked). Skip
# one-off parts and parts that already have a default.
if (wiz_line.push_to_defaults

View File

@@ -15,7 +15,7 @@ class FpDocControl(models.Model):
on this revision (a common audit ask).
"""
_name = 'fusion.plating.doc.control'
_description = 'Fusion Plating Controlled Document'
_description = 'Fusion Plating - Controlled Document'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'doc_type, name, revision desc'

View File

@@ -15,7 +15,7 @@ class FpFair(models.Model):
PPAP needs a FAIR on file.
"""
_name = 'fusion.plating.fair'
_description = 'Fusion Plating First Article Inspection Report'
_description = 'Fusion Plating - First Article Inspection Report'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'performed_date desc, id desc'

View File

@@ -11,12 +11,12 @@ class FpNcr(models.Model):
"""Non-Conformance Report.
The NCR is the entry point of the Fusion Plating QMS. Anything that
falls outside of spec a chemistry deviation, a customer return, an
inspection failure, an audit observation is opened as an NCR and
falls outside of spec - a chemistry deviation, a customer return, an
inspection failure, an audit observation - is opened as an NCR and
walked through containment, disposition, and closure.
"""
_name = 'fusion.plating.ncr'
_description = 'Fusion Plating Non-Conformance Report'
_description = 'Fusion Plating - Non-Conformance Report'
_inherit = ['mail.thread', 'mail.activity.mixin', 'fp.parent.numbered.mixin']
_order = 'reported_date desc, id desc'
@@ -132,7 +132,7 @@ class FpNcr(models.Model):
# ------------------------------------------------------------------
# Parent-numbered mixin hooks
# NCRs don't have a direct SO/job link in core yet falls back to
# NCRs don't have a direct SO/job link in core yet - falls back to
# legacy fusion.plating.ncr sequence. When a future module adds a
# link, it can override _fp_parent_sale_order to enable parent
# naming retroactively without further changes here.
@@ -211,7 +211,7 @@ class FpNcr(models.Model):
missing.append(_('Disposition (use-as-is / rework / scrap / RTV)'))
if missing:
raise UserError(_(
'Cannot close NCR "%(name)s" these fields must be '
'Cannot close NCR "%(name)s" - these fields must be '
'filled in first:\n%(fields)s\n\n'
'AS9100 / Nadcap auditors will reject a closed NCR '
'that doesn\'t document what happened, why, and how '

View File

@@ -56,7 +56,7 @@ class FpPartCatalog(models.Model):
def _compute_has_confirmed_mo(self):
"""True if this part is referenced by at least one live fp.job.
Sub 11 replaced mrp.production lookup with fp.job. Trace:
Sub 11 - replaced mrp.production lookup with fp.job. Trace:
fp.part.catalog → sale.order.line (x_fc_part_catalog_id) →
sale.order → fp.job (via origin name match).
"""
@@ -96,7 +96,7 @@ class FpPartCatalog(models.Model):
and not completed and not in_production
)
# ---- Create override auto-stage + alert -------------------------------
# ---- Create override - auto-stage + alert -------------------------------
@api.model_create_multi
def create(self, vals_list):
@@ -104,7 +104,7 @@ class FpPartCatalog(models.Model):
part is added under a customer that has contract-review
enforcement enabled (res.partner.x_fc_contract_review_required).
Fires only on .create() write/update flows never re-trigger
Fires only on .create() - write/update flows never re-trigger
the alert. Existing parts in the system are unaffected.
Three surfaces, in increasing persistence:
@@ -168,7 +168,7 @@ class FpPartCatalog(models.Model):
)
customer_label = part.partner_id.display_name
# 1) Persistent activity sits in the user's Activities
# 1) Persistent activity - sits in the user's Activities
# inbox + shows on the part record's chatter clock until
# the user marks it done.
if activity_type and model_id:
@@ -197,14 +197,14 @@ class FpPartCatalog(models.Model):
part.id, exc_info=True,
)
# 2) Sticky warning toast visible immediately to the
# 2) Sticky warning toast - visible immediately to the
# user who created the part. Doesn't auto-dismiss.
try:
Bus._sendone(
self.env.user.partner_id,
'simple_notification',
{
'title': _('Contract Review Required %s')
'title': _('Contract Review Required - %s')
% part_label,
'message': _(
'Customer %(c)s requires a Contract Review '

View File

@@ -11,9 +11,9 @@ class FpPricingRule(models.Model):
Lives in fusion_plating_quality because fusion.plating.customer.spec
lives here. Rules can now match on:
- customer_spec_id (most specific e.g. "AMS 2404 surcharge")
- recipe_id (recipe-tier e.g. "EN Mid-Phos $X/sqft")
- both blank (fallback material/cert-level matching)
- customer_spec_id (most specific - e.g. "AMS 2404 surcharge")
- recipe_id (recipe-tier - e.g. "EN Mid-Phos $X/sqft")
- both blank (fallback - material/cert-level matching)
The configurator's matcher is extended in fp_quote_configurator_inherit.
"""

View File

@@ -3,22 +3,22 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
#
# Phase 1 (Sub 11) relocated from fusion_plating_bridge_mrp.
# Phase 1 (Sub 11) - relocated from fusion_plating_bridge_mrp.
# This model never had MRP fields; the bridge module was just its
# initial home. Now lives under fusion_plating_jobs.
"""QC Checklist Template admin config for per-customer QC requirements.
"""QC Checklist Template - admin config for per-customer QC requirements.
Customers differ wildly in what they expect from quality control:
* commercial job-shop accounts often just want "did it plate?" one
* commercial job-shop accounts often just want "did it plate?" - one
visual check
* aerospace / Nadcap customers expect visual, dimensional,
adhesion, and Fischerscope thickness readings every part, every
adhesion, and Fischerscope thickness readings - every part, every
lot, signed off
* internal rework jobs may have no QC requirement at all
Rather than coding that policy into the shop, each customer gets their
own checklist template. On job confirm, the active template is cloned
into a fresh `fusion.plating.quality.check` the instance operators
into a fresh `fusion.plating.quality.check` - the instance operators
actually fill in.
"""
from odoo import api, fields, models, _
@@ -26,14 +26,14 @@ from odoo import api, fields, models, _
class FpQcChecklistTemplate(models.Model):
_name = 'fp.qc.checklist.template'
_description = 'Fusion Plating QC Checklist Template'
_description = 'Fusion Plating - QC Checklist Template'
_inherit = ['mail.thread']
_order = 'partner_id, sequence, name'
name = fields.Char(
string='Template Name', required=True, tracking=True,
help='e.g. "Standard Aerospace CoC + Thickness" or '
'"Commercial Visual Only".',
'"Commercial - Visual Only".',
)
sequence = fields.Integer(default=10)
active = fields.Boolean(default=True)
@@ -46,7 +46,7 @@ class FpQcChecklistTemplate(models.Model):
)
notes = fields.Html(
string='Notes',
help='Context for QC inspectors what this customer cares '
help='Context for QC inspectors - what this customer cares '
'about, common reject reasons, spec docs to reference.',
)
@@ -107,7 +107,7 @@ class FpQcChecklistTemplate(models.Model):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _('QC Checks %s') % self.name,
'name': _('QC Checks - %s') % self.name,
'res_model': 'fusion.plating.quality.check',
'view_mode': 'list,form',
'domain': [('template_id', '=', self.id)],
@@ -117,7 +117,7 @@ class FpQcChecklistTemplate(models.Model):
class FpQcChecklistTemplateLine(models.Model):
_name = 'fp.qc.checklist.template.line'
_description = 'Fusion Plating QC Checklist Template Line'
_description = 'Fusion Plating - QC Checklist Template Line'
_order = 'sequence, id'
template_id = fields.Many2one(
@@ -128,7 +128,7 @@ class FpQcChecklistTemplateLine(models.Model):
name = fields.Char(
string='Check Item', required=True, translate=True,
help='The operator-facing question, e.g. "No visible pits or '
'blemishes on surface", "Thickness within 0.00050.0010".',
'blemishes on surface", "Thickness within 0.0005-0.0010".',
)
description = fields.Text(
string='Inspection Guidance',

View File

@@ -3,18 +3,18 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
#
# Sub 12 Phase B quality alert stage.
# Sub 12 Phase B - quality alert stage.
#
# Shared kanban-stage namespace used by both NCR and RMA. Each model has
# its own state Selection (state machine guards) AND a stage_id Many2one
# (kanban-draggable). The two stay in sync see fp_quality_categorisation_links.
# (kanban-draggable). The two stay in sync - see fp_quality_categorisation_links.
from odoo import fields, models
class FpQualityAlertStage(models.Model):
_name = 'fp.quality.alert.stage'
_description = 'Fusion Plating Quality Alert Stage'
_description = 'Fusion Plating - Quality Alert Stage'
_order = 'sequence, id'
name = fields.Char(required=True, translate=True)

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
#
# Sub 12 Phase B categorisation field extensions.
# Sub 12 Phase B - categorisation field extensions.
#
# Adds the cross-cutting tag_ids / reason_id / team_id fields to all five
# quality records (NCR, CAPA, Hold, Check, RMA). Adds stage_id (kanban
@@ -77,7 +77,7 @@ class FpNcrCategorisation(models.Model):
new_state = NCR_STAGE_CODE_TO_STATE.get(rec.stage_id.code)
if new_state and new_state != rec.state:
# Use direct write to avoid the action_close UserError
# guards kanban drag is an explicit user intent.
# guards - kanban drag is an explicit user intent.
super(FpNcrCategorisation, rec).write({'state': new_state})
@api.model

View File

@@ -3,14 +3,14 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
#
# Phase 1 (Sub 11) relocated from fusion_plating_bridge_mrp.
# Phase 1 (Sub 11) - relocated from fusion_plating_bridge_mrp.
# Now binds to fp.job (native) instead of mrp.production.
"""Per-job QC instance.
When a plating job confirms and the customer requires QC, we clone
the active checklist template into a `fusion.plating.quality.check`
with one line per template line. The inspector picks it up on the
tablet, walks the checks, and signs off which unblocks the job's
tablet, walks the checks, and signs off - which unblocks the job's
`button_mark_done`.
The QC also owns the Fischerscope / XDAL 600 thickness report PDF.
@@ -33,7 +33,7 @@ _logger = logging.getLogger(__name__)
class FpQualityCheck(models.Model):
_name = 'fusion.plating.quality.check'
_description = 'Fusion Plating Quality Check'
_description = 'Fusion Plating - Quality Check'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'create_date desc'
@@ -71,7 +71,7 @@ class FpQualityCheck(models.Model):
overall_result = fields.Selection(
[('pass', 'Pass'), ('fail', 'Fail'), ('partial', 'Partial Pass')],
string='Result', tracking=True,
help='Summary outcome set when inspector signs off.',
help='Summary outcome - set when inspector signs off.',
)
line_ids = fields.One2many(
'fusion.plating.quality.check.line', 'check_id',
@@ -207,7 +207,7 @@ class FpQualityCheck(models.Model):
'result': 'pending',
})
job.message_post(
body=_('QC checklist "%s" created %d items to inspect.') % (
body=_('QC checklist "%s" created - %d items to inspect.') % (
template.name, len(template.line_ids),
),
)
@@ -233,16 +233,16 @@ class FpQualityCheck(models.Model):
'inspector_id': self.env.user.id,
})
rec.message_post(body=Markup(
'<b>QC PASSED</b> inspector %s.'
'<b>QC PASSED</b> - inspector %s.'
) % self.env.user.name)
def action_fail(self):
"""Mark QC failed AND auto-spawn a fusion.plating.quality.hold
so the parts have an AS9100 disposition record. Without this
spawning the parts are in limbo operator can't ship and
spawning the parts are in limbo - operator can't ship and
nothing tracks scrap/rework/use-as-is decisions.
v19.0.4.x Hold auto-spawn added per the tablet usability pass.
v19.0.4.x - Hold auto-spawn added per the tablet usability pass.
"""
Hold = self.env.get('fusion.plating.quality.hold')
for rec in self:
@@ -253,7 +253,7 @@ class FpQualityCheck(models.Model):
'inspector_id': self.env.user.id,
})
rec.message_post(body=Markup(
'<b>QC FAILED</b> inspector %s.'
'<b>QC FAILED</b> - inspector %s.'
) % self.env.user.name)
# Auto-spawn the Hold (best-effort; QC failure stands even
# if Hold creation fails for some odd reason).
@@ -342,7 +342,7 @@ class FpQualityCheck(models.Model):
)
if pending:
raise UserError(_(
'Cannot pass QC "%(name)s" %(n)d required check '
'Cannot pass QC "%(name)s" - %(n)d required check '
'item(s) still pending:\n%(items)s'
) % {
'name': rec.name,
@@ -352,7 +352,7 @@ class FpQualityCheck(models.Model):
failed = rec.line_ids.filtered(lambda l: l.result == 'fail')
if failed:
raise UserError(_(
'Cannot pass QC "%(name)s" %(n)d check item(s) '
'Cannot pass QC "%(name)s" - %(n)d check item(s) '
'failed. Fail the QC instead, or reset those '
'items to pass.'
) % {'name': rec.name, 'n': len(failed)})
@@ -418,7 +418,7 @@ class FpQualityCheck(models.Model):
return result.stdout or ''
except FileNotFoundError:
_logger.warning(
'pdftotext not installed cannot auto-extract '
'pdftotext not installed - cannot auto-extract '
'Fischerscope PDF data. Install poppler-utils on '
'the Odoo host.',
)
@@ -493,7 +493,7 @@ class FpQualityCheck(models.Model):
return {
'type': 'ir.actions.client',
'tag': 'fp_qc_checklist',
'name': _('QC %s') % (self.job_id.name or ''),
'name': _('QC - %s') % (self.job_id.name or ''),
'params': {'check_id': self.id},
'target': 'current',
}
@@ -501,7 +501,7 @@ class FpQualityCheck(models.Model):
class FpQualityCheckLine(models.Model):
_name = 'fusion.plating.quality.check.line'
_description = 'Fusion Plating Quality Check Line'
_description = 'Fusion Plating - Quality Check Line'
_order = 'sequence, id'
check_id = fields.Many2one(
@@ -563,8 +563,8 @@ class FpQualityCheckLine(models.Model):
for rec in self:
if rec.requires_value and not rec.value_in_range:
raise UserError(_(
'Cannot pass "%(item)s" value %(val)s is outside '
'the acceptance range (%(min)s %(max)s %(uom)s).'
'Cannot pass "%(item)s" - value %(val)s is outside '
'the acceptance range (%(min)s - %(max)s %(uom)s).'
) % {
'item': rec.name,
'val': rec.value,
@@ -574,7 +574,7 @@ class FpQualityCheckLine(models.Model):
})
if rec.requires_photo and not rec.photo_attachment_id:
raise UserError(_(
'Cannot pass "%(item)s" a photo is required.'
'Cannot pass "%(item)s" - a photo is required.'
) % {'item': rec.name})
rec.write({
'result': 'pass',

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
#
# Sub 12 Phase D cross-creation actions and CAPA closure-loop linkage.
# Sub 12 Phase D - cross-creation actions and CAPA closure-loop linkage.
#
# - NCR.action_spawn_capa: creates a draft CAPA pre-filled from the NCR.
# - CAPA.action_mark_not_effective override: auto-creates a follow-up NCR
@@ -131,7 +131,7 @@ class FpCapaCrossCreation(models.Model):
self.ensure_one()
if not self.ncr_id:
raise UserError(_(
'CAPA %s has no source NCR verification activity '
'CAPA %s has no source NCR - verification activity '
'cannot be scheduled.'
) % self.display_name)
deadline = fields.Date.context_today(self) + timedelta(days=30)

View File

@@ -9,14 +9,14 @@ from odoo import api, fields, models
class FpQualityHold(models.Model):
"""Quality Hold parts pulled from production for quality review.
"""Quality Hold - parts pulled from production for quality review.
Enables the Steelhead-style "Move Parts Into Quality Management"
workflow. An operator can split a partial quantity off a job and
place it on hold for inspection, rework, or scrap.
"""
_name = 'fusion.plating.quality.hold'
_description = 'Fusion Plating Quality Hold'
_description = 'Fusion Plating - Quality Hold'
_inherit = ['mail.thread', 'mail.activity.mixin', 'fp.parent.numbered.mixin']
_order = 'create_date desc'
@@ -30,7 +30,7 @@ class FpQualityHold(models.Model):
)
# ----- What's on hold -----
# Phase 1 (Sub 11) native plating-job link replaces the legacy
# Phase 1 (Sub 11) - native plating-job link replaces the legacy
# workorder_id / production_id pair that lived in bridge_mrp.
# The bridge fields stay during the migration window so existing
# records keep their FKs; Phase 5 removes bridge_mrp entirely.
@@ -56,7 +56,7 @@ class FpQualityHold(models.Model):
('contamination', 'Contamination'),
('customer_complaint', 'Customer Complaint'),
('process_deviation', 'Process Deviation'),
# v19.0.4.8.0 Distinct bucket so QA can split QC-failed
# v19.0.4.8.0 - Distinct bucket so QA can split QC-failed
# holds (auto-spawned by fusion.plating.quality.check.action_fail)
# from operator-flagged process deviations / contamination.
('qc_failure', 'QC Failure'),
@@ -70,7 +70,7 @@ class FpQualityHold(models.Model):
description = fields.Text(
string='Description',
required=True,
help='Required every hold needs an inspector narrative.',
help='Required - every hold needs an inspector narrative.',
)
attachment_ids = fields.Many2many(
'ir.attachment',

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
#
# Sub 12 Phase C trigger-based quality points.
# Sub 12 Phase C - trigger-based quality points.
#
# Replaces the old "customer.x_fc_requires_qc + customer.x_fc_qc_template_id"
# direct binding. Now an admin defines fp.quality.point rules with filters
@@ -39,7 +39,7 @@ STEP_KINDS = [
class FpQualityPoint(models.Model):
_name = 'fp.quality.point'
_description = 'Fusion Plating Quality Point'
_description = 'Fusion Plating - Quality Point'
_inherit = ['mail.thread']
_order = 'sequence, name'
@@ -179,7 +179,7 @@ class FpQualityPoint(models.Model):
vals = {
'template_id': self.template_id.id,
}
# Best-effort field bindings survives schema variations.
# Best-effort field bindings - survives schema variations.
if 'partner_id' in Check._fields and partner:
vals['partner_id'] = partner.id
if 'job_id' in Check._fields and job:
@@ -198,13 +198,13 @@ class FpQualityPoint(models.Model):
return Check.create(vals)
except Exception as e:
_logger.warning(
'fp.quality.point %s: spawn failed for %s %s',
'fp.quality.point %s: spawn failed for %s - %s',
self.name, source.display_name if source else '?', e,
)
return False
def action_spawn_manual(self):
"""Manual fire present from the form view button. No source ctx."""
"""Manual fire - present from the form view button. No source ctx."""
for rec in self:
rec._spawn_check_for(source=rec)
return {

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
#
# Sub 12 Phase C trigger-hook overrides on receiving / job / step / SO.
# Sub 12 Phase C - trigger-hook overrides on receiving / job / step / SO.
# Each hook walks fp.quality.point with the matching trigger_type and
# spawns a quality check for every match. Best-effort: failures are
# logged but never block the underlying state transition.

View File

@@ -3,14 +3,14 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
#
# Sub 12 Phase B quality reason (root-cause classification library).
# Sub 12 Phase B - quality reason (root-cause classification library).
from odoo import fields, models
class FpQualityReason(models.Model):
_name = 'fp.quality.reason'
_description = 'Fusion Plating Quality Reason'
_description = 'Fusion Plating - Quality Reason'
_order = 'category, name'
name = fields.Char(required=True, translate=True)

View File

@@ -3,12 +3,12 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
#
# Sub 12 Phase D smart-button counts on fp.job, sale.order, res.partner.
# Sub 12 Phase D - smart-button counts on fp.job, sale.order, res.partner.
#
# Each parent record gets badge counts for: Holds, Checks, NCRs, CAPAs,
# RMAs. The buttons hide at a count of 0 (set in the view XML), so the row
# only shows quality work that actually exists. NCR/CAPA counts are scoped
# to the order/job via RMAs + holds (see _fp_quality_ncr_ids) NOT the
# to the order/job via RMAs + holds (see _fp_quality_ncr_ids) - NOT the
# customer's whole NCR history. Action methods open the matching list.
from odoo import _, api, fields, models
@@ -53,7 +53,7 @@ class FpJobQualitySmart(models.Model):
[('ncr_id', 'in', ncr_ids)]) if ncr_ids else 0
def _fp_quality_ncr_ids(self):
"""Job-scoped NCR ids single source of truth for both the badge
"""Job-scoped NCR ids - single source of truth for both the badge
count and the smart-button list.
There is no ncr.job_id / ncr.sale_order_id field, so the only honest
@@ -124,7 +124,7 @@ class FpJobQualitySmart(models.Model):
def action_view_fp_rmas(self):
self.ensure_one()
# SO-scoped to match the badge count. RMA.sale_order_id is required,
# so a job with no order matches nothing same as the count's 0.
# so a job with no order matches nothing - same as the count's 0.
return {
'name': _('RMAs'),
'type': 'ir.actions.act_window',
@@ -178,7 +178,7 @@ class SaleOrderQualitySmart(models.Model):
[('ncr_id', 'in', ncr_ids)]) if ncr_ids else 0
def _fp_quality_ncr_ids(self):
"""Order-scoped NCR ids single source of truth for both the badge
"""Order-scoped NCR ids - single source of truth for both the badge
count and the smart-button list.
There is no ncr.sale_order_id / ncr.job_id field, so the only honest
@@ -283,7 +283,7 @@ class ResPartnerQualitySmart(models.Model):
def action_view_fp_quality_history(self):
self.ensure_one()
return {
'name': _('Quality History %s') % self.display_name,
'name': _('Quality History - %s') % self.display_name,
'type': 'ir.actions.client',
'tag': 'fp_quality_dashboard',
'context': {'default_partner_id': self.id},

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
#
# Sub 12 Phase B quality tag.
# Sub 12 Phase B - quality tag.
#
# Cross-cutting tag library reused by NCR, CAPA, Hold, Check, RMA.
@@ -12,7 +12,7 @@ from odoo import fields, models
class FpQualityTag(models.Model):
_name = 'fp.quality.tag'
_description = 'Fusion Plating Quality Tag'
_description = 'Fusion Plating - Quality Tag'
_order = 'name'
name = fields.Char(required=True, translate=True)

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
#
# Sub 12 Phase B quality team.
# Sub 12 Phase B - quality team.
#
# Dedicated team model rather than reusing res.groups, per Sub 12 locked
# decision: teams need their own kanban grouping + per-team escalation
@@ -14,7 +14,7 @@ from odoo import fields, models
class FpQualityTeam(models.Model):
_name = 'fp.quality.team'
_description = 'Fusion Plating Quality Team'
_description = 'Fusion Plating - Quality Team'
_order = 'sequence, name'
_inherit = ['mail.thread']

View File

@@ -23,7 +23,7 @@ class FpQuoteConfigurator(models.Model):
def _find_matching_rule(self):
"""Extend the configurator's matcher to consider Spec + Recipe.
Spec match adds +8 (highest priority explicit customer spec
Spec match adds +8 (highest priority - explicit customer spec
wins over chemistry filters). Recipe adds +6. Material is +2.
"""
recipe = self.recipe_id or False

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
#
# fp.rma Return Material Authorisation.
# fp.rma - Return Material Authorisation.
#
# Sub 12 Phase A. Internal-only RMA workflow that ties customer returns to
# the existing NCR / CAPA / Hold stack. Portal submission is deferred to a
@@ -35,7 +35,7 @@ _logger = logging.getLogger(__name__)
class FpRma(models.Model):
_name = 'fusion.plating.rma'
_description = 'Fusion Plating Return Material Authorisation'
_description = 'Fusion Plating - Return Material Authorisation'
_inherit = ['mail.thread', 'mail.activity.mixin', 'fp.parent.numbered.mixin']
_order = 'create_date desc, id desc'
_rec_name = 'name'
@@ -204,7 +204,7 @@ class FpRma(models.Model):
default=True, tracking=True,
help='When the carrier delivers the returned parts and an '
'fp.receiving is created against this RMA, an NCR is '
'spawned automatically. Manager can toggle off the '
'spawned automatically. Manager can toggle off - the '
'change is tracked on the chatter for audit.',
)
auto_spawn_hold = fields.Boolean(
@@ -235,7 +235,7 @@ class FpRma(models.Model):
active = fields.Boolean(default=True)
# ------------------------------------------------------------------
# Phase B placeholders (categorisation) added now so views won't
# Phase B placeholders (categorisation) - added now so views won't
# break when Phase B lands. Kept as M2O/M2M to models added later.
# ------------------------------------------------------------------
# tag_ids, reason_id, team_id, stage_id are added in Phase B.
@@ -370,7 +370,7 @@ class FpRma(models.Model):
- from action_mark_received (manual)
- from fp.receiving.create override when rma_id was set
Flips state to `received` and (optionally) spawns NCR + Hold per
the auto_spawn_* toggles. Idempotent re-entry on an already-
the auto_spawn_* toggles. Idempotent - re-entry on an already-
received RMA is a no-op (no double-spawn on ORM retry / split
deliveries).
"""
@@ -389,7 +389,7 @@ class FpRma(models.Model):
spawned.append(_('Hold %s') % hold.name)
label = 'Received'
if spawned:
label += ' auto-spawned ' + ', '.join(spawned)
label += ' - auto-spawned ' + ', '.join(spawned)
rec._post_state_message(label)
# Customer notification: parts arrived at the shop.
rec._fire_rma_notification('rma_received')
@@ -402,7 +402,7 @@ class FpRma(models.Model):
if existing:
return existing
partner = self.partner_id
# Pull a facility prefer the partner's company facility, fall
# Pull a facility - prefer the partner's company facility, fall
# back to the first active facility.
Facility = self.env['fusion.plating.facility']
facility = (
@@ -420,7 +420,7 @@ class FpRma(models.Model):
) or self.sale_order_line_ids[:1].product_id.display_name or '/'
complaint = self.complaint_description or ''
body = (
Markup('<p><strong>RMA %s auto-created from customer return.</strong></p>') % self.name
Markup('<p><strong>RMA %s - auto-created from customer return.</strong></p>') % self.name
+ Markup(complaint or '<p>(no description)</p>')
)
ncr = Ncr.create({
@@ -506,7 +506,7 @@ class FpRma(models.Model):
'RMA %s must be Triaged or Resolving before being '
'marked Resolved.'
) % rec.display_name)
# Refund path needs a wizard return handle separately.
# Refund path needs a wizard return - handle separately.
refund_recs = self.filtered(lambda r: r.resolution_type == 'refund')
if len(refund_recs) > 1:
raise UserError(_(
@@ -572,7 +572,7 @@ class FpRma(models.Model):
new_job._generate_steps_from_recipe()
self.replacement_job_id = new_job.id
# Auto-confirm so the portal mirror, racking inspection and
# 'job_confirmed' notification all fire same as a normal job.
# 'job_confirmed' notification all fire - same as a normal job.
if hasattr(new_job, 'action_confirm') and new_job.state == 'draft':
try:
new_job.action_confirm()
@@ -588,14 +588,14 @@ class FpRma(models.Model):
if self.refund_invoice_id:
return self.refund_invoice_id
# Open the standard refund wizard pre-filled to the original SO.
# We don't auto-confirm accountant verifies amounts first.
# We don't auto-confirm - accountant verifies amounts first.
invoices = self.env['account.move'].search([
('invoice_origin', '=', self.sale_order_id.name),
('move_type', '=', 'out_invoice'),
], limit=1)
if not invoices:
raise UserError(_(
'RMA %s: no posted invoice found for SO %s cannot '
'RMA %s: no posted invoice found for SO %s - cannot '
'create a credit note automatically. Issue refund '
'manually.'
) % (self.display_name, self.sale_order_id.name))

View File

@@ -86,7 +86,7 @@ class FpReceivingRmaLink(models.Model):
"""Walk an RMA-bound receiving from draft to closed in one call.
For RMA returns, the receiving's box-count → racking → close walk
is purely administrative the parts are already plated and the
is purely administrative - the parts are already plated and the
operator opens them on triage, not on intake. Fast-forwarding
here keeps the SO's x_fc_receiving_status accurate without
forcing the receiver to click three buttons in sequence.
@@ -111,7 +111,7 @@ class AccountMoveRmaLink(models.Model):
with resolution_type='refund' and no refund_invoice_id yet.
Also flips the RMA from `resolving` to `resolved` once the credit
note is linked mirrors the auto-progression for replace/rework
note is linked - mirrors the auto-progression for replace/rework
paths so the RMA doesn't get stuck after a refund.
"""
_inherit = 'account.move'

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
#
# Phase 1 (Sub 11) relocated from fusion_plating_bridge_mrp.
# Phase 1 (Sub 11) - relocated from fusion_plating_bridge_mrp.
# Adds the back-reference from fp.thickness.reading to the QC record
# that produced it. Lives here (not in fusion_plating_certificates)
# because the link target is fusion.plating.quality.check, owned by

View File

@@ -14,10 +14,10 @@ class ResPartner(models.Model):
help='When enabled, newly-created parts under this customer will '
'show a reminder banner on the part form inviting QA to '
'complete a Contract Review (QA-005). The review remains '
'fully optional the reminder can be dismissed and never '
'fully optional - the reminder can be dismissed and never '
'blocks production.',
)
# Phase 4 (Sub 11) relocated from fusion_plating_bridge_mrp.
# Phase 4 (Sub 11) - relocated from fusion_plating_bridge_mrp.
x_fc_requires_qc = fields.Boolean(
string='Require QC Sign-off',
default=False, tracking=True,