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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.'
|
||||
)) % {
|
||||
|
||||
@@ -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.0005–0.0010".',
|
||||
'blemishes on surface", "Thickness within 0.0005-0.0010".',
|
||||
)
|
||||
description = fields.Text(
|
||||
string='Inspection Guidance',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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')"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 & 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) -->
|
||||
|
||||
<!-- ============================================================
|
||||
|
||||
@@ -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 & 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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user