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

@@ -4,12 +4,12 @@
# Part of the Fusion Plating product family.
{
"name": "Fusion Plating MRP Bridge",
"name": "Fusion Plating - MRP Bridge",
'version': '19.0.13.0.5',
'category': 'Manufacturing/Plating',
'summary': 'Bridge Fusion Plating facilities, baths and tanks to Odoo MRP work orders.',
'description': """
Fusion Plating MRP Bridge
Fusion Plating - MRP Bridge
============================
Part of the Fusion Plating product family by Nexa Systems Inc.
@@ -59,13 +59,13 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
],
'data': [
'security/ir.model.access.csv',
# Phase 1 (Sub 11) fp_work_role_data + fp_qc_data relocated
# Phase 1 (Sub 11) - fp_work_role_data + fp_qc_data relocated
# to fusion_plating_jobs.
'data/fp_cron_data.xml',
'wizard/fp_recipe_config_wizard_views.xml',
'views/mrp_workcenter_views.xml',
'views/mrp_workorder_views.xml',
# Phase 1 (Sub 11) relocated to fusion_plating_jobs / fusion_plating_quality.
# Phase 1 (Sub 11) - relocated to fusion_plating_jobs / fusion_plating_quality.
# 'views/fp_qc_template_views.xml',
# 'views/fp_quality_check_views.xml',
# 'views/fp_job_consumption_views.xml',
@@ -74,15 +74,15 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
'views/sale_order_views.xml',
'views/fp_quality_hold_views.xml',
'views/fp_batch_views.xml',
# Phase 3 (Sub 11) replaced by native fp.job.step priority kanban
# Phase 3 (Sub 11) - replaced by native fp.job.step priority kanban
# in fusion_plating_jobs/views/fp_step_priority_views.xml.
# 'views/fp_workorder_priority_views.xml',
# Phase 4 (Sub 11) relocated to fusion_plating_quality.
# Phase 4 (Sub 11) - relocated to fusion_plating_quality.
# 'views/res_partner_views.xml',
'views/fp_serial_views.xml',
],
'assets': {
# Phase 2 (Sub 11) QC tablet OWL relocated to fusion_plating_quality.
# Phase 2 (Sub 11) - QC tablet OWL relocated to fusion_plating_quality.
},
'installable': True,
'application': False,

View File

@@ -2,5 +2,5 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Phase 2 (Sub 11) QC controller relocated to fusion_plating_quality.
# Phase 2 (Sub 11) - QC controller relocated to fusion_plating_quality.
# from . import fp_qc_controller

View File

@@ -90,7 +90,7 @@ class FpQcController(http.Controller):
}
# ------------------------------------------------------------------
# GET state OWL calls this on mount + after every action
# GET state - OWL calls this on mount + after every action
# ------------------------------------------------------------------
@http.route(
'/fp/qc/get', type='jsonrpc', auth='user', methods=['POST'],
@@ -135,7 +135,7 @@ class FpQcController(http.Controller):
if check.state == 'draft':
check.action_start()
# Numeric value handling write before action to let
# Numeric value handling - write before action to let
# _compute_value_in_range update the record.
vals = {}
if value is not None and line.requires_value:

View File

@@ -8,7 +8,7 @@
<data noupdate="1">
<!--
Cron auto-finish WOs whose recipe step is `auto_complete`
Cron - auto-finish WOs whose recipe step is `auto_complete`
once they've been in Progress for at least their expected
duration. Used for fully-automated steps (timed immersion,
automated rinse) where the equipment runs unattended.

View File

@@ -19,7 +19,7 @@
<field name="company_id" eval="False"/>
</record>
<!-- ===== Default checklist template (global partner_id blank) =====
<!-- ===== Default checklist template (global - partner_id blank) =====
sequence=5 so it wins over any other global template when
resolve_for_partner falls back from a missing per-customer match. -->
<record id="qc_template_default" model="fp.qc.checklist.template">
@@ -34,7 +34,7 @@
<record id="qc_tpl_line_visual" model="fp.qc.checklist.template.line">
<field name="template_id" ref="qc_template_default"/>
<field name="sequence">10</field>
<field name="name">Visual no pits, burns, or bare spots</field>
<field name="name">Visual - no pits, burns, or bare spots</field>
<field name="description">Examine the entire plated surface under shop lighting. Look for pits, burns, dewetting, bare spots, or rough texture. Reject if any defect is visible to the naked eye.</field>
<field name="check_type">visual</field>
<field name="required">True</field>
@@ -43,7 +43,7 @@
<record id="qc_tpl_line_colour" model="fp.qc.checklist.template.line">
<field name="template_id" ref="qc_template_default"/>
<field name="sequence">20</field>
<field name="name">Colour uniform finish across part</field>
<field name="name">Colour - uniform finish across part</field>
<field name="description">Finish should be uniform with no streaking, blotching, or dull-vs-bright zones. Compare against the customer colour sample if one is on file.</field>
<field name="check_type">visual</field>
<field name="required">True</field>
@@ -52,7 +52,7 @@
<record id="qc_tpl_line_adhesion" model="fp.qc.checklist.template.line">
<field name="template_id" ref="qc_template_default"/>
<field name="sequence">30</field>
<field name="name">Adhesion tape test pass</field>
<field name="name">Adhesion - tape test pass</field>
<field name="description">Apply tape to an inconspicuous area, press firmly for 3 seconds, pull at 90°. No flaking permitted.</field>
<field name="check_type">adhesion</field>
<field name="required">True</field>
@@ -61,7 +61,7 @@
<record id="qc_tpl_line_masking" model="fp.qc.checklist.template.line">
<field name="template_id" ref="qc_template_default"/>
<field name="sequence">40</field>
<field name="name">Masking no plating in masked zones</field>
<field name="name">Masking - no plating in masked zones</field>
<field name="description">Areas that were masked per customer print must be free of plating deposit. Light staining acceptable; build-up is not.</field>
<field name="check_type">visual</field>
<field name="required">True</field>
@@ -70,7 +70,7 @@
<record id="qc_tpl_line_quantity" model="fp.qc.checklist.template.line">
<field name="template_id" ref="qc_template_default"/>
<field name="sequence">50</field>
<field name="name">Quantity matches WO count</field>
<field name="name">Quantity - matches WO count</field>
<field name="description">Count the parts. Must equal the WO quantity minus any documented rework/scrap.</field>
<field name="check_type">functional</field>
<field name="required">True</field>
@@ -79,13 +79,13 @@
<record id="qc_tpl_line_packaging" model="fp.qc.checklist.template.line">
<field name="template_id" ref="qc_template_default"/>
<field name="sequence">60</field>
<field name="name">Packaging parts protected for shipping</field>
<field name="name">Packaging - parts protected for shipping</field>
<field name="description">Parts individually bagged / padded, no direct metal-on-metal contact that could scratch the finish in transit.</field>
<field name="check_type">visual</field>
<field name="required">False</field>
</record>
<!-- ===== Aerospace checklist (stricter used as a starter for
<!-- ===== Aerospace checklist (stricter - used as a starter for
Nadcap customers; admin copies and reassigns to partner) ===== -->
<record id="qc_template_aerospace" model="fp.qc.checklist.template">
<field name="name">Aerospace / Nadcap QC</field>
@@ -99,7 +99,7 @@
<record id="qc_tpl_aero_visual" model="fp.qc.checklist.template.line">
<field name="template_id" ref="qc_template_aerospace"/>
<field name="sequence">10</field>
<field name="name">Visual 10× loupe, no discontinuities</field>
<field name="name">Visual - 10× loupe, no discontinuities</field>
<field name="description">Inspect under 10× magnification. Reject any pit, crack, inclusion, or discontinuity visible at that power.</field>
<field name="check_type">visual</field>
<field name="required">True</field>
@@ -109,7 +109,7 @@
<record id="qc_tpl_aero_thickness_1" model="fp.qc.checklist.template.line">
<field name="template_id" ref="qc_template_aerospace"/>
<field name="sequence">20</field>
<field name="name">Thickness Fischerscope reading #1</field>
<field name="name">Thickness - Fischerscope reading #1</field>
<field name="description">Fischerscope XDAL 600 XRF measurement at primary inspection point. Value must fall inside the customer spec range. Record the NiP mils reading.</field>
<field name="check_type">thickness</field>
<field name="required">True</field>
@@ -120,8 +120,8 @@
<record id="qc_tpl_aero_thickness_2" model="fp.qc.checklist.template.line">
<field name="template_id" ref="qc_template_aerospace"/>
<field name="sequence">30</field>
<field name="name">Thickness Fischerscope reading #2</field>
<field name="description">Second XRF point per customer print's secondary inspection location.</field>
<field name="name">Thickness - Fischerscope reading #2</field>
<field name="description">Second XRF point - per customer print's secondary inspection location.</field>
<field name="check_type">thickness</field>
<field name="required">True</field>
<field name="requires_value">True</field>
@@ -131,8 +131,8 @@
<record id="qc_tpl_aero_thickness_3" model="fp.qc.checklist.template.line">
<field name="template_id" ref="qc_template_aerospace"/>
<field name="sequence">40</field>
<field name="name">Thickness Fischerscope reading #3</field>
<field name="description">Third XRF point per customer print's tertiary inspection location.</field>
<field name="name">Thickness - Fischerscope reading #3</field>
<field name="description">Third XRF point - per customer print's tertiary inspection location.</field>
<field name="check_type">thickness</field>
<field name="required">True</field>
<field name="requires_value">True</field>
@@ -142,7 +142,7 @@
<record id="qc_tpl_aero_adhesion" model="fp.qc.checklist.template.line">
<field name="template_id" ref="qc_template_aerospace"/>
<field name="sequence">50</field>
<field name="name">Adhesion ASTM B571 tape test</field>
<field name="name">Adhesion - ASTM B571 tape test</field>
<field name="description">Apply ASTM B571 tape to freshly-scribed area, remove at 90° per standard. No flaking of plating permitted.</field>
<field name="check_type">adhesion</field>
<field name="required">True</field>
@@ -151,7 +151,7 @@
<record id="qc_tpl_aero_dimensional" model="fp.qc.checklist.template.line">
<field name="template_id" ref="qc_template_aerospace"/>
<field name="sequence">60</field>
<field name="name">Dimensional critical feature verification</field>
<field name="name">Dimensional - critical feature verification</field>
<field name="description">Caliper / mic any feature marked critical on the customer print. Confirm plating did not push dimensions out of tolerance.</field>
<field name="check_type">dimensional</field>
<field name="required">True</field>

View File

@@ -30,7 +30,7 @@
<field name="code">plating_op</field>
<field name="sequence">30</field>
<field name="icon">fa-flask</field>
<field name="description">Runs the plating line chemistry checks, dwell, thickness.</field>
<field name="description">Runs the plating line - chemistry checks, dwell, thickness.</field>
</record>
<record id="work_role_demask" model="fp.work.role">

View File

@@ -11,12 +11,12 @@ from . import fp_portal_job
from . import fp_quality_hold
from . import fp_delivery
from . import fp_batch
# fusion.plating.job.node.override (mrp.production-bound) kept here
# fusion.plating.job.node.override (mrp.production-bound) - kept here
# until Phase 5 deletes the bridge module. The native fp.job-bound
# override is `fp.job.node.override` in fusion_plating_jobs (different
# model, different table).
from . import fp_job_node_override
# Phase 1 (Sub 11) fp.job.consumption is now in fusion_plating_jobs.
# Phase 1 (Sub 11) - fp.job.consumption is now in fusion_plating_jobs.
# bridge_mrp can't depend on jobs (would create a cycle through
# notifications/reports), so the legacy production_id/workorder_id
# fields are gone for good. mrp.production has 0 rows in native mode
@@ -24,22 +24,22 @@ from . import fp_job_node_override
# from . import fp_job_consumption
from . import account_move
from . import sale_order
# Phase 1 (Sub 11) relocated to fusion_plating_jobs.
# Phase 1 (Sub 11) - relocated to fusion_plating_jobs.
# from . import fp_work_role
# Phase 1 (Sub 11) relocated to fusion_plating_jobs.
# Phase 1 (Sub 11) - relocated to fusion_plating_jobs.
# from . import hr_employee
# Phase 1 (Sub 11) relocated to fusion_plating_jobs.
# Phase 1 (Sub 11) - relocated to fusion_plating_jobs.
# from . import fp_proficiency
# Phase 1 (Sub 11) relocated to fusion_plating_jobs (fp.work.role lives there).
# Phase 1 (Sub 11) - relocated to fusion_plating_jobs (fp.work.role lives there).
# from . import fp_process_node
# Phase 1 (Sub 11) relocated to fusion_plating_jobs.
# Phase 1 (Sub 11) - relocated to fusion_plating_jobs.
# from . import fp_qc_template
# Phase 1 (Sub 11) model relocated to fusion_plating_quality.
# Phase 1 (Sub 11) - model relocated to fusion_plating_quality.
# This file now contains only a thin inherit that restores the
# legacy production_id back-link until Phase 5 retires the bridge.
from . import fp_quality_check
# Phase 1 (Sub 11) relocated to fusion_plating_quality.
# Phase 1 (Sub 11) - relocated to fusion_plating_quality.
# from . import fp_thickness_reading
# Phase 4 (Sub 11) relocated to fusion_plating_quality.
# Phase 4 (Sub 11) - relocated to fusion_plating_quality.
# from . import res_partner
from . import fp_serial

View File

@@ -48,6 +48,6 @@ class AccountMove(models.Model):
'invoice_ref': invoice.name,
})
job.message_post(
body='Invoice %s posted job complete.' % invoice.name,
body='Invoice %s posted - job complete.' % invoice.name,
)
return res

View File

@@ -29,7 +29,7 @@ class FpDelivery(models.Model):
if open_windows:
bad = open_windows[0]
raise UserError(_(
'Cannot mark delivery %s delivered job %s has an open '
'Cannot mark delivery %s delivered - job %s has an open '
'bake window (%s, state: %s). Complete the relief bake '
'or mark it scrapped before shipping.'
) % (delivery.name, delivery.job_ref, bad.name, bad.state))
@@ -47,5 +47,5 @@ class FpDelivery(models.Model):
'actual_ship_date': fields.Date.today(),
'tracking_ref': delivery.name,
})
job.message_post(body=_('Parts shipped delivery %s marked delivered.') % delivery.name)
job.message_post(body=_('Parts shipped - delivery %s marked delivered.') % delivery.name)
return res

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) the model proper now lives in
# Phase 1 (Sub 11) - the model proper now lives in
# fusion_plating_jobs. This file restores the legacy production_id +
# workorder_id back-links so bridge_mrp's mrp.production O2M
# (x_fc_consumption_ids) keeps resolving until Phase 5 deletes the

View File

@@ -15,7 +15,7 @@ class FpJobNodeOverride(models.Model):
included. The planner changes these via the configuration wizard.
"""
_name = 'fusion.plating.job.node.override'
_description = 'Fusion Plating Job Node Override'
_description = 'Fusion Plating - Job Node Override'
_order = 'node_sequence, id'
production_id = fields.Many2one(

View File

@@ -2,7 +2,7 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
"""Operator proficiency tracker counts successful WO completions per
"""Operator proficiency tracker - counts successful WO completions per
(employee, role) pair and auto-promotes the employee once the role's
mastery threshold is crossed.
@@ -20,7 +20,7 @@ from odoo import _, api, fields, models
class FpOperatorProficiency(models.Model):
_name = 'fp.operator.proficiency'
_description = 'Fusion Plating Operator Task Proficiency'
_description = 'Fusion Plating - Operator Task Proficiency'
_rec_name = 'display_name'
_order = 'employee_id, role_id'
@@ -52,7 +52,7 @@ class FpOperatorProficiency(models.Model):
index=True,
help='True once the role has been added to the operator\'s Shop '
'Roles automatically. Stays True even if a manager removes '
'the role afterwards the count and promotion history are '
'the role afterwards - the count and promotion history are '
'preserved as a training record.',
)
promoted_at = fields.Datetime(
@@ -80,7 +80,7 @@ class FpOperatorProficiency(models.Model):
def _compute_display_name(self):
for rec in self:
rec.display_name = (
f'{rec.employee_id.name or "?"} {rec.role_id.name or "?"}'
f'{rec.employee_id.name or "?"} - {rec.role_id.name or "?"}'
)
@api.depends('completed_count', 'role_id.mastery_required')
@@ -99,7 +99,7 @@ class FpOperatorProficiency(models.Model):
def _record_completion(self, employee, role):
"""Increment the (employee, role) tally and promote if at threshold.
Idempotent for the (employee, role) pair if no record exists,
Idempotent for the (employee, role) pair - if no record exists,
we create one. Always uses sudo() because the worker may not
have write access to their own profile.
"""
@@ -163,7 +163,7 @@ class FpOperatorProficiency(models.Model):
})
employee.message_post(
body=Markup(_(
'🎉 <b>%(name)s promoted</b> qualified for '
'🎉 <b>%(name)s promoted</b> - qualified for '
'<b>%(role)s</b> after %(count)s successful '
'completions.'
)) % {

View File

@@ -2,19 +2,19 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
"""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 MO 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, _
@@ -22,14 +22,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)
@@ -42,7 +42,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.',
)
@@ -104,7 +104,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)],
@@ -114,7 +114,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(
@@ -125,7 +125,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,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
#
# Phase 1 (Sub 11) the QC model proper now lives in
# Phase 1 (Sub 11) - the QC model proper now lives in
# fusion_plating_quality. This file restores the legacy production_id
# back-link on fusion.plating.quality.check so bridge_mrp's
# mrp.production O2M (x_fc_qc_check_ids) keeps resolving until Phase 5

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
#
# Sub 5 attach MO reverse links to fp.serial. Defined here rather than
# Sub 5 - attach MO reverse links to fp.serial. Defined here rather than
# in fusion_plating_configurator because configurator loads before
# bridge_mrp; declaring the O2M at configurator setup time would fail
# because mrp.production.x_fc_serial_id wouldn't exist yet.

View File

@@ -16,11 +16,11 @@ class FpWorkRole(models.Model):
role each.
- Cross-trained workers: multiple roles per worker.
The model is intentionally flat no hierarchy, no workflow. Roles
The model is intentionally flat - no hierarchy, no workflow. Roles
are just tags that the WO auto-assignment compares.
"""
_name = 'fp.work.role'
_description = 'Fusion Plating Shop Work Role'
_description = 'Fusion Plating - Shop Work Role'
_order = 'sequence, code'
name = fields.Char(string='Role Name', required=True, translate=True)
@@ -44,7 +44,7 @@ class FpWorkRole(models.Model):
active = fields.Boolean(default=True)
# ------------------------------------------------------------------
# Mastery threshold how many successful WO completions a worker
# Mastery threshold - how many successful WO completions a worker
# needs on this role before they're auto-promoted (added to their
# x_fc_work_role_ids). Default reads from the company-level Fusion
# Plating settings so a new role inherits the shop default; the

View File

@@ -14,7 +14,7 @@ class HrEmployee(models.Model):
of them. A small shop where the owner wears every hat just tags
themselves with every role.
Lead hands are a separate per-role list they don't have to be
Lead hands are a separate per-role list - they don't have to be
primary owners of those roles, but they're authorised to step in
when the regular owner is absent or behind. The Manager Desk
promotes lead hands above other workers in its dropdown for any
@@ -53,9 +53,9 @@ class HrEmployee(models.Model):
)
# ------------------------------------------------------------------
# Attendance helpers used by the Manager Desk to show who is
# Attendance helpers - used by the Manager Desk to show who is
# currently clocked in. Works with vanilla hr_attendance or the
# full fusion_clock module both store an open record (no
# full fusion_clock module - both store an open record (no
# check_out) for as long as the employee is on shift.
# ------------------------------------------------------------------
x_fc_is_clocked_in = fields.Boolean(
@@ -70,7 +70,7 @@ class HrEmployee(models.Model):
"""Compute attendance status from hr.attendance.
Batched so the manager dashboard doesn't issue one query per
employee important when the shop has dozens of operators.
employee - important when the shop has dozens of operators.
"""
if not self:
return
@@ -96,7 +96,7 @@ class HrEmployee(models.Model):
1. Odoo 19 normalises ``('=', True)`` into
``('in', OrderedSet([True]))`` before invoking the search
method. The previous code only handled ``=`` / ``!=`` and
fell through to ``return []`` for ``in`` / ``not in``
fell through to ``return []`` for ``in`` / ``not in`` -
which Odoo treats as "no constraint" and matches every
row.
@@ -110,7 +110,7 @@ class HrEmployee(models.Model):
on the cached open-attendance employee ids. Variable signature
future-proofs against Odoo's compute-field API shifting again.
"""
# Variable signature Odoo 19 may pass (records, op, val).
# Variable signature - Odoo 19 may pass (records, op, val).
if len(args) == 3:
_records, operator, value = args
elif len(args) == 2:

View File

@@ -59,7 +59,7 @@ class MrpProduction(models.Model):
)
# ------------------------------------------------------------------
# Quality Control gate (Phase 1 2026-04-20)
# Quality Control gate (Phase 1 - 2026-04-20)
# ------------------------------------------------------------------
x_fc_qc_check_ids = fields.One2many(
'fusion.plating.quality.check', 'production_id',
@@ -82,7 +82,7 @@ class MrpProduction(models.Model):
)
x_fc_qc_required = fields.Boolean(
string='QC Required', compute='_compute_qc_required',
help='Computed from the customer on this MO true when the '
help='Computed from the customer on this MO - true when the '
'customer has "Require QC Sign-off" turned on.',
)
x_fc_qc_check_count = fields.Integer(
@@ -111,7 +111,7 @@ class MrpProduction(models.Model):
'before this one. Copied from the first SO line that set it.',
)
# ---- Sub 5 traceability fields copied from the source SO line --------
# ---- Sub 5 - traceability fields copied from the source SO line --------
# Populated by bridge_mrp's _prepare_mo_vals override, which pulls these
# from the first linked SO line. Lets the fp.serial registry show every
# MO it spawned via a direct FK rather than heuristic origin matching.
@@ -135,7 +135,7 @@ class MrpProduction(models.Model):
)
# ------------------------------------------------------------------
# T1.4 Rework / strip-and-replate
# T1.4 - Rework / strip-and-replate
# ------------------------------------------------------------------
x_fc_is_rework = fields.Boolean(
string='Rework Order',
@@ -156,21 +156,21 @@ class MrpProduction(models.Model):
)
# ------------------------------------------------------------------
# T1.5 Parts location (computed from workorder progress)
# T1.5 - Parts location (computed from workorder progress)
# ------------------------------------------------------------------
x_fc_current_location = fields.Char(
string='Parts Location',
compute='_compute_current_location',
store=True,
help='Where the parts physically are right now the active work centre, '
help='Where the parts physically are right now - the active work centre, '
'or "Ready to Ship" when all work is done.',
)
# ------------------------------------------------------------------
# T3.3 Actuals vs quoted margin
# T3.4 Consumables tied to jobs
# T3.3 - Actuals vs quoted margin
# T3.4 - Consumables tied to jobs
# ------------------------------------------------------------------
# Phase 1 (Sub 11) fp.job.consumption relocated to
# Phase 1 (Sub 11) - fp.job.consumption relocated to
# fusion_plating_jobs. The MO-side O2M would create a circular
# dependency (bridge_mrp → jobs → notifications → bridge_mrp), and
# mrp.production has 0 rows in native mode, so the field is gone.
@@ -273,7 +273,7 @@ class MrpProduction(models.Model):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _('Work Orders %s') % self.name,
'name': _('Work Orders - %s') % self.name,
'res_model': 'mrp.workorder',
'view_mode': 'list,form',
'domain': [('production_id', '=', self.id)],
@@ -291,7 +291,7 @@ class MrpProduction(models.Model):
if len(recvs) == 1:
return {
'type': 'ir.actions.act_window',
'name': _('Receiving %s') % recvs.name,
'name': _('Receiving - %s') % recvs.name,
'res_model': 'fp.receiving',
'res_id': recvs.id,
'view_mode': 'form',
@@ -315,7 +315,7 @@ class MrpProduction(models.Model):
SO = self.env['sale.order']
for mo in self:
currency = mo.company_id.currency_id
# Phase 1 (Sub 11) consumption now lives on fp.job, not MO.
# Phase 1 (Sub 11) - consumption now lives on fp.job, not MO.
consumables = 0.0
labour = 0.0
for wo in mo.workorder_ids:
@@ -345,7 +345,7 @@ class MrpProduction(models.Model):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _('Consumables %s') % self.name,
'name': _('Consumables - %s') % self.name,
'res_model': 'fp.job.consumption',
'view_mode': 'list,form',
'domain': [('production_id', '=', self.id)],
@@ -434,7 +434,7 @@ class MrpProduction(models.Model):
}
def action_create_rework(self):
"""Open a wizard or just copy the MO with is_rework flag set."""
"""Open a wizard - or just copy the MO with is_rework flag set."""
self.ensure_one()
if self.state != 'done':
raise UserError(_('Rework can only be created from a completed MO.'))
@@ -446,7 +446,7 @@ class MrpProduction(models.Model):
'origin': self.origin, # Keep original SO link for billing
})
rework.message_post(
body=_('Rework of MO %s reason will be recorded in the '
body=_('Rework of MO %s - reason will be recorded in the '
'rework reason field.') % self.name,
)
self.message_post(
@@ -468,7 +468,7 @@ class MrpProduction(models.Model):
raise UserError(_('Please select a recipe first.'))
return {
'type': 'ir.actions.act_window',
'name': f'Configure Steps {self.x_fc_recipe_id.name}',
'name': f'Configure Steps - {self.x_fc_recipe_id.name}',
'res_model': 'fp.recipe.config.wizard',
'view_mode': 'form',
'target': 'new',
@@ -523,7 +523,7 @@ class MrpProduction(models.Model):
if not production._resolve_mo_process_tree():
continue # No recipe / part tree assigned
if production.workorder_ids:
continue # WOs already exist don't duplicate
continue # WOs already exist - don't duplicate
# Build lookup of overrides keyed by node ID
override_map = {} # {node_id: included_bool}
@@ -571,7 +571,7 @@ class MrpProduction(models.Model):
# Walk tree and collect operation WO values
wo_vals_list = []
wo_steps = {} # {sequence: instruction text} posted to WO chatter after create
wo_steps = {} # {sequence: instruction text} - posted to WO chatter after create
seq_counter = [10] # mutable for closure, increments by 10
def _is_node_included(node):
@@ -610,7 +610,7 @@ class MrpProduction(models.Model):
mrp_wc = node.work_center_id.x_fc_mrp_workcenter_id.id
if not mrp_wc:
_logger.warning(
'MO %s: operation "%s" has no mapped MRP work centre '
'MO %s: operation "%s" has no mapped MRP work centre - '
'skipping WO creation.',
production.name, node.name,
)
@@ -647,7 +647,7 @@ class MrpProduction(models.Model):
'x_fc_recipe_node_id': node.id,
}
# Recipe estimated_duration also fills the WO's
# x_fc_dwell_time_minutes operators see the recipe-
# x_fc_dwell_time_minutes - operators see the recipe-
# spec'd dwell next to the actual time logged.
if node.estimated_duration:
vals['x_fc_dwell_time_minutes'] = node.estimated_duration
@@ -674,7 +674,7 @@ class MrpProduction(models.Model):
or 'chrome' in name_l or 'anodiz' in name_l
)
if coating and is_plating_node:
# thickness_max is the upper spec limit that's
# thickness_max is the upper spec limit - that's
# what we target. thickness_min is the floor.
if coating.thickness_max:
vals['x_fc_thickness_target'] = coating.thickness_max
@@ -698,13 +698,13 @@ class MrpProduction(models.Model):
seq_counter[0] += 10
elif node.node_type in ('recipe', 'sub_process'):
# Container nodes recurse into children
# Container nodes - recurse into children
for child in node.child_ids.sorted('sequence'):
walk_node(child)
# 'step' nodes at top level are handled by their parent operation
# Start walking from recipe root
# Sub 3 resolve via helper (part-cloned tree preferred,
# Sub 3 - resolve via helper (part-cloned tree preferred,
# recipe_id fallback)
root = production._resolve_mo_process_tree()
if root:
@@ -779,7 +779,7 @@ class MrpProduction(models.Model):
),
)
# Lead-time application recipe lead time wins only if the
# Lead-time application - recipe lead time wins only if the
# MO's planned finish was at the model default (i.e. operator
# hasn't deliberately scheduled a date).
recipe = mo.x_fc_recipe_id
@@ -788,7 +788,7 @@ class MrpProduction(models.Model):
days=recipe.default_lead_time,
)
# Don't overwrite if the planner already set a tighter
# (earlier) commit date only push it later if no commit.
# (earlier) commit date - only push it later if no commit.
if not mo.date_finished or mo.date_finished < target:
mo.date_finished = target
mo.message_post(
@@ -800,7 +800,7 @@ class MrpProduction(models.Model):
)
# ------------------------------------------------------------------
# Sub 8 Auto-create a racking inspection alongside every new MO,
# Sub 8 - Auto-create a racking inspection alongside every new MO,
# regardless of how the MO came into being (bridge_mrp auto-create,
# Odoo sale_mrp procurement, manual create). One row per MO via the
# unique SQL constraint on fp.racking.inspection.production_id.
@@ -849,7 +849,7 @@ class MrpProduction(models.Model):
self._auto_assign_recipe_from_so()
# Auto-derive facility (where the job runs) so x_fc_facility_id is
# never empty downstream it's compliance-critical (AS9100 §7.1.4
# never empty downstream - it's compliance-critical (AS9100 §7.1.4
# "infrastructure"). Order: explicit value > SO override >
# company default > first active facility.
for mo in self:
@@ -871,13 +871,13 @@ class MrpProduction(models.Model):
if facility:
mo.x_fc_facility_id = facility.id
# Hard gate: MO can't be confirmed without a facility without
# Hard gate: MO can't be confirmed without a facility - without
# this, every downstream record (WO, batch, bath log, cert) is
# missing the "where" half of "what was made where by whom".
for mo in self:
if not mo.x_fc_facility_id:
raise UserError(_(
'Cannot confirm MO "%s" no plating facility set.\n\n'
'Cannot confirm MO "%s" - no plating facility set.\n\n'
'Set the facility on the MO, or configure a default '
'in Settings → Companies → Fusion Plating Defaults.'
) % (mo.name or mo.display_name))
@@ -896,7 +896,7 @@ class MrpProduction(models.Model):
mo.x_fc_assigned_manager_id = manager.id
if mo.x_fc_portal_job_id:
# Already linked just update state
# Already linked - just update state
mo.x_fc_portal_job_id.write({'state': 'in_progress'})
continue
# Resolve customer from sale order via origin
@@ -908,7 +908,7 @@ class MrpProduction(models.Model):
if so:
partner = so.partner_id
if not partner:
continue # No customer skip portal job creation
continue # No customer - skip portal job creation
job = PortalJob.create({
'name': mo.name,
'partner_id': partner.id,
@@ -927,7 +927,7 @@ class MrpProduction(models.Model):
self._generate_workorders_from_recipe()
# Spawn a QC check for customers that require sign-off.
# Safe to call unconditionally the factory returns an empty
# Safe to call unconditionally - the factory returns an empty
# recordset when the customer hasn't opted in to QC.
QCheck = self.env.get('fusion.plating.quality.check')
if QCheck is not None:
@@ -968,7 +968,7 @@ class MrpProduction(models.Model):
portal job + delivery so the operator doesn't have to open
the cert and click "Generate".
QC Gate (Phase 1 2026-04-20):
QC Gate (Phase 1 - 2026-04-20):
If the customer has `x_fc_requires_qc=True`, the active QC
check must be in the `passed` state. Additionally, if the
resolved QC template demands thickness readings / a
@@ -988,7 +988,7 @@ class MrpProduction(models.Model):
# Portal job → ready_to_ship
job.write({'state': 'ready_to_ship'})
job.message_post(body=_('Manufacturing complete ready to ship.'))
job.message_post(body=_('Manufacturing complete - ready to ship.'))
# Resolve SO for denormalized fields on the certificate
so = False
@@ -1049,7 +1049,7 @@ class MrpProduction(models.Model):
# Pull in any thickness readings the inspector logged
# against this MO so they show up on the CoC PDF.
# Aerospace/Nadcap customers require these without them
# Aerospace/Nadcap customers require these - without them
# the cert is just a piece of paper.
ThicknessReading = self.env.get('fp.thickness.reading')
if coc_cert and ThicknessReading is not None:
@@ -1060,7 +1060,7 @@ class MrpProduction(models.Model):
if orphan_readings:
orphan_readings.write({'certificate_id': coc_cert.id})
# Skip thickness cert when CoC also wanted the CoC
# Skip thickness cert when CoC also wanted - the CoC
# template already embeds thickness readings, so creating
# a separate thickness cert just produces a duplicate PDF.
# Only create a standalone thickness cert when the customer
@@ -1116,7 +1116,7 @@ class MrpProduction(models.Model):
The manager-bypass context flag `fp_qc_bypass` lets a plant
manager push a job through when the QC was done on paper and
logged late they still own it via chatter.
logged late - they still own it via chatter.
"""
if self.env.context.get('fp_qc_bypass'):
return
@@ -1142,7 +1142,7 @@ class MrpProduction(models.Model):
# Emit a gentle hint with a direct URL into the QC
# tablet so the user can fix it in one click.
raise UserError(_(
'Cannot close MO "%(mo)s" customer "%(cust)s" '
'Cannot close MO "%(mo)s" - customer "%(cust)s" '
'requires QC sign-off and no passing quality check '
'exists yet.\n\nOpen Plating → Quality → Quality '
'Checks to inspect and sign off, or open the '
@@ -1162,7 +1162,7 @@ class MrpProduction(models.Model):
])
if reading_count == 0:
raise UserError(_(
'Cannot close MO "%(mo)s" QC template requires '
'Cannot close MO "%(mo)s" - QC template requires '
'at least one Fischerscope thickness reading, '
'but none have been logged.'
) % {'mo': mo.name})
@@ -1170,7 +1170,7 @@ class MrpProduction(models.Model):
# Thickness report PDF check
if qc.require_thickness_report_pdf and not qc.thickness_report_pdf_id:
raise UserError(_(
'Cannot close MO "%(mo)s" QC template requires '
'Cannot close MO "%(mo)s" - QC template requires '
'the Fischerscope / XDAL 600 report PDF, but none '
'has been uploaded to QC "%(qc)s".'
) % {'mo': mo.name, 'qc': qc.name})
@@ -1178,7 +1178,7 @@ class MrpProduction(models.Model):
# Inspector sign-off
if qc.require_inspector_signoff and not qc.inspector_id:
raise UserError(_(
'Cannot close MO "%(mo)s" QC "%(qc)s" is flagged '
'Cannot close MO "%(mo)s" - QC "%(qc)s" is flagged '
'passed but has no inspector on file.'
) % {'mo': mo.name, 'qc': qc.name})
@@ -1206,7 +1206,7 @@ class MrpProduction(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': [('production_id', '=', self.id)],
@@ -1215,12 +1215,12 @@ class MrpProduction(models.Model):
}
# ------------------------------------------------------------------
# Sub 3 Process tree resolution (single source for WO walker)
# Sub 3 - Process tree resolution (single source for WO walker)
# ------------------------------------------------------------------
def _resolve_mo_process_tree(self):
"""Resolve which process-tree root to walk for this MO.
Resolution priority (Sub 9 process variants):
Resolution priority (Sub 9 - process variants):
1. SO line's `x_fc_process_variant_id` (per-order variant pick)
2. Linked part's `default_process_id` (the part's default variant)
3. Legacy `x_fc_recipe_id` (coating config / product match)
@@ -1252,7 +1252,7 @@ class MrpProduction(models.Model):
return self.x_fc_recipe_id
# ------------------------------------------------------------------
# Sub 2 Certificate requirement resolution (single source)
# Sub 2 - Certificate requirement resolution (single source)
# ------------------------------------------------------------------
def _fp_resolve_cert_requirement(self):
"""Resolve which certs are required for this MO.
@@ -1286,14 +1286,14 @@ class MrpProduction(models.Model):
partner = so.partner_id
lines = so.order_line
# No SO link use partner-level fallback with safe defaults
# No SO link - use partner-level fallback with safe defaults
if not lines:
if partner and 'x_fc_send_coc' in partner._fields:
return (
bool(partner.x_fc_send_coc),
bool(partner.x_fc_send_thickness_report),
)
# No partner at all safe default: CoC yes, thickness no
# No partner at all - safe default: CoC yes, thickness no
return (True, False)
want_coc_any = False
@@ -1324,14 +1324,14 @@ class MrpProduction(models.Model):
return (want_coc_any, want_thickness_any)
# ------------------------------------------------------------------
# #5 Delivery auto-prefill helpers
# #5 - Delivery auto-prefill helpers
# ------------------------------------------------------------------
def _fp_build_delivery_vals(self, mo, job):
"""Build the create-vals for the auto-generated draft delivery.
Sets scheduled_date and assigned_driver_id so the dispatcher
doesn't have to fill them in for every job. tracking_ref stays
empty it's the carrier's number, the operator pastes it once
empty - it's the carrier's number, the operator pastes it once
the carrier accepts the package.
"""
from datetime import timedelta
@@ -1368,7 +1368,7 @@ class MrpProduction(models.Model):
'assigned_driver_id': driver.id if driver else False,
'state': 'draft',
}
# Sub 5 carry serial / job# / thickness / revision from the MO
# Sub 5 - carry serial / job# / thickness / revision from the MO
# onto the draft delivery for end-to-end traceability.
Delivery = self.env.get('fusion.plating.delivery')
if Delivery is not None:
@@ -1384,7 +1384,7 @@ class MrpProduction(models.Model):
return vals
# ------------------------------------------------------------------
# #3 Render the cert PDF + cross-link it everywhere it's needed
# #3 - Render the cert PDF + cross-link it everywhere it's needed
# ------------------------------------------------------------------
def _fp_generate_cert_pdf(self, cert, job, delivery):
"""Render a fp.certificate to PDF and attach it to the cert,
@@ -1394,7 +1394,7 @@ class MrpProduction(models.Model):
Uses the rich fp.certificate-bound report (action_report_coc_en
or action_report_coc_fr based on partner lang). The older
action_report_coc is portal-job bound and produces a bare header
don't use it here.
- don't use it here.
"""
# Pick the report variant by the customer's preferred language.
lang = (cert.partner_id.lang or '').lower() if cert.partner_id else ''
@@ -1423,7 +1423,7 @@ class MrpProduction(models.Model):
# Append the Fischerscope / XDAL 600 PDF as page 2+ of the CoC
# when a QC uploaded one. Aerospace / Nadcap customers need the
# raw equipment report in the same PDF as the cert this is
# raw equipment report in the same PDF as the cert - this is
# how Steelhead does it and what auditors expect.
if cert.certificate_type == 'coc':
merged = self._fp_merge_thickness_into_cert(cert, pdf_content)
@@ -1468,7 +1468,7 @@ class MrpProduction(models.Model):
- PyPDF2 is not installed, or
- either PDF fails to parse (corrupt / encrypted upload).
The uploaded PDF is treated as opaque we don't try to normalise
The uploaded PDF is treated as opaque - we don't try to normalise
page size or re-render. WinFTM exports are US-letter portrait,
which matches our CoC template. Mismatches will just show two
sizes in a PDF reader, which is fine.
@@ -1511,7 +1511,7 @@ class MrpProduction(models.Model):
from pypdf import PdfMerger # newer name
except ImportError:
_logger.warning(
'Neither PyPDF2 nor pypdf installed cannot merge '
'Neither PyPDF2 nor pypdf installed - cannot merge '
'Fischerscope PDF into CoC %s. Attaching CoC only.',
cert.name,
)
@@ -1527,7 +1527,7 @@ class MrpProduction(models.Model):
merged = out.getvalue()
except Exception:
_logger.exception(
'PDF merge failed for CoC %s falling back to CoC-only. '
'PDF merge failed for CoC %s - falling back to CoC-only. '
'The Fischerscope PDF may be corrupt / encrypted / '
'malformed.',
cert.name,

View File

@@ -87,14 +87,14 @@ class MrpWorkorder(models.Model):
'fusion.plating.bake.oven', string='Oven',
domain="[('facility_id', '=', x_fc_facility_id)]",
help='The specific oven this bake / cure WO ran in. Required '
'for bake WOs multiple ovens means we need to pin '
'for bake WOs - multiple ovens means we need to pin '
'which one for the chart-recorder trail.',
)
x_fc_bake_temp = fields.Float(
string='Bake Temp', digits=(5, 1),
help='Setpoint temperature recorded for this bake WO. Unit '
'follows the company default (Settings → Fusion Plating → '
'Units of Measure) overrideable per WO via Temp Unit.',
'Units of Measure) - overrideable per WO via Temp Unit.',
)
x_fc_bake_temp_uom = fields.Selection(
[('F', '°F'), ('C', '°C')],
@@ -115,7 +115,7 @@ class MrpWorkorder(models.Model):
('other', 'Other (see notes)')],
string='Masking Material',
help='Which material was used to mask off the parts. Required '
'on mask / de-mask WOs needed later when stripping or '
'on mask / de-mask WOs - needed later when stripping or '
'replating because each material requires a different '
'removal process.',
)
@@ -125,7 +125,7 @@ class MrpWorkorder(models.Model):
string='Thickness Unit', default='mils',
)
x_fc_dwell_time_minutes = fields.Float(string='Dwell Time (min)')
# Falls back to the MO's facility when the workcenter has none
# Falls back to the MO's facility when the workcenter has none -
# most stub workcenters auto-created from process node names don't
# have facility_id, but the MO always does (enforced at confirm).
x_fc_facility_id = fields.Many2one(
@@ -149,7 +149,7 @@ class MrpWorkorder(models.Model):
)
# ------------------------------------------------------------------
# Worker assignment the Manager Dashboard writes this field;
# Worker assignment - the Manager Dashboard writes this field;
# the Tablet Station filters "My Queue" by it.
# ------------------------------------------------------------------
x_fc_assigned_user_id = fields.Many2one(
@@ -159,13 +159,13 @@ class MrpWorkorder(models.Model):
'manager; the Tablet Station shows only WOs assigned to the '
'logged-in user.',
)
# Phase 1 (Sub 11) fp.work.role relocated to fusion_plating_jobs.
# Phase 1 (Sub 11) - fp.work.role relocated to fusion_plating_jobs.
# bridge_mrp can't depend on jobs (cycle through notifications →
# bridge_mrp), so the legacy WO field is gone. mrp.workorder has 0
# rows in native mode, so nothing breaks.
# ------------------------------------------------------------------
# Timer audit surface the who / when of the timer on the WO header.
# Timer audit - surface the who / when of the timer on the WO header.
# Odoo records every start/stop in mrp.workcenter.productivity but
# the operator + manager need to see "started by Sarah at 09:14,
# finished by Sarah at 11:42" without drilling into time_ids.
@@ -209,13 +209,13 @@ class MrpWorkorder(models.Model):
x_fc_requires_signoff = fields.Boolean(
related='x_fc_recipe_node_id.requires_signoff',
store=True, readonly=True,
help='Recipe says this is a quality hold point finish is '
help='Recipe says this is a quality hold point - finish is '
'blocked until an operator records a sign-off.',
)
x_fc_is_manual = fields.Boolean(
related='x_fc_recipe_node_id.is_manual',
store=True, readonly=True,
help='If false, this is an automated step the worker '
help='If false, this is an automated step - the worker '
'assignment gate is skipped on Start.',
)
x_fc_auto_complete = fields.Boolean(
@@ -235,7 +235,7 @@ class MrpWorkorder(models.Model):
readonly=True, copy=False,
)
# Contract-review approver list lifted from the recipe root via the
# node link. Computed on the fly we tried a `related=` field but
# node link. Computed on the fly - we tried a `related=` field but
# Odoo's M2M-through-M2O-through-M2O related chain didn't populate
# reliably in tests. A small compute is more predictable.
x_fc_contract_review_user_ids = fields.Many2many(
@@ -441,7 +441,7 @@ class MrpWorkorder(models.Model):
return {
'type': 'ir.actions.client',
'tag': 'fp_process_tree',
'name': f'Process Tree {self.production_id.name}',
'name': f'Process Tree - {self.production_id.name}',
'context': {
'production_id': self.production_id.id,
'back_workorder_id': self.id,
@@ -620,7 +620,7 @@ class MrpWorkorder(models.Model):
return {'holds': holds, 'ncrs': ncrs}
# ------------------------------------------------------------------
# write() fire an in-Odoo notification when a worker is assigned.
# write() - fire an in-Odoo notification when a worker is assigned.
# Email is intentionally NOT sent here; the operator gets a bell-icon
# ping in Odoo Discuss the moment the manager picks them. The
# fp.notification.template hooks still send emails for customer-facing
@@ -645,7 +645,7 @@ class MrpWorkorder(models.Model):
Uses message_type='user_notification' which routes to the user's
Inbox in Discuss without creating a chatter entry on the record
(Odoo treats it as a transient ping). The body is intentionally
terse operators read these on a tablet between jobs.
terse - operators read these on a tablet between jobs.
"""
for wo in self:
user = wo.x_fc_assigned_user_id
@@ -663,7 +663,7 @@ class MrpWorkorder(models.Model):
# Build a short, scannable body
lines = [
_('You have been assigned <b>%s</b>.', wo.display_name or wo.name),
_('MO: %s · %s · Qty %s', mo.name if mo else '', product, qty),
_('MO: %s · %s · Qty %s', mo.name if mo else '-', product, qty),
]
if wc:
lines.append(_('Work centre: %s', wc))
@@ -675,22 +675,22 @@ class MrpWorkorder(models.Model):
wo.message_notify(
partner_ids=user.partner_id.ids,
subject=_('Work order assigned %s', wo.display_name or wo.name),
subject=_('Work order assigned - %s', wo.display_name or wo.name),
body=body,
# Inbox-only ping; no chatter post, no email.
email_layout_xmlid=False,
)
# ------------------------------------------------------------------
# T2.2 Certification gate on WO start
# T2.3 Required-field gate (bath/tank for wet WOs, assigned operator)
# T2.2 - Certification gate on WO start
# T2.3 - Required-field gate (bath/tank for wet WOs, assigned operator)
# ------------------------------------------------------------------
WET_FAMILIES = (
'plating', 'pre_treatment', 'post_treatment',
'strip', 'passivation',
)
# Keyword fallback used when the workcenter / process-type metadata
# is missing covers most shop floor naming conventions. Lowercased.
# is missing - covers most shop floor naming conventions. Lowercased.
WET_NAME_KEYWORDS = (
'plat', 'nickel', 'chrome', 'anodiz', 'zinc',
'etch', 'clean', 'rinse', 'strip', 'passivat',
@@ -784,12 +784,12 @@ class MrpWorkorder(models.Model):
"""Bucket this WO into wet/bake/mask/rack/inspect/other.
Priority order (top wins):
1. Explicit equipment links (bath_id / oven_id) definitive.
1. Explicit equipment links (bath_id / oven_id) - definitive.
2. Specific-process keywords (inspect/mask/rack/bake) beat
the broader wet keywords. Otherwise "Post-plate Inspection"
matches "plat" → wet, which is wrong.
3. Workcenter wet process family definitive.
4. Wet name keyword fallback broad (catches plat/etch/rinse...).
3. Workcenter wet process family - definitive.
4. Wet name keyword fallback - broad (catches plat/etch/rinse...).
"""
self.ensure_one()
if self.x_fc_bath_id:
@@ -852,7 +852,7 @@ class MrpWorkorder(models.Model):
for wo in self:
missing = []
# Automated steps (recipe.is_manual=False) don't need a
# human operator the equipment runs unattended (timed
# human operator - the equipment runs unattended (timed
# immersion, automated rinse, etc.). The kind-specific
# equipment checks below still apply.
if not wo.x_fc_assigned_user_id and wo.x_fc_is_manual:
@@ -874,7 +874,7 @@ class MrpWorkorder(models.Model):
missing.append(_('Masking Material'))
if missing:
raise UserError(_(
'Cannot start work order "%(wo)s" please fill these '
'Cannot start work order "%(wo)s" - please fill these '
'required fields first:\n%(fields)s\n\n'
'Open the work order form and have the planner set them.'
) % {
@@ -888,7 +888,7 @@ class MrpWorkorder(models.Model):
required field for traceability is filled in."""
self._fp_check_required_fields_before_start()
self._fp_check_operator_certification()
# Sub 8 soft gate: block the first plating WO if the MO's
# Sub 8 - soft gate: block the first plating WO if the MO's
# racking inspection is still Draft or Inspecting. Non-manager
# operators get a clear error; Plating Managers override.
self._fp_warn_if_racking_inspection_pending()
@@ -898,7 +898,7 @@ class MrpWorkorder(models.Model):
now = fields.Datetime.now()
uid = self.env.user.id
for wo in self:
# Only stamp the first time subsequent pause/resume cycles
# Only stamp the first time - subsequent pause/resume cycles
# shouldn't overwrite the original start.
if not wo.x_fc_started_at:
wo.sudo().write({
@@ -915,7 +915,7 @@ class MrpWorkorder(models.Model):
return
for wo in self:
# Figure out process_type: use bath's process if bath set,
# else workcenter's x_fc_facility_id.* bath is the reliable one.
# else workcenter's x_fc_facility_id.* - bath is the reliable one.
if not wo.x_fc_bath_id or not wo.x_fc_bath_id.process_type_id:
continue # Nothing to check
process_type = wo.x_fc_bath_id.process_type_id
@@ -935,7 +935,7 @@ class MrpWorkorder(models.Model):
) % (employee.name, process_type.name))
def _fp_warn_if_racking_inspection_pending(self):
"""Sub 8 block first plating WO start if racking inspection is still
"""Sub 8 - block first plating WO start if racking inspection is still
Draft or Inspecting.
Only applies to the first-sequence WO of an MO. Later WOs assume
@@ -984,7 +984,7 @@ class MrpWorkorder(models.Model):
# ---- Contract Review approver gate ---------------------------
# Only authorised users (per the recipe's
# contract_review_user_ids) can finish the Contract Review WO.
# Detected by the recipe-node name match robust enough since
# Detected by the recipe-node name match - robust enough since
# this is a well-known operation in every recipe.
node = wo.x_fc_recipe_node_id
if (
@@ -999,7 +999,7 @@ class MrpWorkorder(models.Model):
wo.x_fc_contract_review_user_ids.mapped('name')
) or '(none configured)'
raise UserError(_(
'Cannot finish Contract Review for "%(wo)s" '
'Cannot finish Contract Review for "%(wo)s" - '
'this approval is restricted to: %(allowed)s.\n\n'
'You (%(user)s) are not on the approver list for '
'recipe "%(recipe)s". Ask one of the approvers to '
@@ -1009,13 +1009,13 @@ class MrpWorkorder(models.Model):
'wo': wo.display_name or wo.name,
'allowed': allowed,
'user': self.env.user.name,
'recipe': (node.recipe_root_id.name or ''),
'recipe': (node.recipe_root_id.name or '-'),
})
# ---- Quality hold point: requires sign-off -------------------
if wo.x_fc_requires_signoff and not wo.x_fc_signoff_user_id:
raise UserError(_(
'Cannot finish work order "%(wo)s" recipe step '
'Cannot finish work order "%(wo)s" - recipe step '
'"%(node)s" is a quality hold point and requires '
'an operator sign-off first.\n\n'
'On the WO form: tap "Sign Off" before clicking '
@@ -1040,7 +1040,7 @@ class MrpWorkorder(models.Model):
) % wo.x_fc_oven_id.name)
if missing:
raise UserError(_(
'Cannot finish bake work order "%(wo)s" Nadcap / '
'Cannot finish bake work order "%(wo)s" - Nadcap / '
'AS9100 require these fields before close:\n%(fields)s\n\n'
'On the iPad: tap the WO → Process Details → '
'fill in Bake Temp + Duration. Chart Recorder Ref '
@@ -1051,8 +1051,8 @@ class MrpWorkorder(models.Model):
})
# ------------------------------------------------------------------
# T1.1 Bake window auto-create on plating WO finish
# T1.3 Rack MTO increment when a rack was used
# T1.1 - Bake window auto-create on plating WO finish
# T1.3 - Rack MTO increment when a rack was used
# ------------------------------------------------------------------
def button_finish(self):
"""Finish the WO, bump rack MTO, spawn bake window if required.
@@ -1068,7 +1068,7 @@ class MrpWorkorder(models.Model):
for wo in self:
if wo.x_fc_rack_id:
wo.x_fc_rack_id._increment_mto(1.0)
# Audit stamp overwrite each time the WO is closed so the
# Audit stamp - overwrite each time the WO is closed so the
# most recent finish is what's shown.
wo.sudo().write({
'x_fc_finished_at': now,
@@ -1090,13 +1090,13 @@ class MrpWorkorder(models.Model):
"""Increment the (employee, role) completion counter and promote
the employee if they've crossed the role's mastery threshold.
Runs on the assigned worker, NOT the user who clicked Finish
Runs on the assigned worker, NOT the user who clicked Finish -
sometimes a manager finishes a job on behalf of an absent
operator. The CREDIT belongs to the assigned worker.
"""
Prof = self.env.get('fp.operator.proficiency')
if Prof is None:
return # tracker model not installed yet nothing to do
return # tracker model not installed yet - nothing to do
for wo in self:
user = wo.x_fc_assigned_user_id
role = wo.x_fc_work_role_id
@@ -1123,7 +1123,7 @@ class MrpWorkorder(models.Model):
) else False
if not coating or not getattr(coating, 'requires_bake_relief', False):
continue
# Only fire on the *plating* WO the one whose bath's process
# Only fire on the *plating* WO - the one whose bath's process
# matches the coating config's process.
if wo.x_fc_bath_id.process_type_id != coating.process_type_id:
continue
@@ -1144,7 +1144,7 @@ class MrpWorkorder(models.Model):
})
wo.production_id.message_post(
body=_(
'Bake-window record created relief bake must start '
'Bake-window record created - relief bake must start '
'within %s hours of plate exit.'
) % (coating.bake_window_hours or 4.0)
)
@@ -1162,7 +1162,7 @@ class MrpWorkorder(models.Model):
for wo in self:
if not wo.x_fc_requires_signoff:
raise UserError(_(
'Work order "%s" is not a quality hold point '
'Work order "%s" is not a quality hold point - '
'no sign-off required.'
) % (wo.display_name or wo.name))
wo.write({
@@ -1181,7 +1181,7 @@ class MrpWorkorder(models.Model):
# ------------------------------------------------------------------
@api.model
def _fp_cron_auto_finish_completed_wos(self):
"""Cron entry point auto-finish WOs whose recipe step is marked
"""Cron entry point - auto-finish WOs whose recipe step is marked
`auto_complete` once they've been in Progress for at least their
expected duration.
@@ -1203,7 +1203,7 @@ class MrpWorkorder(models.Model):
finished = 0
for wo in candidates:
if wo.x_fc_requires_signoff and not wo.x_fc_signoff_user_id:
# Quality hold trumps auto-complete wait for the
# Quality hold trumps auto-complete - wait for the
# operator's sign-off before closing.
continue
elapsed_min = (now - wo.x_fc_started_at).total_seconds() / 60.0
@@ -1216,7 +1216,7 @@ class MrpWorkorder(models.Model):
wo.message_post(
body=Markup(_(
'Auto-finished by recipe (auto_complete) after '
'%.1f min expected %.1f min.'
'%.1f min - expected %.1f min.'
)) % (elapsed_min, wo.duration_expected),
subtype_xmlid='mail.mt_note',
)

View File

@@ -2,7 +2,7 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
"""Per-customer QC policy does this customer require quality control
"""Per-customer QC policy - does this customer require quality control
sign-off on every job, and which checklist template governs the checks?
"""
from odoo import fields, models

View File

@@ -39,7 +39,7 @@ class SaleOrder(models.Model):
)
# ------------------------------------------------------------------
# Workflow stage drives which contextual next-step button appears
# Workflow stage - drives which contextual next-step button appears
# on the SO form header. Shows ONE action at a time so users aren't
# overwhelmed. Pattern mirrors fusion_claims ADP case buttons.
# ------------------------------------------------------------------
@@ -73,16 +73,16 @@ class SaleOrder(models.Model):
# ------------------------------------------------------------------
# SO confirm → auto-create a draft MO so the manager has something
# to assign. The configurator emits a service-product line, which
# bypasses Odoo's native MO routing without this hook the workflow
# bypasses Odoo's native MO routing - without this hook the workflow
# stage stalls at 'assign_work' because action_fp_assign_to_me
# searches for DRAFT MOs that don't exist.
#
# Idempotent never creates a second MO for the same SO.
# Idempotent - never creates a second MO for the same SO.
# ------------------------------------------------------------------
def action_confirm(self):
res = super().action_confirm()
# Cutover gate (2026-04-25): when the native job model is the
# primary, skip MO creation here fusion_plating_jobs handles
# primary, skip MO creation here - fusion_plating_jobs handles
# SO → fp.job. Both modules' SO-confirm hooks would otherwise
# run on every confirm and create duplicate work.
ICP = self.env['ir.config_parameter'].sudo()
@@ -92,7 +92,7 @@ class SaleOrder(models.Model):
try:
so._fp_auto_create_mo()
except Exception as exc:
# Don't block SO confirm log + continue. The manager
# Don't block SO confirm - log + continue. The manager
# can still create the MO manually.
so.message_post(
body=Markup(_('Auto-MO creation failed: <code>%s</code>. '
@@ -140,7 +140,7 @@ class SaleOrder(models.Model):
# If a legacy untagged MO already exists for this SO, it
# represents the pre-PR "one MO for the whole order" work.
# Adopt it by linking EVERY untagged plating line to it, and
# treat those lines as covered don't create per-line MOs on
# treat those lines as covered - don't create per-line MOs on
# top of the legacy MO.
untagged_lines = plating_lines.filtered(lambda l: not l.x_fc_wo_group_tag)
tagged_lines = plating_lines - untagged_lines
@@ -194,7 +194,7 @@ class SaleOrder(models.Model):
if not product:
self.env.cr.execute('RELEASE SAVEPOINT %s' % savepoint_name)
self.message_post(body=_(
'Auto-MO skipped (group %s) no manufacturable '
'Auto-MO skipped (group %s) - no manufacturable '
'product available.'
) % (tag or 'single-line'))
continue
@@ -236,7 +236,7 @@ class SaleOrder(models.Model):
start_node = ln.x_fc_start_at_node_id
break
# Sub 5 carry serial / job# / thickness / revision from
# Sub 5 - carry serial / job# / thickness / revision from
# the first line of the group. Single-line groups pick it up
# cleanly; multi-line groups inherit the primary line's
# tracking data. These fields are metadata only (reports and
@@ -265,7 +265,7 @@ class SaleOrder(models.Model):
mo_vals['x_fc_revision_snapshot'] = primary.x_fc_revision_snapshot
mo = Production.create(mo_vals)
created.append((mo, tag, len(lines)))
# Sub 8 the racking inspection is auto-created by
# Sub 8 - the racking inspection is auto-created by
# mrp.production.create() (see mrp_production.py), so
# no extra work here. The hook there picks up the
# x_fc_sale_order_line_ids written above to seed the
@@ -280,7 +280,7 @@ class SaleOrder(models.Model):
if created or adopted:
# _() needs a lang in env.context; in shell/cron this may be
# unset. Compose the message with plain format strings this
# unset. Compose the message with plain format strings - this
# text is an internal chatter log, not user-facing UI.
msg_parts = []
if created:
@@ -341,7 +341,7 @@ class SaleOrder(models.Model):
)
if not product:
self.message_post(body=_(
'Auto-MO skipped no manufacturable product available.'
'Auto-MO skipped - no manufacturable product available.'
))
return
@@ -403,7 +403,7 @@ class SaleOrder(models.Model):
so.x_fc_workflow_stage = 'paid'
continue
# Once an invoice is posted (regardless of payment), the SO has
# moved past 'shipped' the action is on accounting, not us.
# moved past 'shipped' - the action is on accounting, not us.
if shipped and has_posted_invoice:
so.x_fc_workflow_stage = 'invoicing'
continue
@@ -469,7 +469,7 @@ class SaleOrder(models.Model):
rec.state = 'accepted'
if 'x_fc_receiving_status' in self._fields:
self.x_fc_receiving_status = 'received'
self.message_post(body=_('Parts accepted ready to assign manager.'))
self.message_post(body=_('Parts accepted - ready to assign manager.'))
return True
def action_fp_assign_to_me(self):
@@ -516,7 +516,7 @@ class SaleOrder(models.Model):
return {
'type': 'ir.actions.client',
'tag': 'fp_plant_overview',
'name': _('Shop Floor %s') % self.name,
'name': _('Shop Floor - %s') % self.name,
'target': 'current',
}
@@ -565,7 +565,7 @@ class SaleOrder(models.Model):
mos = self.env['mrp.production'].search([('origin', '=', self.name)])
action = {
'type': 'ir.actions.act_window',
'name': _('Manufacturing Orders %s') % self.name,
'name': _('Manufacturing Orders - %s') % self.name,
'res_model': 'mrp.production',
'domain': [('id', 'in', mos.ids)],
'context': {'default_origin': self.name},
@@ -582,7 +582,7 @@ class SaleOrder(models.Model):
wos = mos.mapped('workorder_ids')
action = {
'type': 'ir.actions.act_window',
'name': _('Work Orders %s') % self.name,
'name': _('Work Orders - %s') % self.name,
'res_model': 'mrp.workorder',
'domain': [('id', 'in', wos.ids)],
'view_mode': 'list,form,kanban',
@@ -601,7 +601,7 @@ class SaleOrder(models.Model):
)
action = {
'type': 'ir.actions.act_window',
'name': _('Portal Jobs %s') % self.name,
'name': _('Portal Jobs - %s') % self.name,
'res_model': 'fusion.plating.portal.job',
'domain': [('id', 'in', jobs.ids)],
'view_mode': 'list,form',
@@ -615,7 +615,7 @@ class SaleOrder(models.Model):
mos = self.env['mrp.production'].search([('origin', '=', self.name)])
return {
'type': 'ir.actions.act_window',
'name': _('Quality Holds %s') % self.name,
'name': _('Quality Holds - %s') % self.name,
'res_model': 'fusion.plating.quality.hold',
'domain': [('production_id', 'in', mos.ids)],
'view_mode': 'list,form',
@@ -625,7 +625,7 @@ class SaleOrder(models.Model):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _('Certificates %s') % self.name,
'name': _('Certificates - %s') % self.name,
'res_model': 'fp.certificate',
'domain': [('sale_order_id', '=', self.id)],
'view_mode': 'list,form',
@@ -643,7 +643,7 @@ class SaleOrder(models.Model):
)
return {
'type': 'ir.actions.act_window',
'name': _('Deliveries %s') % self.name,
'name': _('Deliveries - %s') % self.name,
'res_model': 'fusion.plating.delivery',
'domain': [('job_ref', 'in', jobs.mapped('name'))],
'view_mode': 'list,form',

View File

@@ -1,6 +1,6 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating Mobile QC Checklist (OWL backend client action)
// Fusion Plating - Mobile QC Checklist (OWL backend client action)
// Copyright 2026 Nexa Systems Inc.
// License OPL-1 (Odoo Proprietary License v1.0)
//
@@ -120,7 +120,7 @@ export class FpQcChecklist extends Component {
}
}
// Value input debounced write on blur. Pending result stays until
// Value input - debounced write on blur. Pending result stays until
// operator taps pass/fail.
onValueInput(line, ev) {
const v = parseFloat(ev.target.value);
@@ -215,7 +215,7 @@ export class FpQcChecklist extends Component {
return;
}
this.notification.add(
`Uploaded ${json.reading_count || 0} reading(s) extracted`,
`Uploaded - ${json.reading_count || 0} reading(s) extracted`,
{ type: "success" },
);
await this.refresh();

View File

@@ -1,5 +1,5 @@
// =============================================================================
// Fusion Plating Mobile QC Checklist styles
// Fusion Plating - Mobile QC Checklist styles
// Copyright 2026 Nexa Systems Inc. · License OPL-1
//
// Built on the shop-floor design system tokens (_fp_shopfloor_tokens.scss).
@@ -230,7 +230,7 @@
transform $fp-dur $fp-ease;
&.o_fp_qc_item_pass {
// Left accent strip subtle indicator that doesn't scream at you
// Left accent strip - subtle indicator that doesn't scream at you
background:
linear-gradient(to right, $fp-ok 4px, transparent 4px) $fp-card;
}

View File

@@ -186,7 +186,7 @@
</div>
<t t-if="line.value_min or line.value_max">
<div class="o_fp_qc_range">
Range: <t t-esc="line.value_min"/> <t t-esc="line.value_max"/>
Range: <t t-esc="line.value_min"/> - <t t-esc="line.value_max"/>
<t t-esc="line.value_uom"/>
</div>
</t>
@@ -209,7 +209,7 @@
<textarea rows="2"
t-att-value="line.notes or ''"
t-on-input="(ev) => this.onNotesInput(line, ev)"
placeholder="Optional anything the inspector saw that matters"/>
placeholder="Optional - anything the inspector saw that matters"/>
</div>
<div class="o_fp_qc_actions_row">
@@ -253,7 +253,7 @@
t-on-click="() => this.finalize('pass')"
t-att-disabled="!canFinalize or state.saving">
<i class="fa fa-check"/>
<span>Sign Off PASS</span>
<span>Sign Off - PASS</span>
</button>
<button class="o_fp_qc_btn o_fp_qc_btn_fail_lg"
t-on-click="() => this.finalize('fail')"

View File

@@ -149,7 +149,7 @@
<group>
<field name="thickness_report_pdf_id"
widget="many2one_binary"
help="Upload the Fischerscope / XDAL 600 PDF readings will be auto-extracted."/>
help="Upload the Fischerscope / XDAL 600 PDF - readings will be auto-extracted."/>
<field name="thickness_reading_count" readonly="1"/>
<field name="require_thickness_readings" readonly="1"/>
<field name="require_thickness_report_pdf" readonly="1"/>
@@ -218,7 +218,7 @@
<field name="search_view_id" ref="fp_quality_check_search"/>
</record>
<!-- ===== Menu add QC Checks + QC Templates under Quality ===== -->
<!-- ===== Menu - add QC Checks + QC Templates under Quality ===== -->
<menuitem id="menu_fp_quality_check"
name="Quality Checks"
parent="fusion_plating_quality.menu_fp_quality"

View File

@@ -3,7 +3,7 @@
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Sub 5 Serial Number registry views.
Sub 5 - Serial Number registry views.
-->
<odoo>

View File

@@ -80,7 +80,7 @@
sequence="55"
groups="fusion_plating.group_fusion_plating_manager"/>
<!-- Employee form Shop Roles + Lead Hand For + Proficiency tracker -->
<!-- Employee form - Shop Roles + Lead Hand For + Proficiency tracker -->
<record id="view_hr_employee_form_fp_roles" model="ir.ui.view">
<field name="name">hr.employee.form.fp.roles</field>
<field name="model">hr.employee</field>
@@ -141,7 +141,7 @@
</field>
</record>
<!-- Process node form add role field -->
<!-- Process node form - add role field -->
<record id="view_fp_process_node_form_fp_roles" model="ir.ui.view">
<field name="name">fusion.plating.process.node.form.fp.roles</field>
<field name="model">fusion.plating.process.node</field>

View File

@@ -38,7 +38,7 @@
<div><strong><field name="name"/></strong></div>
<div class="text-muted">
Qty: <field name="qty_remaining"/>
<t t-if="record.duration.raw_value"> <field name="duration" widget="float_time"/> elapsed</t>
<t t-if="record.duration.raw_value"> - <field name="duration" widget="float_time"/> elapsed</t>
</div>
</div>
<div class="o_kanban_record_bottom">

View File

@@ -62,7 +62,7 @@
</xpath>
<xpath expr="//div[@name='button_box']" position="inside">
<!-- Sale Order back to the customer's order -->
<!-- Sale Order - back to the customer's order -->
<button name="action_view_sale_order" type="object"
class="oe_stat_button" icon="fa-file-text-o"
invisible="not x_fc_sale_order_id">
@@ -73,13 +73,13 @@
<span class="o_stat_text">Sale Order</span>
</div>
</button>
<!-- Work Orders drill into the WO list -->
<!-- Work Orders - drill into the WO list -->
<button name="action_view_workorders" type="object"
class="oe_stat_button" icon="fa-cogs">
<field name="x_fc_workorder_count" widget="statinfo"
string="Work Orders"/>
</button>
<!-- Receiving link to the parts-receiving record(s) -->
<!-- Receiving - link to the parts-receiving record(s) -->
<button name="action_view_receiving" type="object"
class="oe_stat_button" icon="fa-truck"
invisible="x_fc_receiving_count == 0">
@@ -111,7 +111,7 @@
<field name="x_fc_consumption_count" widget="statinfo"
string="Consumables"/>
</button>
<!-- Quality Check tablet-style checklist -->
<!-- Quality Check - tablet-style checklist -->
<button name="action_open_active_qc" type="object"
class="oe_stat_button" icon="fa-check-square-o"
invisible="x_fc_qc_check_count == 0">

View File

@@ -78,7 +78,7 @@
</xpath>
<!-- ============================================================
3. CUSTOMER FIELD left column, after workcenter_id
3. CUSTOMER FIELD - left column, after workcenter_id
============================================================ -->
<xpath expr="//sheet//field[@name='workcenter_id']" position="after">
<field name="x_fc_customer_id" readonly="1"
@@ -86,7 +86,7 @@
</xpath>
<!-- ============================================================
3b. STEP BADGE + PRIORITY right column, after production_id
3b. STEP BADGE + PRIORITY - right column, after production_id
============================================================ -->
<xpath expr="//sheet//field[@name='production_id']" position="after">
<field name="x_fc_step_display" widget="badge" readonly="1"/>
@@ -112,7 +112,7 @@
</xpath>
<!-- ============================================================
SIGN OFF BUTTON only visible when the recipe step
SIGN OFF BUTTON - only visible when the recipe step
requires a sign-off and the WO is in progress.
============================================================ -->
<xpath expr="//header" position="inside">
@@ -145,7 +145,7 @@
</xpath>
<!-- ============================================================
5. NOTEBOOK restructured tabs
5. NOTEBOOK - restructured tabs
============================================================ -->
<!-- 5a. Rename "Time Tracking" → "Time &amp; Cost" and add cost summary -->
@@ -185,7 +185,7 @@
</group>
</xpath>
<!-- 5b. Process Details tab content adapts to WO kind so
<!-- 5b. Process Details tab - content adapts to WO kind so
operators see only the equipment fields that matter. -->
<xpath expr="//notebook/page[@name='time_tracking']" position="after">
<page string="Process Details" name="plating_details">
@@ -227,7 +227,7 @@
</group>
</group>
<!-- Rack / de-rack WOs.
Note: required="x_fc_wo_kind == 'rack'" (not "1")
Note: required="x_fc_wo_kind == 'rack'" (not "1") -
in Odoo 19 a `required="1"` on a field inside an
invisible group still triggers the missing-required
flag, painting the whole tab red on every WO. -->
@@ -248,14 +248,14 @@
<!-- Inspection -->
<group invisible="x_fc_wo_kind != 'inspect'">
<div class="alert alert-info" role="alert">
Inspection record Fischerscope readings via
Inspection - record Fischerscope readings via
the Tablet Station. Cal-std + n measurements
per part. Readings auto-link to the CoC.
</div>
</group>
<group invisible="x_fc_wo_kind != 'other'">
<div class="alert alert-light text-muted" role="alert">
Generic operation equipment is identified
Generic operation - equipment is identified
by the work centre.
</div>
</group>
@@ -287,9 +287,9 @@
</page>
</xpath>
<!-- 5d. Components tab already exists at name="components" no change needed -->
<!-- 5d. Components tab already exists at name="components" - no change needed -->
<!-- 5e. Blocked By tab keep existing, just push it last visually
<!-- 5e. Blocked By tab - keep existing, just push it last visually
(it's already the last page in the base view, so no reorder needed) -->
<!-- ============================================================

View File

@@ -54,7 +54,7 @@
</button>
</xpath>
<!-- Hide Odoo's default state statusbar replaced below by
<!-- Hide Odoo's default state statusbar - replaced below by
the custom plating workflow statusbar that reflects the
real lifecycle (awaiting parts → in production → shipped → ...). -->
<xpath expr="//header//field[@name='state']" position="attributes">
@@ -63,7 +63,7 @@
<!-- ===== Contextual workflow buttons on the header =====
One (sometimes two) visible at a time. Pattern mirrors
fusion_claims ADP handling invisible bindings key off
fusion_claims ADP handling - invisible bindings key off
the computed x_fc_workflow_stage selector. -->
<xpath expr="//header" position="inside">
<field name="x_fc_workflow_stage" widget="statusbar"
@@ -80,7 +80,7 @@
string="Accept Parts" type="object"
class="btn-primary" icon="fa-check"
invisible="x_fc_workflow_stage not in ('inspecting', 'accept_parts')"
help="Parts pass inspection ready for assignment."/>
help="Parts pass inspection - ready for assignment."/>
<button name="action_fp_assign_to_me"
string="Assign To Me &amp; Release" type="object"
@@ -101,7 +101,7 @@
help="Close the open delivery record(s) and fire auto-invoice per strategy."/>
</xpath>
<!-- Workflow stage banner sits ABOVE the form header so it's
<!-- Workflow stage banner - sits ABOVE the form header so it's
the first thing users see, matches the Account Hold banner.
Hidden for terminal states (invoicing/paid/complete/cancelled)
and the initial draft so it only shows when there's an

View File

@@ -99,7 +99,7 @@ class FpRecipeConfigWizard(models.TransientModel):
class FpRecipeConfigWizardLine(models.TransientModel):
"""One line in the recipe config wizard an optional step."""
"""One line in the recipe config wizard - an optional step."""
_name = 'fp.recipe.config.wizard.line'
_description = 'Recipe Config Wizard Line'
_order = 'node_sequence, id'

View File

@@ -17,8 +17,8 @@
<separator string="Optional Steps"/>
<p class="text-muted">
Toggle which optional steps are included for this job.
<strong>Opt-In</strong> steps are skipped by default check to include.
<strong>Opt-Out</strong> steps are included by default uncheck to skip.
<strong>Opt-In</strong> steps are skipped by default - check to include.
<strong>Opt-Out</strong> steps are included by default - uncheck to skip.
</p>
<field name="line_ids">
<list editable="bottom" no_open="True">