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,13 +4,13 @@
# Part of the Fusion Plating product family.
{
'name': 'Fusion Plating Quality (QMS)',
'name': 'Fusion Plating - Quality (QMS)',
'version': '19.0.8.2.0',
'category': 'Manufacturing/Plating',
'summary': 'Native QMS for plating shops: NCR, CAPA, calibration, AVL, FAIR, '
'internal audits, customer specs, document control. CE + EE compatible.',
'description': """
Fusion Plating Quality (QMS)
Fusion Plating - Quality (QMS)
==============================
Part of the Fusion Plating product family by Nexa Systems Inc.
@@ -27,24 +27,24 @@ Odoo records.
Records included
----------------
* Non-Conformance Reports (NCR) containment, disposition, MRB workflow
* Corrective & Preventive Actions (CAPA) root cause, action plan,
* Non-Conformance Reports (NCR) - containment, disposition, MRB workflow
* Corrective & Preventive Actions (CAPA) - root cause, action plan,
effectiveness verification, NCR linkage
* Calibration Equipment register + individual calibration events with
pass / limited / fail and impact assessment
* Approved Vendor List (AVL) supplier approval state, expiry,
* Approved Vendor List (AVL) - supplier approval state, expiry,
scorecard rating
* Customer Specification library industry, customer, and internal
* Customer Specification library - industry, customer, and internal
specs (e.g. AMS 2404, ASTM B733, MIL-C-26074)
* Internal Audits internal, customer, certification, supplier scope
* First Article Inspection Reports (FAIR) per part / revision / customer
* Document Control procedures, work instructions, forms, standards,
* Internal Audits - internal, customer, certification, supplier scope
* First Article Inspection Reports (FAIR) - per part / revision / customer
* Document Control - procedures, work instructions, forms, standards,
manuals with revision tracking and trained-user lists
Integration
-----------
* Reuses the core res.groups.privilege ACLs from fusion_plating
(operator, supervisor, manager, admin) no new groups to manage
(operator, supervisor, manager, admin) - no new groups to manage
* Linked to facilities, baths, and process types from core
* Chatter on every record for full audit trail
* Sequence-numbered references for NCR, CAPA, FAIR, audits
@@ -70,7 +70,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
'fusion_plating_certificates', # fp.thickness.reading link from QC
'fusion_plating_shopfloor', # _fp_shopfloor_tokens.scss for QC tablet
'fusion_plating_receiving', # rma_id on fp.receiving (Sub 12 Phase A)
# NB: deliberately NOT depending on fusion_plating_jobs it depends
# NB: deliberately NOT depending on fusion_plating_jobs - it depends
# on us already (extends fusion.plating.quality.hold). Many2one('fp.job')
# on fp.rma is resolved by the registry once jobs loads after us.
'mail',
@@ -120,15 +120,15 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
'assets': {
'web.assets_backend': [
'fusion_plating_quality/static/src/scss/fusion_plating_quality.scss',
# Phase 2 (Sub 11) QC tablet OWL relocated from bridge_mrp.
# Phase 2 (Sub 11) - QC tablet OWL relocated from bridge_mrp.
'fusion_plating_quality/static/src/scss/fp_qc_checklist.scss',
'fusion_plating_quality/static/src/xml/fp_qc_checklist.xml',
'fusion_plating_quality/static/src/js/fp_qc_checklist.js',
# Sub 12 Phase D Unified Quality Dashboard.
# Sub 12 Phase D - Unified Quality Dashboard.
'fusion_plating_quality/static/src/scss/fp_quality_dashboard.scss',
'fusion_plating_quality/static/src/xml/fp_quality_dashboard.xml',
'fusion_plating_quality/static/src/js/fp_quality_dashboard.js',
# Contract Review flow forced redirect into QA-005 after
# Contract Review flow - forced redirect into QA-005 after
# an estimator creates a part under a toggle-on customer.
# Pairs with the bus.bus push in fp_part_catalog.py:create.
'fusion_plating_quality/static/src/js/contract_review_redirect.js',

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

@@ -22,7 +22,7 @@ from odoo.http import request
_logger = logging.getLogger(__name__)
# Canonical section order drives sections[] in the response.
# Canonical section order - drives sections[] in the response.
# Sections whose model isn't installed are omitted (no error).
SECTION_ORDER = ['cert', 'hold', 'ncr', 'rma', 'capa', 'check']
@@ -50,7 +50,7 @@ TYPE_CONFIG = {
}
# Per-type "overdue" thresholds (reused from the old counts endpoint
# Per-type "overdue" thresholds (reused from the old counts endpoint -
# battle-tested). CAPA branches on due_date < today via use_due_date.
OVERDUE_THRESHOLDS = {
'cert': {'days': 1, 'use_due_date': False,
@@ -152,7 +152,7 @@ class FpQualityDashboardSnapshot:
Returns list of item dicts in the snapshot shape.
"""
state_dom = list(OVERDUE_THRESHOLDS[type_code]['state_domain'])
# We need urgency labels per record fetch open set, sort in
# We need urgency labels per record - fetch open set, sort in
# Python by (overdue_flag, age) so the top-N reflects urgency.
recs = Model.search(state_dom, limit=200) # safety cap
if not recs:
@@ -169,7 +169,7 @@ class FpQualityDashboardSnapshot:
for r in top]
def _overdue_ids(self, type_code, Model):
"""IDs of overdue records for the type reuses _overdue_count
"""IDs of overdue records for the type - reuses _overdue_count
domain logic."""
cfg = OVERDUE_THRESHOLDS[type_code]
dom = list(cfg['state_domain'])
@@ -184,7 +184,7 @@ class FpQualityDashboardSnapshot:
def _build_item(self, type_code, rec, is_overdue):
"""Shape one record into the snapshot item dict."""
cfg = TYPE_CONFIG[type_code]
# Customer name partner_id direct, OR via job_id for check
# Customer name - partner_id direct, OR via job_id for check
partner = self._resolve_partner(rec)
return {
'id': rec.id,
@@ -215,7 +215,7 @@ class FpQualityDashboardSnapshot:
def _build_subtitle(self, type_code, rec, is_overdue):
"""Second-line text per type. Implementation-phase choice
per spec open question 1."""
# Age in human terms hours if < 24h, days otherwise
# Age in human terms - hours if < 24h, days otherwise
if rec.create_date:
age = self.now - rec.create_date
hours = int(age.total_seconds() / 3600)
@@ -232,7 +232,7 @@ class FpQualityDashboardSnapshot:
def _fetch_banner_candidates(self, type_code, Model):
"""Per-type pull of records that qualify for the banner:
(overdue) OR (critical-customer AND state-is-open). Returns
list of (rec, urgency, critical_badge) tuples deduped.
list of (rec, urgency, critical_badge) tuples - deduped.
"""
overdue_ids = set(self._overdue_ids(type_code, Model))
critical_ids = set(self._critical_customer_ids(type_code, Model))
@@ -281,7 +281,7 @@ class FpQualityDashboardSnapshot:
if not critical_partner_ids:
return []
partner_path = partner_field_map.get(type_code, 'partner_id')
# Compose the record-side filter direct or via dotted path
# Compose the record-side filter - direct or via dotted path
dom = list(OVERDUE_THRESHOLDS[type_code]['state_domain'])
dom.append((partner_path, 'in', critical_partner_ids))
try:
@@ -303,7 +303,7 @@ class FpQualityDashboardSnapshot:
return 'RUSH'
if 'x_fc_vip' in partner._fields and getattr(partner, 'x_fc_vip', False):
return 'VIP'
# Aerospace check part name OR spec code if reachable
# Aerospace - check part name OR spec code if reachable
for path in ('part_catalog_id.name', 'customer_spec_id.code'):
head, _, attr = path.partition('.')
if head in rec._fields and rec[head] \

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc. DEMO DATA (temporary)
Copyright 2026 Nexa Systems Inc. - DEMO DATA (temporary)
Remove this file and its manifest entry before production release.
-->
<odoo noupdate="1">
@@ -13,7 +13,7 @@
<field name="state">containment</field>
<field name="source">inspection</field>
<field name="severity">high</field>
<field name="part_ref">P/N 4422-B Hydraulic Cylinder Rod</field>
<field name="part_ref">P/N 4422-B - Hydraulic Cylinder Rod</field>
<field name="reported_date" eval="(DateTime.today() - timedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="description" type="html"><p>EN deposit thickness below spec on OD of part. Spec calls for 0.0005" ± 0.0001", measured 0.0003" average across 4 readings. Bath temperature was at low end of range (185°F vs 188°F target). Possible root cause: heater element degradation.</p></field>
<field name="quantity_affected">12</field>
@@ -26,7 +26,7 @@
<field name="state">open</field>
<field name="source">customer</field>
<field name="severity">critical</field>
<field name="part_ref">P/N 7810-A Landing Gear Pin</field>
<field name="part_ref">P/N 7810-A - Landing Gear Pin</field>
<field name="reported_date" eval="(DateTime.today() - timedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="description" type="html"><p>Customer reported micro-cracking on hard chrome deposit. Parts returned for investigation. Lot of 6 pins from WO-2026-0412. Immediate containment: quarantine remaining stock from same bath run.</p></field>
<field name="quantity_affected">6</field>
@@ -38,12 +38,12 @@
<field name="state">closed</field>
<field name="source">shop_floor</field>
<field name="severity">low</field>
<field name="part_ref">P/N 1133-C Bracket Assembly</field>
<field name="part_ref">P/N 1133-C - Bracket Assembly</field>
<field name="reported_date" eval="(DateTime.today() - timedelta(days=30)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="closed_date" eval="(DateTime.today() - timedelta(days=20)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="description" type="html"><p>Minor cosmetic discolouration on black oxide finish. Traced to elevated bath temperature (147°C vs 141°C target). Thermostat recalibrated. Parts accepted by customer with concession.</p></field>
<field name="root_cause" type="html"><p>Thermostat drift on BOX-01 tank heater. Last calibration was 14 months ago (overdue).</p></field>
<field name="containment" type="html"><p>Segregated affected lot. Verified all parts visually. 4 of 20 showed discolouration reworked.</p></field>
<field name="containment" type="html"><p>Segregated affected lot. Verified all parts visually. 4 of 20 showed discolouration - reworked.</p></field>
<field name="disposition">rework</field>
<field name="quantity_affected">20</field>
</record>
@@ -57,7 +57,7 @@
<field name="state">implementation</field>
<field name="due_date" eval="(DateTime.today() + timedelta(days=15)).strftime('%Y-%m-%d')"/>
<field name="description" type="html"><p>Corrective action for NCR-2026-003: black oxide thermostat drift causing out-of-spec bath temperature.</p></field>
<field name="root_cause_analysis" type="html"><p>Root cause: calibration interval for tank heater thermostats was set to 18 months. Industry best practice for hot-process tanks is 612 months. Maintenance PM schedule did not flag the overdue calibration.</p></field>
<field name="root_cause_analysis" type="html"><p>Root cause: calibration interval for tank heater thermostats was set to 18 months. Industry best practice for hot-process tanks is 6-12 months. Maintenance PM schedule did not flag the overdue calibration.</p></field>
<field name="action_plan" type="html"><p>1. Reduce calibration interval for all hot-process thermostats to 6 months.<br/>2. Add calibration due-date alerts to the maintenance dashboard.<br/>3. Retrain maintenance team on calibration SOP revision.<br/>4. Verify all other hot-process tank thermostats within 30 days.</p></field>
</record>
@@ -72,7 +72,7 @@
<!-- ========== CALIBRATION EQUIPMENT ========== -->
<record id="demo_cal_thickness" model="fusion.plating.calibration.equipment">
<field name="name">XRF Thickness Gauge Fischer XDL-B</field>
<field name="name">XRF Thickness Gauge - Fischer XDL-B</field>
<field name="code">CAL-XRF-01</field>
<field name="facility_id" ref="fusion_plating.demo_facility_main"/>
<field name="equipment_type">xrf</field>
@@ -83,7 +83,7 @@
</record>
<record id="demo_cal_ph" model="fusion.plating.calibration.equipment">
<field name="name">pH Meter Hanna HI-2020</field>
<field name="name">pH Meter - Hanna HI-2020</field>
<field name="code">CAL-PH-01</field>
<field name="facility_id" ref="fusion_plating.demo_facility_main"/>
<field name="equipment_type">ph_meter</field>
@@ -94,7 +94,7 @@
</record>
<record id="demo_cal_temp" model="fusion.plating.calibration.equipment">
<field name="name">Thermocouple Probe Fluke 52 II</field>
<field name="name">Thermocouple Probe - Fluke 52 II</field>
<field name="code">CAL-TC-01</field>
<field name="facility_id" ref="fusion_plating.demo_facility_main"/>
<field name="equipment_type">thermocouple</field>
@@ -106,7 +106,7 @@
<!-- ========== DOC CONTROL ========== -->
<record id="demo_doc_sop_en" model="fusion.plating.doc.control">
<field name="name">SOP-EN-001 Electroless Nickel Plating Procedure</field>
<field name="name">SOP-EN-001 - Electroless Nickel Plating Procedure</field>
<field name="doc_type">procedure</field>
<field name="revision">Rev C</field>
<field name="state">effective</field>
@@ -115,7 +115,7 @@
</record>
<record id="demo_doc_sop_cr" model="fusion.plating.doc.control">
<field name="name">SOP-CR-001 Hard Chrome Plating Procedure</field>
<field name="name">SOP-CR-001 - Hard Chrome Plating Procedure</field>
<field name="doc_type">procedure</field>
<field name="revision">Rev B</field>
<field name="state">effective</field>
@@ -124,7 +124,7 @@
</record>
<record id="demo_doc_qm" model="fusion.plating.doc.control">
<field name="name">QM-001 Quality Manual</field>
<field name="name">QM-001 - Quality Manual</field>
<field name="doc_type">manual</field>
<field name="revision">Rev 5</field>
<field name="state">effective</field>

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

@@ -4,7 +4,7 @@
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Sub 12 Phase B seed data for the kanban stage namespace + a small
Sub 12 Phase B - seed data for the kanban stage namespace + a small
starter set of tags, reasons, and one default quality team. All are
`noupdate=1` so a customer's edits survive module upgrades.
-->

View File

@@ -2,7 +2,7 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1
#
# Phase 1 (Sub 11) relocate fusion.plating.quality.check (+line)
# Phase 1 (Sub 11) - relocate fusion.plating.quality.check (+line)
# from fusion_plating_bridge_mrp to fusion_plating_quality.
# Drop the legacy production_id column on the existing table.
@@ -13,7 +13,7 @@ _logger = logging.getLogger(__name__)
def migrate(cr, version):
if not version:
return # Fresh install nothing to migrate
return # Fresh install - nothing to migrate
patterns = [
'model_fusion_plating_quality_check',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 4 Contract Review QWeb report action.
Sub 4 - Contract Review QWeb report action.
-->
<odoo>

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 4 Contract Review QA-005 QWeb template (1:1 paper form).
Sub 4 - Contract Review QA-005 QWeb template (1:1 paper form).
-->
<odoo>
@@ -80,7 +80,7 @@
<td><span t-field="doc.date_received"/></td>
<td><b>Qty</b></td>
<td><span t-field="doc.qty"/>
Due <span t-field="doc.due_date"/></td>
- Due <span t-field="doc.due_date"/></td>
</tr>
</table>
@@ -225,7 +225,7 @@
<tr><td>1</td><td>Not Likely</td><td>Current approach / process will effectively avoid this risk</td></tr>
<tr><td>2</td><td>Low Likelihood</td><td>Current approach / process has usually mitigated</td></tr>
<tr><td>3</td><td>Likely</td><td>Current approach / process may mitigate this risk</td></tr>
<tr><td>4</td><td>Highly Likely</td><td>Current approach / process cannot mitigate different approach might</td></tr>
<tr><td>4</td><td>Highly Likely</td><td>Current approach / process cannot mitigate - different approach might</td></tr>
<tr><td>5</td><td>Near Certainty</td><td>Current approach / process cannot mitigate the risk and no processes are available</td></tr>
</table>
</div>

View File

@@ -1,4 +1,4 @@
# Battle test real shop failure modes.
# Battle test - real shop failure modes.
#
# This is the "what if my operator is sloppy / forgetful / lazy" suite.
# We document what the system does TODAY, then identify what's missing.
@@ -41,7 +41,7 @@ def make_job(po_suffix):
# ====================================================================== 1
print('='*72)
print('SCENARIO 1 Carlos forgot to click Start. Realizes 2 hours later.')
print('SCENARIO 1 - Carlos forgot to click Start. Realizes 2 hours later.')
print('='*72)
job = make_job('S1-' + fields.Datetime.now().strftime('%H%M%S'))
step = job.step_ids.sorted('sequence')[0]
@@ -58,7 +58,7 @@ print(f' GAP: No "Adjust Time" affordance for forgetful operators.')
# ====================================================================== 2
print()
print('='*72)
print('SCENARIO 2 Carlos finished step physically. Forgot Finish. Went home.')
print('SCENARIO 2 - Carlos finished step physically. Forgot Finish. Went home.')
print('='*72)
job = make_job('S2-' + fields.Datetime.now().strftime('%H%M%S'))
step = job.step_ids.sorted('sequence')[0]
@@ -77,7 +77,7 @@ print(f' GAP: No way to retroactively correct the timelog interval.')
# ====================================================================== 3
print()
print('='*72)
print('SCENARIO 3 Two operators tap Start on the same step.')
print('SCENARIO 3 - Two operators tap Start on the same step.')
print('='*72)
job = make_job('S3-' + fields.Datetime.now().strftime('%H%M%S'))
step = job.step_ids.sorted('sequence')[0]
@@ -97,7 +97,7 @@ except Exception as e:
# ====================================================================== 4
print()
print('='*72)
print('SCENARIO 4 Operator finishes step #6 before #5 is started.')
print('SCENARIO 4 - Operator finishes step #6 before #5 is started.')
print('='*72)
job = make_job('S4-' + fields.Datetime.now().strftime('%H%M%S'))
steps = job.step_ids.sorted('sequence')
@@ -119,7 +119,7 @@ print(f' But there\'s no "force serial" flag for steps that MUST be in order.')
# ====================================================================== 5
print()
print('='*72)
print('SCENARIO 5 Job stuck mid-process. Manager wants to take over.')
print('SCENARIO 5 - Job stuck mid-process. Manager wants to take over.')
print('='*72)
job = make_job('S5-' + fields.Datetime.now().strftime('%H%M%S'))
step = job.step_ids.sorted('sequence')[0]
@@ -141,7 +141,7 @@ print(f' Bob finishes: state={step.state}, finished_by={step.finished_by_user_i
# ====================================================================== 6
print()
print('='*72)
print('SCENARIO 6 Bake window expired (operator at lunch). Override?')
print('SCENARIO 6 - Bake window expired (operator at lunch). Override?')
print('='*72)
BW = env['fusion.plating.bake.window']
Bath = env['fusion.plating.bath']
@@ -169,7 +169,7 @@ except Exception as e:
# ====================================================================== 7
print()
print('='*72)
print('SCENARIO 7 Operator clocks 6 hours on a step expected to take 30 min.')
print('SCENARIO 7 - Operator clocks 6 hours on a step expected to take 30 min.')
print('='*72)
job = make_job('S7-' + fields.Datetime.now().strftime('%H%M%S'))
step = job.step_ids.sorted('sequence')[0]
@@ -186,7 +186,7 @@ print(f' GAP: System silently accepted 12x overrun. No alert, no chatter post.'
# ====================================================================== 8
print()
print('='*72)
print('SCENARIO 8 Operator did 4 of 5 parts. 1 contaminated. Qty drift.')
print('SCENARIO 8 - Operator did 4 of 5 parts. 1 contaminated. Qty drift.')
print('='*72)
job = make_job('S8-' + fields.Datetime.now().strftime('%H%M%S'))
print(f' Job qty={job.qty}, qty_done={job.qty_done}, qty_scrapped={job.qty_scrapped}')
@@ -196,7 +196,7 @@ for s in job.step_ids.sorted('sequence'):
s.button_start()
if s.state == 'in_progress':
s.button_finish()
# Try to mark done qty_done is still 0
# Try to mark done - qty_done is still 0
try:
job.button_mark_done()
print(f' Job done: qty_done={job.qty_done}, qty_scrapped={job.qty_scrapped}')

View File

@@ -1,4 +1,4 @@
# Battle test v2 re-verify after fixes for: bake-window override,
# Battle test v2 - re-verify after fixes for: bake-window override,
# duration overrun chatter, qty reconciliation, recompute-duration.
from datetime import timedelta
@@ -32,7 +32,7 @@ def make_job(po_suffix):
# ====================================================================== Fix 1
print('='*72)
print('FIX 1 Bake-window: missed_window blocks, manager override allowed + audited')
print('FIX 1 - Bake-window: missed_window blocks, manager override allowed + audited')
print('='*72)
BW = env['fusion.plating.bake.window']
Bath = env['fusion.plating.bath']
@@ -47,7 +47,7 @@ BW._cron_update_states()
expired.invalidate_recordset()
print(f' Window {expired.name} state: {expired.state}')
# Naive operator (no override) should fail
# Naive operator (no override) - should fail
try:
expired.action_start_bake()
print(f' ❌ start_bake worked without override')
@@ -67,7 +67,7 @@ except Exception as e:
# ====================================================================== Fix 2
print()
print('='*72)
print('FIX 2 Duration overrun: > 1.5x expected posts chatter warning')
print('FIX 2 - Duration overrun: > 1.5x expected posts chatter warning')
print('='*72)
job = make_job('F2-' + fields.Datetime.now().strftime('%H%M%S'))
step = job.step_ids.sorted('sequence')[0]
@@ -90,7 +90,7 @@ if overrun_msgs:
# ====================================================================== Fix 3
print()
print('='*72)
print('FIX 3 Qty reconciliation: job mark-done blocks if qty mismatch')
print('FIX 3 - Qty reconciliation: job mark-done blocks if qty mismatch')
print('='*72)
job = make_job('F3-' + fields.Datetime.now().strftime('%H%M%S'))
for s in job.step_ids.sorted('sequence'):
@@ -120,7 +120,7 @@ except Exception as e:
# ====================================================================== Fix 4
print()
print('='*72)
print('FIX 4 Supervisor edits timelog → Recompute Duration action picks it up')
print('FIX 4 - Supervisor edits timelog → Recompute Duration action picks it up')
print('='*72)
job = make_job('F4-' + fields.Datetime.now().strftime('%H%M%S'))
step = job.step_ids.sorted('sequence')[0]

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""Battle test post-shop state machine (awaiting_cert + awaiting_ship).
"""Battle test - post-shop state machine (awaiting_cert + awaiting_ship).
Spec: docs/superpowers/specs/2026-05-25-post-shop-cert-shipping-job-states-design.md
Plan: docs/superpowers/plans/2026-05-25-post-shop-cert-shipping-job-states-plan.md
@@ -16,7 +16,7 @@ The script rolls back at the end so it leaves the DB clean.
2. Walk every step to terminal → assert state='awaiting_cert' (or
'awaiting_ship' if no cert required)
3. Assert card appears in plant_kanban under 'inspection' / 'shipping'
4. Assert activity scheduled on a QM (notification fire is async
4. Assert activity scheduled on a QM (notification fire is async -
skip strict email assertion in this test)
5. As a Technician, call cert.action_issue() → assert AccessError
6. As a QM, call cert.action_issue() → state='issued', job state→'awaiting_ship'
@@ -51,7 +51,7 @@ _assert(bool(partner), 'partner exists')
product = env['product.product'].search([], limit=1)
_assert(bool(product), 'product exists')
# Role lookups (transitive via all_group_ids Owners reach QM via implication)
# Role lookups (transitive via all_group_ids - Owners reach QM via implication)
qm_gid = env.ref('fusion_plating.group_fp_quality_manager').id
mgr_gid = env.ref('fusion_plating.group_fp_manager').id
tech_gid = env.ref('fusion_plating.group_fp_technician').id
@@ -88,7 +88,7 @@ step = env['fp.job.step'].create({
_assert(job.state == 'in_progress', 'job created in_progress')
# ---- 2. Finish the step → auto-advance -----------------------------
# Bypass other gates that aren't relevant here (qty, bake, qc not
# Bypass other gates that aren't relevant here (qty, bake, qc - not
# the system under test).
ctx = {
'fp_skip_required_inputs_gate': True,
@@ -135,11 +135,11 @@ if cert_required_path:
except AccessError:
print('OK - Technician issue raised AccessError')
except UserError as e:
# Tech might hit a UserError gate before the ACL check fires
# Tech might hit a UserError gate before the ACL check fires -
# accept that as "tech blocked" too.
print(f'OK - Technician blocked: UserError: {str(e)[:80]}')
# QM issues first pre-fill the gates so action_issue can proceed
# QM issues - first pre-fill the gates so action_issue can proceed
if not cert.spec_reference:
cert.spec_reference = 'TEST-SPEC'
if not cert.process_description:
@@ -201,7 +201,7 @@ if cert_required_path and job.state == 'awaiting_cert':
try:
new_cert.with_user(qm).action_issue()
job.invalidate_recordset()
# Need ALL required certs issued for the advance there may be
# Need ALL required certs issued for the advance - there may be
# a remaining voided cert from step 8 that's still in draft/etc.
# Just check that state has moved off awaiting_cert.
print(f'OK - re-issue path: job state now {job.state}')
@@ -218,6 +218,6 @@ if job.state == 'awaiting_ship':
print()
print('--- bt_post_shop_states: ALL PASS ---')
# Leave DB clean rollback the test data.
# Leave DB clean - rollback the test data.
env.cr.rollback()
print('rolled back test data')

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""Quality Dashboard redesign entech smoke.
"""Quality Dashboard redesign - entech smoke.
Spec: docs/superpowers/specs/2026-05-25-quality-dashboard-redesign-design.md
Plan: docs/superpowers/plans/2026-05-25-quality-dashboard-redesign-plan.md

View File

@@ -1,9 +1,9 @@
# Scenario 10 Carlos paused for lunch. Got pulled to another job. Step
# Scenario 10 - Carlos paused for lunch. Got pulled to another job. Step
# is now sitting in 'paused' state for 3 days. No alert. Costing is wrong
# (the open timelog row was already closed at pause, but the step shows
# zero progress).
#
# Real shop pattern: this happens daily interruptions, shift change,
# Real shop pattern: this happens daily - interruptions, shift change,
# operator pulled to rush job.
#
# What we want:
@@ -42,7 +42,7 @@ step.button_start()
step.button_pause()
print(f'[Carlos] Started + paused step "{step.name}" (state={step.state})')
# Simulate 3 days passing backdate the pause by setting date_started
# Simulate 3 days passing - backdate the pause by setting date_started
step.date_started = fields.Datetime.now() - timedelta(days=3)
print(f' Pretending it has been paused 3 days')

View File

@@ -1,6 +1,6 @@
# Scenario 11 Carlos plating step #4 in tank 3. 8 minutes in, the
# Scenario 11 - Carlos plating step #4 in tank 3. 8 minutes in, the
# rectifier dies. Parts come out half-plated. Carlos needs to:
# 1. Abort the current step (parts not finished but partial work
# 1. Abort the current step (parts not finished - but partial work
# already happened)
# 2. Switch to backup tank 5
# 3. Restart the step there
@@ -14,7 +14,7 @@
#
# Operator's options today:
# A) button_cancel → state=cancelled, but then step shows as cancelled
# and won't be replayed. Not what we want we WANT a retry.
# and won't be replayed. Not what we want - we WANT a retry.
# B) button_finish + open NCR manually + create a new step manually?
# Way too much paperwork.
# C) button_pause + change tank_id + button_start → preserves history
@@ -88,7 +88,7 @@ print()
# Today's options:
print(f' Today\'s options the operator has:')
print(f' A) button_cancel → step becomes cancelled (job stuck no replay)')
print(f' A) button_cancel → step becomes cancelled (job stuck - no replay)')
print(f' B) button_pause + write tank_id + button_start (no failure record)')
print(f' C) ???')
print()

View File

@@ -1,5 +1,5 @@
# Scenario 12 Sarah enters SO qty=5. Job spawns with qty=5. Carlos
# starts step 1. Customer calls they want 8 instead of 5. Sarah edits
# Scenario 12 - Sarah enters SO qty=5. Job spawns with qty=5. Carlos
# starts step 1. Customer calls - they want 8 instead of 5. Sarah edits
# the SO line from 5 to 8.
#
# Question: does the job pick up the change?
@@ -42,7 +42,7 @@ step.button_start()
print(f' Carlos started step "{step.name}" (state={step.state})')
print()
# Customer calls wants 8 not 5
# Customer calls - wants 8 not 5
print(f' 📞 Customer: "Make it 8 instead of 5"')
print(f' Sarah edits SO line qty from 5 to 8...')
try:
@@ -63,7 +63,7 @@ if job.qty != sol.product_uom_qty:
else:
print(f' ✓ Job qty auto-updated.')
# Try the reverse what if Sarah tries to LOWER the qty?
# Try the reverse - what if Sarah tries to LOWER the qty?
print()
print(f' Customer changes mind: now wants 3 instead of 8')
try:

View File

@@ -37,7 +37,7 @@ warn = job.message_ids.filtered(lambda m: 'qty changed mid-job' in (m.body or ''
print(f' Warning messages on job: {len(warn)}')
if warn:
print(f' ✓ Chatter warning posted')
print(f' Job.qty still: {job.qty} (unchanged supervisor must explicitly sync)')
print(f' Job.qty still: {job.qty} (unchanged - supervisor must explicitly sync)')
print()
print(f' Bob clicks "Sync qty from SO" on the job')

View File

@@ -32,9 +32,9 @@ if not plating:
plating = job.step_ids.sorted('sequence')[3:4]
plating.instructions = (
'<p><b>Plating bath checklist:</b></p><ul>'
'<li>Verify nickel concentration is 4.05.5 g/L (Fischerscope reading)</li>'
'<li>pH must be 4.44.8 adjust with ammonium hydroxide if needed</li>'
'<li>Bath temp 8893°C, agitation ON</li>'
'<li>Verify nickel concentration is 4.0-5.5 g/L (Fischerscope reading)</li>'
'<li>pH must be 4.4-4.8 - adjust with ammonium hydroxide if needed</li>'
'<li>Bath temp 88-93°C, agitation ON</li>'
'<li>Dwell 45 minutes for 25 µm coating; longer for thicker</li>'
'<li>Rinse for 60s before next station</li></ul>'
)
@@ -57,7 +57,7 @@ from odoo.http import request as _req
# Find code prefix used
print(f' Step code: {plating.id}, name: {plating.name}')
# Direct call to the scan response builder (no http) easier approach:
# Direct call to the scan response builder (no http) - easier approach:
# The scan endpoint builds the dict inline. Verify by replicating its code path.
step = plating
payload = {

View File

@@ -1,10 +1,10 @@
# Scenario 14 Recipe author wants step "Plating" to be hard-blocked
# Scenario 14 - Recipe author wants step "Plating" to be hard-blocked
# until step "Acid Etch" finishes. (Real reason: passivation layer
# starts forming on bare metal in seconds; if Plating starts before
# acid etch is done, adhesion fails.)
#
# Today the system allows ANY step to start any time. Out-of-order is
# allowed for parallel work but for SERIAL-MUST steps, there's no
# allowed for parallel work - but for SERIAL-MUST steps, there's no
# enforcement. We need an opt-in flag the recipe author can set per
# step: requires_predecessor_done.

View File

@@ -1,7 +1,7 @@
# Scenario 15 Job has a coating that requires hydrogen embrittlement
# Scenario 15 - Job has a coating that requires hydrogen embrittlement
# bake. Operator finishes plating step → bake.window auto-spawns
# (state=awaiting_bake). Operator finishes the rest of the steps and
# clicks Mark Done on the job but never started the bake.
# clicks Mark Done on the job - but never started the bake.
#
# Today: job closes done. Customer ships parts. Field failure 3 weeks
# later. AS9100 auditor: "Show me the bake record for lot X." There's
@@ -44,7 +44,7 @@ so = env['sale.order'].browse(r['res_id'])
so.action_confirm()
job = env['fp.job'].search([('sale_order_id', '=', so.id)], limit=1)
# Walk all steps to done the plating step will spawn a bake.window
# Walk all steps to done - the plating step will spawn a bake.window
for s in job.step_ids.sorted('sequence'):
if s.state in ('pending', 'ready'):
s.button_start()
@@ -61,11 +61,11 @@ for bw in bws:
print(f' {bw.name}: state={bw.state}, required_by={bw.bake_required_by}')
print()
print(f' [Operator careless] Clicks Mark Done WITHOUT starting bake')
print(f' [Operator - careless] Clicks Mark Done WITHOUT starting bake')
try:
job.button_mark_done()
print(f' ❌ Job closed with bake awaiting! state={job.state}')
print(f' COMPLIANCE BOMB no bake record but parts ship.')
print(f' COMPLIANCE BOMB - no bake record but parts ship.')
except Exception as e:
print(f' ✓ Blocked: {str(e)[:200]}')

View File

@@ -1,4 +1,4 @@
# Scenario 16 Carlos clicked Start on a step. Got pulled to a rush
# Scenario 16 - Carlos clicked Start on a step. Got pulled to a rush
# job. Forgot to come back. The original step is still in_progress 8
# hours later. The open timelog row is accumulating phantom time. Cost
# rollup is wrong. Manager has no nudge.

View File

@@ -1,6 +1,6 @@
# Scenario 17 Mid-job Carlos drops 2 parts (out of 5). Sets
# Scenario 17 - Mid-job Carlos drops 2 parts (out of 5). Sets
# qty_scrapped from 0 → 2. With my qty-reconciliation gate, he MUST
# update this for the job to close but there's no NCR / hold record
# update this for the job to close - but there's no NCR / hold record
# explaining WHY 2 parts went away.
#
# Real shop: every scrap event is investigated. Material cost lost,

View File

@@ -1,4 +1,4 @@
# Scenario 18 Certificate flow simulation.
# Scenario 18 - Certificate flow simulation.
# Persona: Sarah (CSR) → Carlos (operator) → Tom (shipper)
# Goal: complete cert issuance from SO entry to customer email.
# Track every gap.
@@ -81,7 +81,7 @@ if not certs:
cert = certs[0]
# DISCOVERABILITY would Tom find the cert from the job form?
# DISCOVERABILITY - would Tom find the cert from the job form?
print()
print(f' [Tom] Looking at the job form, smart-button row:')
print(f' job.certificate_count = {getattr(job, "certificate_count", "no field")}')
@@ -99,7 +99,7 @@ except Exception as e:
# If blocked due to spec_reference, fix and retry
if cert.state == 'draft' and not cert.spec_reference:
print()
print(f' [Tom] Manually fills spec_reference (workflow gap should auto-fill from coating)')
print(f' [Tom] Manually fills spec_reference (workflow gap - should auto-fill from coating)')
cert.spec_reference = coating.spec_reference or 'AMS 2404'
try:
cert.action_issue()
@@ -110,7 +110,7 @@ if cert.state == 'draft' and not cert.spec_reference:
# Try Send to Customer
print()
print(f' [Tom] Clicks Send to Customer:')
print(f' cert.attachment_id = {cert.attachment_id.name if cert.attachment_id else "(none PDF not generated!)"}')
print(f' cert.attachment_id = {cert.attachment_id.name if cert.attachment_id else "(none - PDF not generated!)"}')
try:
act = cert.action_send_to_customer()
print(f' Composer opens. Default attachments: '

View File

@@ -1,4 +1,4 @@
# Scenario 19 Fischerscope thickness report PDF appended to CoC.
# Scenario 19 - Fischerscope thickness report PDF appended to CoC.
#
# Goal: when QC has a thickness_report_pdf_id uploaded by the operator
# on the tablet, action_issue should produce a multi-page CoC with the
@@ -104,7 +104,7 @@ cert = Cert.search([('x_fc_job_id', '=', job.id)], limit=1)
print()
print(f' Cert: {cert.name}, state={cert.state}')
# v19.0.6.20.0 new UI visibility fields (S19 Phase 2). Assert the
# v19.0.6.20.0 - new UI visibility fields (S19 Phase 2). Assert the
# operator would see "Will Append on Issue" badge BEFORE clicking Issue.
print(f' x_fc_thickness_status (pre-Issue): {cert.x_fc_thickness_status!r}')
print(f' x_fc_thickness_qc_id: {cert.x_fc_thickness_qc_id.name if cert.x_fc_thickness_qc_id else "(none)"}')
@@ -112,11 +112,11 @@ print(f' x_fc_thickness_pdf_id: {cert.x_fc_thickness_pdf_id.name
if cert.x_fc_thickness_status == 'pending':
print(f' ✓ UI banner WILL show "Fischerscope thickness PDF is on file"')
elif cert.x_fc_thickness_status == 'none':
print(f' ❌ UI says no PDF merge would not run on Issue')
print(f' ❌ UI says no PDF - merge would not run on Issue')
else:
print(f' ⚠️ unexpected status: {cert.x_fc_thickness_status}')
# Issue the cert should render CoC + merge Fischerscope as page 2
# Issue the cert - should render CoC + merge Fischerscope as page 2
cert.action_issue()
cert.invalidate_recordset(['x_fc_thickness_status', 'x_fc_thickness_qc_id', 'x_fc_thickness_pdf_id'])
print(f' After Issue: state={cert.state}')
@@ -141,7 +141,7 @@ if cert.attachment_id:
if len(reader.pages) >= 2:
print(f' ✓ CoC + Fischerscope merged (multi-page)')
else:
print(f' ❌ Only 1 page merge did not run')
print(f' ❌ Only 1 page - merge did not run')
except Exception as e:
print(f' ⚠️ couldn\'t parse output PDF: {e}')

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""Battle test S24 Live step priority chain + board state filter.
"""Battle test S24 - Live step priority chain + board state filter.
Run end-to-end via odoo shell with stdin redirection:
@@ -32,7 +32,7 @@ def run():
partner = env['res.partner'].search([('customer_rank', '>', 0)], limit=1)
if not partner:
raise AssertionError(
'No customer partner found seed test data first'
'No customer partner found - seed test data first'
)
recipe = env['fusion.plating.process.node'].search([
@@ -40,7 +40,7 @@ def run():
('child_ids', '!=', False),
], limit=1)
if not recipe:
raise AssertionError('No recipe found seed test data first')
raise AssertionError('No recipe found - seed test data first')
job = env['fp.job'].create({
'partner_id': partner.id,
@@ -52,7 +52,7 @@ def run():
if len(steps) < 3:
raise AssertionError('Need at least 3 steps for the test')
# === Phase A between-step assertion ===
# === Phase A - between-step assertion ===
s1 = steps[0]
s2 = steps[1]
s1.button_start()
@@ -68,9 +68,9 @@ def run():
'Card column should match s2.area_kind=%s, got %s'
% (s2.area_kind, _resolve_card_area(job))
)
_logger.info('[bt_s24] Phase A OK between-step routing correct')
_logger.info('[bt_s24] Phase A OK - between-step routing correct')
# === Phase B paused step assertion ===
# === Phase B - paused step assertion ===
s2.button_start()
s2.button_pause('lunch break')
job.invalidate_recordset(['active_step_id', 'card_state'])
@@ -79,9 +79,9 @@ def run():
'Paused step should remain the live step, got %s'
% job.active_step_id.id
)
_logger.info('[bt_s24] Phase B OK paused step stays live')
_logger.info('[bt_s24] Phase B OK - paused step stays live')
# === Phase C done job filter ===
# === Phase C - done job filter ===
for s in steps:
if s.state != 'done':
if s.state == 'paused':
@@ -104,7 +104,7 @@ def run():
raise AssertionError(
'Done job %s should be filtered off board' % job.id
)
_logger.info('[bt_s24] Phase C OK done jobs filtered off board')
_logger.info('[bt_s24] Phase C OK - done jobs filtered off board')
_logger.info('[bt_s24] ALL ASSERTIONS PASSED')

View File

@@ -1,4 +1,4 @@
# Scenario 9 Carlos starts step, Bob (supervisor) reassigns to Mike.
# Scenario 9 - Carlos starts step, Bob (supervisor) reassigns to Mike.
# Verify chatter audit trail.
from odoo import fields
@@ -36,14 +36,14 @@ print(f' assigned_user_id: {step.assigned_user_id.name}')
before_count = len(job.message_ids)
print(f' Job chatter messages before reassign: {before_count}')
# Bob reassigns to "another user" for the test we just write to ourselves
# Bob reassigns to "another user" - for the test we just write to ourselves
# to simulate, but the field write IS the operation.
print()
print('[Bob] Reassigning step to a different operator...')
# Use a different user if available.
other = env['res.users'].search([('id', '!=', env.user.id), ('share', '=', False)], limit=1)
if not other:
other = env.user # fallback at least the write fires
other = env.user # fallback - at least the write fires
step.assigned_user_id = other.id
step.invalidate_recordset()
job.invalidate_recordset()

View File

@@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
# E2E persona walk order entry from start to finish.
# E2E persona walk - order entry from start to finish.
#
# Personas:
# Sarah Customer Service Rep
# Mike Receiver
# Carlos Plating Operator
# Lisa QC Inspector
# Tom Shipper
# Jane Accounting
# Sarah - Customer Service Rep
# Mike - Receiver
# Carlos - Plating Operator
# Lisa - QC Inspector
# Tom - Shipper
# Jane - Accounting
#
# This script fills every visible-to-operator field per step, walks the
# workflow with no shortcuts, asserts the data is sane after each phase,
@@ -40,7 +40,7 @@ def e2e(env):
findings = []
# ----- pick a real partner with a recipe-able product -----
section('SETUP pick a customer + a part already in the catalog')
section('SETUP - pick a customer + a part already in the catalog')
Partner = env['res.partner']
Part = env['fp.part.catalog']
Coating = env['fp.coating.config']
@@ -59,11 +59,11 @@ def e2e(env):
step('Sarah', f'Part: {part.part_number or part.name} rev {part.revision or "?"} (id={part.id})')
step('Sarah', f'Coating: {coating.display_name if coating else "NONE"} (id={coating.id if coating else 0})')
if not coating:
findings.append('No fp.coating.config found in DB cannot create realistic SO')
findings.append('No fp.coating.config found in DB - cannot create realistic SO')
return findings
# ----- Sarah builds a sale order -----
section('PHASE 1 Sarah (CSR) creates the sale order')
section('PHASE 1 - Sarah (CSR) creates the sale order')
SO = env['sale.order']
SOL = env['sale.order.line']
so_vals = {
@@ -79,13 +79,13 @@ def e2e(env):
'x_fc_delivery_method': 'shipping_partner',
'x_fc_rush_order': False,
'x_fc_is_blanket_order': False,
'x_fc_internal_note': 'E2E test SO full persona walk.',
'x_fc_internal_note': 'E2E test SO - full persona walk.',
'x_fc_external_note': 'Standard plating per spec.',
}
so = SO.create(so_vals)
step('Sarah', f'Created SO {so.name} (id={so.id})')
# add a line fill the part / coating / treatment fields
# add a line - fill the part / coating / treatment fields
product = env['product.product'].search([('sale_ok', '=', True)], limit=1)
if not product:
findings.append('No saleable product available for SO line')
@@ -94,7 +94,7 @@ def e2e(env):
'order_id': so.id,
'product_id': product.id,
'product_uom_qty': 25,
'name': f'{part.part_number or part.name} Plating per coating spec',
'name': f'{part.part_number or part.name} - Plating per coating spec',
'x_fc_part_catalog_id': part.id,
'x_fc_coating_config_id': coating.id,
'x_fc_internal_description': 'Process via standard recipe; bake ASAP.',
@@ -103,7 +103,7 @@ def e2e(env):
line = SOL.create(line_vals)
step('Sarah', f'Added line: {line.product_uom_qty} × {line.name[:40]}')
# confirm does account hold block?
# confirm - does account hold block?
if partner.x_fc_account_hold:
find('Sarah', 'Customer is on account hold; SO confirm should block (or warn)')
try:
@@ -129,7 +129,7 @@ def e2e(env):
find('Sarah', 'NO fp.receiving auto-created on SO confirm! Receiver has nothing to track.')
findings.append('SO confirm did not auto-spawn fp.receiving')
if jobs and not portal_jobs:
find('Sarah', 'fp.job exists but no portal.job mirror customer can\'t track on portal.')
find('Sarah', 'fp.job exists but no portal.job mirror - customer can\'t track on portal.')
findings.append('Portal job mirror missing post-confirm')
# smart-button visibility check
@@ -140,7 +140,7 @@ def e2e(env):
f'NCRs visible? {so.fp_qc_ncr_count_so > 0} (count={so.fp_qc_ncr_count_so})')
# ----- Mike receives parts -----
section('PHASE 2 Mike (Receiver) processes inbound parts')
section('PHASE 2 - Mike (Receiver) processes inbound parts')
receiving = receivings[:1]
if not receiving:
receiving = Receiving.create({
@@ -148,7 +148,7 @@ def e2e(env):
'expected_qty': 25,
})
step('Mike', f'Manually created receiving {receiving.name} (auto-create did not fire)')
find('Mike', 'Had to manually create receiving auto-create from SO confirm is missing')
find('Mike', 'Had to manually create receiving - auto-create from SO confirm is missing')
findings.append('Auto-receiving on SO confirm not wired')
else:
step('Mike', f'Found auto-created receiving {receiving.name} (state={receiving.state})')
@@ -199,17 +199,17 @@ def e2e(env):
racks = Inspection.search([('sale_order_id', '=', so.id)])
step('Mike', f'Racking inspections for this SO: {len(racks)}')
if not racks:
find('Mike', 'Racking inspection NOT auto-created racking crew has nothing to walk.')
find('Mike', 'Racking inspection NOT auto-created - racking crew has nothing to walk.')
findings.append('No racking inspection auto-created post-confirm')
# ----- Carlos works the plating job -----
section('PHASE 3 Carlos (Operator) walks the plating job')
section('PHASE 3 - Carlos (Operator) walks the plating job')
if not jobs:
fail('Carlos', 'No job to work SO confirm did not spawn one. Skipping phase.')
fail('Carlos', 'No job to work - SO confirm did not spawn one. Skipping phase.')
else:
job = jobs[0]
step('Carlos', f'Job {job.name}: state={job.state}, qty={job.qty}, deadline={job.date_deadline}')
step('Carlos', f'Steps: {len(job.step_ids)} recipe={job.recipe_id.name or "(none)"}')
step('Carlos', f'Steps: {len(job.step_ids)} - recipe={job.recipe_id.name or "(none)"}')
if not job.step_ids:
find('Carlos', f'Job has zero steps! Recipe not assigned or not generated. Recipe field: {job.recipe_id}')
findings.append('Job confirmed with zero steps')
@@ -244,7 +244,7 @@ def e2e(env):
done_count = len(job.step_ids.filtered(lambda st: st.state == 'done'))
step('Carlos', f'Walked {done_count}/{len(job.step_ids)} steps to done')
# try to mark job done should hit QC gate if customer requires QC
# try to mark job done - should hit QC gate if customer requires QC
wants_qc = 'x_fc_requires_qc' in partner._fields and partner.x_fc_requires_qc
step('Carlos', f'Customer requires QC? {wants_qc}')
try:
@@ -258,7 +258,7 @@ def e2e(env):
findings.append(f'button_mark_done: {e}')
# ----- Lisa runs QC -----
section('PHASE 4 Lisa (QC) walks the checklist (if any)')
section('PHASE 4 - Lisa (QC) walks the checklist (if any)')
QC = env['fusion.plating.quality.check']
qcs = QC.search([('job_id', 'in', jobs.ids)]) if jobs else QC.browse()
step('Lisa', f'QC checks for this job: {len(qcs)}')
@@ -292,7 +292,7 @@ def e2e(env):
findings.append(f'Job blocked post-QC: {e}')
# ----- Tom ships -----
section('PHASE 5 Tom (Shipper) prepares the delivery')
section('PHASE 5 - Tom (Shipper) prepares the delivery')
Delivery = env['fusion.plating.delivery']
deliveries = Delivery.search([
'|', ('job_ref', 'in', jobs.mapped('name') if jobs else []),
@@ -325,14 +325,14 @@ def e2e(env):
findings.append('Certificate auto-create missing')
# ----- Jane invoices -----
section('PHASE 6 Jane (Accounting) creates and posts invoice')
section('PHASE 6 - Jane (Accounting) creates and posts invoice')
invoices_before = env['account.move'].search_count([
('invoice_origin', '=', so.name),
])
try:
if so.invoice_status == 'to invoice':
inv_action = so._create_invoices()
step('Jane', f'Invoiced {invoices_before}{env["account.move"].search_count([("invoice_origin","=",so.name)])} moves')
step('Jane', f'Invoiced - {invoices_before}{env["account.move"].search_count([("invoice_origin","=",so.name)])} moves')
else:
step('Jane', f'invoice_status={so.invoice_status} (nothing to invoice)')
except Exception as e:
@@ -340,7 +340,7 @@ def e2e(env):
findings.append(f'invoice creation: {e}')
# ----- common-sense edge case sweeps -----
section('PHASE 7 common-sense edge case sweeps')
section('PHASE 7 - common-sense edge case sweeps')
# smart-button results: do they actually return non-empty data?
section_name = ' smart-button result probes'
@@ -381,7 +381,7 @@ def e2e(env):
for i, f in enumerate(findings, 1):
print(f' {i}. {f}')
else:
print(' ✅ No findings workflow is clean end-to-end.')
print(' ✅ No findings - workflow is clean end-to-end.')
env.cr.commit()
return findings

View File

@@ -1,16 +1,16 @@
# Step 1 verification Direct Order wizard onchange + hold guard fixes.
# Step 1 verification - Direct Order wizard onchange + hold guard fixes.
W = env['fp.direct.order.wizard']
ISD = env['fp.invoice.strategy.default']
P = env['res.partner']
target = P.browse(2529) # 2CM INNOVATIVE
print('Test 1 customer with NO invoice strategy default:')
print('Test 1 - customer with NO invoice strategy default:')
ISD.search([('partner_id', '=', target.id)]).unlink()
w = W.new({'partner_id': target.id})
w._onchange_partner_id()
print(f' invoice_strategy={w.invoice_strategy}, payment_term={w.payment_term_id.name if w.payment_term_id else None}')
print('\nTest 2 customer WITH strategy default but NO payment_term:')
print('\nTest 2 - customer WITH strategy default but NO payment_term:')
isd = ISD.create({'partner_id': target.id, 'default_strategy': 'net_terms'})
w = W.new({'partner_id': target.id})
w._onchange_partner_id()
@@ -18,7 +18,7 @@ print(f' invoice_strategy={w.invoice_strategy} (expect: net_terms)')
print(f' payment_term={w.payment_term_id.name if w.payment_term_id else None}')
isd.unlink()
print('\nTest 3 customer with strategy + deposit + payment_term:')
print('\nTest 3 - customer with strategy + deposit + payment_term:')
pt = env['account.payment.term'].search([], limit=1)
isd = ISD.create({
'partner_id': target.id, 'default_strategy': 'deposit',
@@ -31,7 +31,7 @@ print(f' deposit_percent={w.deposit_percent} (expect: 50.0)')
print(f' payment_term={w.payment_term_id.name} (expect: {pt.name})')
isd.unlink()
print('\nTest 4 account-hold warning fires on partner change:')
print('\nTest 4 - account-hold warning fires on partner change:')
target.x_fc_account_hold = True
w = W.new({'partner_id': target.id})
result = w._onchange_partner_id()
@@ -39,7 +39,7 @@ warning = (result or {}).get('warning')
print(f' warning title: {warning.get("title") if warning else None}')
print(f' warning msg: {(warning.get("message") or "")[:100] if warning else None}')
print('\nTest 5 account-hold blocks action_create_order:')
print('\nTest 5 - account-hold blocks action_create_order:')
w = W.create({'partner_id': target.id, 'po_pending': True})
# add one line so the line check passes
part = env['fp.part.catalog'].search([], limit=1)
@@ -53,7 +53,7 @@ env['fp.direct.order.line'].create({
})
try:
w.action_create_order()
print(' ❌ HELD CUSTOMER CREATED ORDER guard failed')
print(' ❌ HELD CUSTOMER CREATED ORDER - guard failed')
except Exception as e:
print(f' ✓ blocked: {str(e)[:120]}')
target.x_fc_account_hold = False

View File

@@ -1,4 +1,4 @@
# Step 2 verification picking a part on the wizard line pre-fills coating + treatments.
# Step 2 verification - picking a part on the wizard line pre-fills coating + treatments.
W = env['fp.direct.order.wizard']
Line = env['fp.direct.order.line']
Part = env['fp.part.catalog']

View File

@@ -1,4 +1,4 @@
# Step 3 Sarah hits "Create Order" in wizard, then confirms SO.
# Step 3 - Sarah hits "Create Order" in wizard, then confirms SO.
# Watch every side-effect: SO state, fp.job auto-spawn, fp.receiving
# auto-spawn, fp.racking.inspection, portal.job mirror, QC check.

View File

@@ -1,4 +1,4 @@
# Step 4 Mike receives parts. Walk the receiving form, fill every
# Step 4 - Mike receives parts. Walk the receiving form, fill every
# visible field, walk the state machine, verify SO status updates at
# every transition.
@@ -54,7 +54,7 @@ try:
print(f' recv.state = {recv.state}')
print(f' SO status: {so.x_fc_receiving_status} (should still be partial)')
assert so.x_fc_receiving_status == 'partial'
print(' ✓ Still partial racking not done yet')
print(' ✓ Still partial - racking not done yet')
except Exception as e:
print(f'{e}')
@@ -71,7 +71,7 @@ try:
except Exception as e:
print(f'{e}')
# At this point Mike's done racking crew takes over.
# At this point Mike's done - racking crew takes over.
# But the receiving stays at "staged" until racking crew finishes
# inspection and someone clicks "Close" on the receiving.
# Let's pretend racking is done and close the receiving.

View File

@@ -1,8 +1,8 @@
# Step 5 Carlos walks the plating job. Test BOTH paths:
# Step 5 - Carlos walks the plating job. Test BOTH paths:
# A) Try to mark_done with steps still ready → must be blocked
# B) Walk every step → mark_done succeeds
# Build a fresh SO + job (don't reuse 423 its job is already done).
# Build a fresh SO + job (don't reuse 423 - its job is already done).
from odoo import fields
W = env['fp.direct.order.wizard']
Line = env['fp.direct.order.line']
@@ -41,7 +41,7 @@ print()
print('[Carlos] Try Mark Done WITHOUT walking any step (compliance test):')
try:
job.button_mark_done()
print(' ❌ JOB CLOSED WITH ZERO STEPS WALKED guard failed')
print(' ❌ JOB CLOSED WITH ZERO STEPS WALKED - guard failed')
except Exception as e:
print(f' ✓ blocked: {str(e)[:200]}')
@@ -58,7 +58,7 @@ print(f' walked {done_count}/{len(job.step_ids)} to done')
try:
job.button_mark_done()
print(f' ✓ Job marked done state={job.state}, finished={job.date_finished}')
print(f' ✓ Job marked done - state={job.state}, finished={job.date_finished}')
except Exception as e:
print(f' ❌ Mark Done failed AFTER walking: {e}')
@@ -90,7 +90,7 @@ job2 = env['fp.job'].search([('sale_order_id', '=', so2.id)], limit=1)
print(f' Created fresh job {job2.name} with {len(job2.step_ids)} unworked steps')
try:
job2.with_context(fp_skip_step_gate=True).button_mark_done()
print(f' ✓ Manager bypass worked job state={job2.state}')
print(f' ✓ Manager bypass worked - job state={job2.state}')
except Exception as e:
print(f' ❌ Bypass failed: {e}')

View File

@@ -1,4 +1,4 @@
# Step 6 Lisa walks the QC checklist for a customer that requires QC.
# Step 6 - Lisa walks the QC checklist for a customer that requires QC.
# Test:
# A) Customer requires QC but no template configured → spawn fails gracefully?
# B) Customer requires QC + template configured → check auto-spawns on confirm
@@ -21,7 +21,7 @@ if not default_tpl:
'name': 'Default QC Template (E2E)',
'active': True,
})
TplLine.create({'template_id': default_tpl.id, 'sequence': 10, 'name': 'Visual inspection appearance'})
TplLine.create({'template_id': default_tpl.id, 'sequence': 10, 'name': 'Visual inspection - appearance'})
TplLine.create({'template_id': default_tpl.id, 'sequence': 20, 'name': 'Thickness measurement (Fischerscope)'})
TplLine.create({'template_id': default_tpl.id, 'sequence': 30, 'name': 'Tape adhesion test'})
print(f'[setup] Created default QC template: {default_tpl.name} ({len(default_tpl.line_ids)} lines)')
@@ -91,7 +91,7 @@ for s in job.step_ids.sorted('sequence'):
s.button_finish()
try:
job.button_mark_done()
print(f' ✓ Job done state={job.state}')
print(f' ✓ Job done - state={job.state}')
except Exception as e:
print(f' ❌ Job mark_done blocked: {e}')

View File

@@ -1,6 +1,6 @@
# Step 7 Tom (Shipper) walks the delivery from draft to delivered.
# Step 7 - Tom (Shipper) walks the delivery from draft to delivered.
# Test:
# A) Delivery exists post-job-done what fields visible? what state?
# A) Delivery exists post-job-done - what fields visible? what state?
# B) Try action_start_route without driver → must block
# C) Assign driver + vehicle + box count, schedule
# D) Try action_mark_delivered without POD → must block

View File

@@ -1,4 +1,4 @@
# Step 8 re-verify fresh SO with net_terms strategy should now get
# Step 8 re-verify - fresh SO with net_terms strategy should now get
# Net-30 payment term auto-filled, and the invoice should post.
from odoo import fields

View File

@@ -1,4 +1,4 @@
# Step 8 Jane creates the invoice for the completed SO and posts it.
# Step 8 - Jane creates the invoice for the completed SO and posts it.
# Test:
# A) SO has invoice_status = 'to invoice' after delivery
# B) Jane creates the invoice
@@ -40,7 +40,7 @@ if so.invoice_status == 'to invoice':
except Exception as e:
print(f'{e}')
else:
print(f' Skipped invoice_status={so.invoice_status} (nothing to invoice)')
print(f' Skipped - invoice_status={so.invoice_status} (nothing to invoice)')
new_invs = env['account.move'].browse()
# Path B: post.

View File

@@ -23,7 +23,7 @@ treats = Treat.search([], limit=2)
# ====================================================================== A
print('='*72)
print('Scenario A Brand-new part (no defaults)')
print('Scenario A - Brand-new part (no defaults)')
print('='*72)
fresh = Part.create({
'partner_id': target.id,
@@ -47,7 +47,7 @@ if result and result.get('warning'):
# ====================================================================== B
print()
print('='*72)
print('Scenario B Existing part WITH defaults already set')
print('Scenario B - Existing part WITH defaults already set')
print('='*72)
existing = Part.search([('x_fc_default_coating_config_id', '!=', False)], limit=1)
print(f' Using part: {existing.display_name} (default coating={existing.x_fc_default_coating_config_id.name})')
@@ -56,14 +56,14 @@ w2._onchange_partner_id()
ln2 = Line.new({'wizard_id': w2.id})
ln2.part_catalog_id = existing
result2 = ln2._onchange_part_clears_variant()
print(f' push_to_defaults after onchange: {ln2.push_to_defaults} (expect False defaults already exist)')
print(f' push_to_defaults after onchange: {ln2.push_to_defaults} (expect False - defaults already exist)')
print(f' pre-filled coating: {ln2.coating_config_id.name if ln2.coating_config_id else "(none)"}')
print(f' warning fired? {bool(result2 and result2.get("warning"))} (expect False)')
# ====================================================================== C
print()
print('='*72)
print('Scenario C Brand-new part flagged is_one_off (don\'t persist)')
print('Scenario C - Brand-new part flagged is_one_off (don\'t persist)')
print('='*72)
fresh3 = Part.create({
'partner_id': target.id,
@@ -75,13 +75,13 @@ w3._onchange_partner_id()
ln3 = Line.new({'wizard_id': w3.id, 'is_one_off': True})
ln3.part_catalog_id = fresh3
result3 = ln3._onchange_part_clears_variant()
print(f' push_to_defaults after onchange: {ln3.push_to_defaults} (expect False is_one_off blocks)')
print(f' push_to_defaults after onchange: {ln3.push_to_defaults} (expect False - is_one_off blocks)')
print(f' warning fired? {bool(result3 and result3.get("warning"))} (expect False)')
# ====================================================================== D
print()
print('='*72)
print('Scenario D End-to-end: order #1 saves defaults, order #2 pre-fills')
print('Scenario D - End-to-end: order #1 saves defaults, order #2 pre-fills')
print('='*72)
fresh_d = Part.create({
'partner_id': target.id,
@@ -96,7 +96,7 @@ w_d._onchange_partner_id()
ln_d = Line.new({'wizard_id': w_d.id})
ln_d.part_catalog_id = fresh_d
ln_d._onchange_part_clears_variant()
print(f' Order #1 line auto-ticked push_to_defaults: {ln_d.push_to_defaults}')
print(f' Order #1 line - auto-ticked push_to_defaults: {ln_d.push_to_defaults}')
# Sarah picks the coating + treatments she wants
saved = Line.create({
'wizard_id': w_d.id, 'part_catalog_id': fresh_d.id,
@@ -117,9 +117,9 @@ print(f' Part defaults after order #1:')
print(f' x_fc_default_coating_config_id: {fresh_d.x_fc_default_coating_config_id.name if fresh_d.x_fc_default_coating_config_id else "(none)"}')
print(f' x_fc_default_treatment_ids: {fresh_d.x_fc_default_treatment_ids.mapped("name") if fresh_d.x_fc_default_treatment_ids else "(none)"}')
# ORDER #2 Sarah picks the same part again
# ORDER #2 - Sarah picks the same part again
print()
print(' Order #2 Sarah picks the same part:')
print(' Order #2 - Sarah picks the same part:')
w_d2 = W.create({'partner_id': target.id, 'po_pending': True, 'invoice_strategy': 'net_terms'})
w_d2._onchange_partner_id()
ln_d2 = Line.new({'wizard_id': w_d2.id})
@@ -127,7 +127,7 @@ ln_d2.part_catalog_id = fresh_d
ln_d2._onchange_part_clears_variant()
print(f' Pre-filled coating: {ln_d2.coating_config_id.name if ln_d2.coating_config_id else "(none)"}')
print(f' Pre-filled treatments: {ln_d2.treatment_ids.mapped("name") if ln_d2.treatment_ids else "(none)"}')
print(f' push_to_defaults: {ln_d2.push_to_defaults} (expect False defaults exist)')
print(f' push_to_defaults: {ln_d2.push_to_defaults} (expect False - defaults exist)')
if ln_d2.coating_config_id == coating:
print(f' ✓ Order #2 correctly auto-filled from order #1\'s saved defaults')
else:

View File

@@ -1,7 +1,7 @@
# Comprehensive internal-process walk.
#
# Phases:
# A) Pause / resume multiple intervals merge into duration_actual
# A) Pause / resume - multiple intervals merge into duration_actual
# B) Skip an opt-in step
# C) Skipped steps don't block job mark-done
# D) Wet plating step finish auto-spawns bake.window with right window_hours
@@ -65,7 +65,7 @@ print(f'[setup] Job {job.name} with {len(job.step_ids)} steps')
# ====================================================================== A
print()
print('='*72)
print('A Pause + resume on a step. Multiple intervals must merge.')
print('A - Pause + resume on a step. Multiple intervals must merge.')
print('='*72)
masking = job.step_ids.sorted('sequence')[0]
masking.button_start()
@@ -90,7 +90,7 @@ else:
# ====================================================================== B
print()
print('='*72)
print('B Skip an opt-in step')
print('B - Skip an opt-in step')
print('='*72)
racking = job.step_ids.sorted('sequence')[1]
print(f' Step: {racking.name} state={racking.state}')
@@ -99,10 +99,10 @@ print(f' After Skip: state={racking.state}')
if racking.state == 'skipped':
print(f' ✓ Skip works')
# ====================================================================== C walk rest, then mark-done
# ====================================================================== C - walk rest, then mark-done
print()
print('='*72)
print('C Walk remaining steps (some will spawn bake-window). Mark job done.')
print('C - Walk remaining steps (some will spawn bake-window). Mark job done.')
print('='*72)
spawn_count_before = env['fusion.plating.bake.window'].search_count([])
for s in job.step_ids.sorted('sequence'):
@@ -121,23 +121,23 @@ bws = env['fusion.plating.bake.window'].search([('part_ref', '=', job.name)])
for bw in bws:
print(f' {bw.name}: state={bw.state}, plate_exit={bw.plate_exit_time}, required_by={bw.bake_required_by}, time_remaining={bw.time_remaining_display}')
# ====================================================================== D try to mark job done
# ====================================================================== D - try to mark job done
print()
print('='*72)
print('D Mark job done (skipped+done steps both count as terminal)')
print('D - Mark job done (skipped+done steps both count as terminal)')
print('='*72)
try:
job.button_mark_done()
print(f' ✓ Job done state={job.state}')
print(f' ✓ Job done - state={job.state}')
except Exception as e:
print(f'{e}')
# ====================================================================== E bake-window lifecycle
# ====================================================================== E - bake-window lifecycle
if bws:
bw = bws[0]
print()
print('='*72)
print('E Bake-window lifecycle: start → end')
print('E - Bake-window lifecycle: start → end')
print('='*72)
print(f' Before start: state={bw.state}, color={bw.status_color}')
bw.action_start_bake()
@@ -146,10 +146,10 @@ if bws:
bw.action_end_bake()
print(f' After end_bake: state={bw.state}, bake_end={bw.bake_end_time}, duration_h={bw.bake_duration_hours:.4f}')
# ====================================================================== F failure: start a done step
# ====================================================================== F - failure: start a done step
print()
print('='*72)
print('F Failure paths')
print('F - Failure paths')
print('='*72)
done_step = job.step_ids.filtered(lambda s: s.state == 'done')[:1]
if done_step:

View File

@@ -1,11 +1,11 @@
# Internal-process walk test time tracking, pause, skip, bake-window
# Internal-process walk - test time tracking, pause, skip, bake-window
# auto-spawn, duration overrun. Persona: Carlos (operator) walking the
# tablet station for a real plating job.
#
# Goals:
# 1) Time tracking captures every start/stop interval correctly
# 2) Multiple intervals (start/finish/start/finish) sum to duration_actual
# 3) Pause / resume flow works (currently NOT implemented gap to fix)
# 3) Pause / resume flow works (currently NOT implemented - gap to fix)
# 4) Skip flow works for opt-in steps (currently NOT implemented)
# 5) Wet plating step finishing auto-spawns a bake.window when the
# coating requires hydrogen embrittlement relief
@@ -46,7 +46,7 @@ print(f'[setup] Fresh job {job.name} with {len(job.step_ids)} steps')
# ====================================================================== STEP 1
print()
print('='*72)
print('STEP 1 Carlos opens the first step on the tablet, clicks Start')
print('STEP 1 - Carlos opens the first step on the tablet, clicks Start')
print('='*72)
first = job.step_ids.sorted('sequence')[0]
print(f' Step: {first.name} (kind={first.kind}, state={first.state})')
@@ -60,7 +60,7 @@ print(f' Open time-log rows: {len(first.time_log_ids.filtered(lambda l: not l.d
# ====================================================================== STEP 2
print()
print('='*72)
print('STEP 2 Carlos works for 6 seconds, then clicks Finish')
print('STEP 2 - Carlos works for 6 seconds, then clicks Finish')
print('='*72)
time.sleep(6)
first.button_finish()
@@ -74,12 +74,12 @@ print(f' ✓ Single interval captured cleanly')
# ====================================================================== STEP 3
print()
print('='*72)
print('STEP 3 Test pause/resume on the next step (currently NotImplementedError)')
print('STEP 3 - Test pause/resume on the next step (currently NotImplementedError)')
print('='*72)
second = job.step_ids.sorted('sequence')[1]
second.button_start()
print(f' Started step: {second.name} (state={second.state})')
print(f' Carlos now needs a smoke break clicks Pause')
print(f' Carlos now needs a smoke break - clicks Pause')
try:
second.button_pause()
print(f' ✓ Paused: state={second.state}, open timelog={len(second.time_log_ids.filtered(lambda l: not l.date_finished))}')
@@ -91,7 +91,7 @@ except Exception as e:
# ====================================================================== STEP 4
print()
print('='*72)
print('STEP 4 Test skip (currently NotImplementedError)')
print('STEP 4 - Test skip (currently NotImplementedError)')
print('='*72)
third = job.step_ids.sorted('sequence')[2]
print(f' Step: {third.name}, state={third.state}')
@@ -107,7 +107,7 @@ except Exception as e:
# ====================================================================== STEP 5
print()
print('='*72)
print('STEP 5 Wet plating step finishes, does a bake.window auto-spawn?')
print('STEP 5 - Wet plating step finishes, does a bake.window auto-spawn?')
print('='*72)
# Find a step with kind='wet' (or use step #4 as plating analog)
wet_step = job.step_ids.filtered(lambda s: 'plating' in (s.name or '').lower())[:1]
@@ -125,7 +125,7 @@ BW = env['fusion.plating.bake.window']
bw_before = BW.search_count([('part_ref', '=', job.name)])
print(f' Bake windows for this job BEFORE finish: {bw_before}')
# Skip if currently in_progress (it is paused step #2 still open)
# Skip if currently in_progress (it is - paused step #2 still open)
if wet_step.state in ('pending', 'ready'):
wet_step.button_start()
if wet_step.state == 'in_progress':
@@ -137,7 +137,7 @@ print(f' Bake windows for this job AFTER finish: {bw_after}')
if coating.requires_bake_relief and bw_after == bw_before:
print(f' ❌ Coating requires bake relief BUT no bake.window was auto-created!')
elif not coating.requires_bake_relief:
print(f' (coating doesn\'t require bake relief auto-spawn would skip anyway)')
print(f' (coating doesn\'t require bake relief - auto-spawn would skip anyway)')
else:
print(f' ✓ Bake window spawned')

View File

@@ -1,7 +1,7 @@
# Walk: Sarah opens Direct Order, creates a brand-new part inline, attaches a process.
#
# Personas:
# Sarah (CSR) driving the wizard
# Sarah (CSR) - driving the wizard
#
# What we're testing:
# 1) Wizard now allows creating a new part (no_quick_create lets the
@@ -30,7 +30,7 @@ print()
# ====================================================================== STEP 2
print('='*72)
print('STEP 2 Sarah opens wizard, hits "Create and edit..." on Part field')
print('STEP 2 - Sarah opens wizard, hits "Create and edit..." on Part field')
print('='*72)
w = W.create({
@@ -56,7 +56,7 @@ print(f'[Sarah] Filled popup → created part: {new_part.display_name}')
print(f' partner_id correctly set: {new_part.partner_id.name}')
print(f' part_number: {new_part.part_number}')
print(f' revision: {new_part.revision}')
print(f' default_process_id: {new_part.default_process_id.name if new_part.default_process_id else "(none no variants composed yet)"}')
print(f' default_process_id: {new_part.default_process_id.name if new_part.default_process_id else "(none - no variants composed yet)"}')
print(f' process_variant_count: {new_part.process_variant_count}')
# Now Sarah adds a line with the new part.
@@ -65,7 +65,7 @@ ln.part_catalog_id = new_part
ln._onchange_part_clears_variant()
print()
print(f'[Sarah] Adds line with new part:')
print(f' Pre-filled coating: {ln.coating_config_id.name if ln.coating_config_id else "(none new part has no defaults)"}')
print(f' Pre-filled coating: {ln.coating_config_id.name if ln.coating_config_id else "(none - new part has no defaults)"}')
print(f' Pre-filled treatments: {ln.treatment_ids.mapped("name") if ln.treatment_ids else "(none)"}')
# Sarah picks coating manually (since new part has no defaults).
@@ -83,13 +83,13 @@ real_line = Line.create({
# Check variant dropdown availability
print()
print(f'[Sarah] Variant dropdown for new part:')
print(f' Available variants: {len(new_part.process_variant_ids)} (expect 0 none composed yet)')
print(f' Available variants: {len(new_part.process_variant_ids)} (expect 0 - none composed yet)')
print(f' → Sarah leaves variant blank; coating.recipe_id will drive job')
# ====================================================================== STEP 3
print()
print('='*72)
print('STEP 3 Sarah confirms order, verify part landed in catalog + job uses coating recipe')
print('STEP 3 - Sarah confirms order, verify part landed in catalog + job uses coating recipe')
print('='*72)
result = w.action_create_order()
so = env['sale.order'].browse(result['res_id'])
@@ -113,7 +113,7 @@ print(f' Part survives in catalog: {fetched.display_name} (id={fetched.id})')
# ====================================================================== STEP 4
print()
print('='*72)
print('STEP 4 Bob attaches a variant to the new part (compose flow)')
print('STEP 4 - Bob attaches a variant to the new part (compose flow)')
print('='*72)
from odoo.addons.fusion_plating_configurator.controllers.fp_part_composer_controller \
import _clone_subtree
@@ -133,10 +133,10 @@ new_part.invalidate_recordset()
print(f' process_variant_count now: {new_part.process_variant_count}')
print(f' default_process_id: {new_part.default_process_id.name}')
# Now Sarah enters a SECOND order this time variant dropdown should show "Standard"
# Now Sarah enters a SECOND order - this time variant dropdown should show "Standard"
print()
print('='*72)
print('STEP 5 Sarah enters a follow-up order; variant dropdown should now show "Standard"')
print('STEP 5 - Sarah enters a follow-up order; variant dropdown should now show "Standard"')
print('='*72)
w2 = W.create({
'partner_id': target.id, 'po_pending': True,

View File

@@ -1,13 +1,13 @@
# Walk part creation + 4 process variants step by step.
# Personas:
# Bob (Estimator) owns the part catalog, designs process variants
# Sarah (CSR) picks a variant on order entry
# Bob (Estimator) - owns the part catalog, designs process variants
# Sarah (CSR) - picks a variant on order entry
#
# Goal: prove that
# 1) Bob can create a part
# 2) Bob can attach 4 distinct process variants via the Composer flow
# 3) One is flagged default; switching default works
# 4) Sarah opens a Direct Order, picks the part variant dropdown lists ALL FOUR
# 4) Sarah opens a Direct Order, picks the part - variant dropdown lists ALL FOUR
# 5) Sarah picks a non-default variant; the SO + job actually use it
from odoo import fields
@@ -23,7 +23,7 @@ Tpl = Node # template recipes are also fp.process.node records
# ====================================================================== STEP 2
print('='*72)
print('STEP 2 Bob creates a brand-new part')
print('STEP 2 - Bob creates a brand-new part')
print('='*72)
target_partner = P.browse(2529) # 2CM INNOVATIVE
default_coating = Coating.search([], limit=1)
@@ -55,14 +55,14 @@ template = Node.search([
('part_catalog_id', '=', False),
], limit=1)
if not template:
print(' ❌ No shared template recipes available cannot continue!')
print(' ❌ No shared template recipes available - cannot continue!')
raise SystemExit
print(f'[Bob] Will clone from shared template: {template.name} ({len(template.child_ids)} root children)')
# ====================================================================== STEP 3
print()
print('='*72)
print('STEP 3 Bob adds variant #1: Standard Production')
print('STEP 3 - Bob adds variant #1: Standard Production')
print('='*72)
v1 = _clone_subtree(env, template, part, parent=False)
v1.variant_label = 'Standard Production'
@@ -75,17 +75,17 @@ print(f' child nodes cloned: {len(v1.child_ids)}')
# ====================================================================== STEP 4
print()
print('='*72)
print('STEP 4 Bob adds variant #2: Aerospace Cert (AS9100)')
print('STEP 4 - Bob adds variant #2: Aerospace Cert (AS9100)')
print('='*72)
v2 = _clone_subtree(env, template, part, parent=False)
v2.variant_label = 'Aerospace Cert (AS9100)'
print(f'[Bob] Created variant: {v2.variant_label} (root id={v2.id})')
print(f' is_default: {v2.is_default_variant} (correct first one stays default)')
print(f' is_default: {v2.is_default_variant} (correct - first one stays default)')
# ====================================================================== STEP 5
print()
print('='*72)
print('STEP 5 Bob adds variant #3: Quick-turn (no bake)')
print('STEP 5 - Bob adds variant #3: Quick-turn (no bake)')
print('='*72)
v3 = _clone_subtree(env, template, part, parent=False)
v3.variant_label = 'Quick-turn (no bake)'
@@ -94,7 +94,7 @@ print(f'[Bob] Created variant: {v3.variant_label} (root id={v3.id})')
# ====================================================================== STEP 6
print()
print('='*72)
print('STEP 6 Bob adds variant #4: Heavy build (wear)')
print('STEP 6 - Bob adds variant #4: Heavy build (wear)')
print('='*72)
v4 = _clone_subtree(env, template, part, parent=False)
v4.variant_label = 'Heavy build (wear)'
@@ -103,18 +103,18 @@ print(f'[Bob] Created variant: {v4.variant_label} (root id={v4.id})')
# Refresh the part and inspect what the form would show.
part.invalidate_recordset()
print()
print(f'[Bob] After 4 adds part {part.display_name}:')
print(f'[Bob] After 4 adds - part {part.display_name}:')
print(f' process_variant_count: {part.process_variant_count}')
print(f' default_process_id: {part.default_process_id.name if part.default_process_id else None}')
print(f' Variants list (per Composer endpoint /fp/part/composer/state):')
for entry in _list_variants(part):
flag = '★ default' if entry['is_default'] else ' '
print(f' {flag} id={entry["id"]:>5} "{entry["label"]}" {entry["node_count"]} nodes')
print(f' {flag} id={entry["id"]:>5} "{entry["label"]}" - {entry["node_count"]} nodes')
# ====================================================================== STEP 7
print()
print('='*72)
print('STEP 7 Sarah enters a Direct Order, picks the part, picks a variant')
print('STEP 7 - Sarah enters a Direct Order, picks the part, picks a variant')
print('='*72)
W = env['fp.direct.order.wizard']
Line = env['fp.direct.order.line']
@@ -163,7 +163,7 @@ print(f' Saved line: process_variant_id={new_line.process_variant_id.variant_la
# ====================================================================== STEP 8
print()
print('='*72)
print('STEP 8 Confirm SO; verify the JOB uses variant #3, not the default')
print('STEP 8 - Confirm SO; verify the JOB uses variant #3, not the default')
print('='*72)
result = w.action_create_order()
so = env['sale.order'].browse(result['res_id'])

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# End-to-end order walkthrough simulates each role on the shop floor.
# End-to-end order walkthrough - simulates each role on the shop floor.
#
# Run via odoo-shell:
# echo 'exec(open("/mnt/extra-addons/custom/fusion_plating_quality/scripts/sub12_e2e_walkthrough.py").read())' \
@@ -24,7 +24,7 @@ def walk():
print('====================== E2E ORDER WALKTHROUGH ======================')
# ------------------------------------------------------------------
# ROLE: Sales / Estimator open Plating > Sales > Quotations
# ROLE: Sales / Estimator - open Plating > Sales > Quotations
# ------------------------------------------------------------------
print('\n[ROLE: Estimator] Plating > Sales > Quotations > New Quote')
@@ -47,7 +47,7 @@ def walk():
part = Part.search([], limit=1)
if not part:
gap('Estimator', 'fp.part.catalog',
'No parts in catalog estimator has nothing to quote against')
'No parts in catalog - estimator has nothing to quote against')
return
print(f' Part chosen: {part.display_name} '
f'(part#={getattr(part, "part_number", "?")} '
@@ -68,7 +68,7 @@ def walk():
coating = e['fp.coating.config'].search([], limit=1)
if not coating:
gap('Estimator', 'fp.coating.config',
'No coating configs defined estimator cannot configure quote')
'No coating configs defined - estimator cannot configure quote')
else:
print(f' Coating chosen: {coating.display_name}')
@@ -91,7 +91,7 @@ def walk():
gap('Estimator', 'fp.quote.configurator.create', str(ex))
return
# 4a. Try the "Create Quotation" path what action confirms the SO?
# 4a. Try the "Create Quotation" path - what action confirms the SO?
so = False
for meth in ('action_create_quotation', 'action_promote_to_direct_order',
'action_create_sale_order', 'action_generate_quote'):
@@ -112,7 +112,7 @@ def walk():
# Fall back: create SO directly and see if the configurator workflow is wired.
gap('Estimator', 'configurator',
'No working "create quote" action found on the configurator '
' estimator has no button to make a quote')
'- estimator has no button to make a quote')
# Manual SO creation for the rest of the walkthrough
SO = e['sale.order']
try:
@@ -155,7 +155,7 @@ def walk():
if so.state == 'draft':
try:
so.action_confirm()
print(f' ✓ SO confirmed state={so.state}')
print(f' ✓ SO confirmed - state={so.state}')
except Exception as ex:
gap('Estimator', 'sale.order.action_confirm', str(ex))
return
@@ -167,7 +167,7 @@ def walk():
jobs = Job.search([('sale_order_id', '=', so.id)])
if not jobs:
gap('Planner', 'fp.job auto-create',
'No fp.job auto-created on SO confirm planner has nothing '
'No fp.job auto-created on SO confirm - planner has nothing '
'to plan against')
else:
print(f'{len(jobs)} fp.job(s) created: '
@@ -178,7 +178,7 @@ def walk():
receivings = Recv.search([('sale_order_id', '=', so.id)])
if not receivings:
gap('Receiver', 'fp.receiving auto-create',
'No fp.receiving auto-created on SO confirm receiver has '
'No fp.receiving auto-created on SO confirm - receiver has '
'nothing to count against')
else:
print(f' ✓ Receiving record(s): {", ".join(receivings.mapped("name"))}')
@@ -188,7 +188,7 @@ def walk():
insps = Insp.search([('sale_order_id', '=', so.id)])
if not insps and jobs:
gap('Racker', 'fp.racking.inspection auto-create',
'jobs exist but no racking inspection racker walks empty')
'jobs exist but no racking inspection - racker walks empty')
elif insps:
print(f' ✓ Racking inspection(s): '
f'{", ".join(insps.mapped("name"))}')
@@ -202,10 +202,10 @@ def walk():
f'{", ".join(pjs.mapped("name"))}')
else:
gap('Portal', 'portal job auto-create',
'No portal.job mirror customer sees nothing on portal')
'No portal.job mirror - customer sees nothing on portal')
# ------------------------------------------------------------------
# ROLE: Receiver Plating > Receiving > All Receiving
# ROLE: Receiver - Plating > Receiving > All Receiving
# ------------------------------------------------------------------
print('\n[ROLE: Receiver] Open the receiving record, count boxes')
if receivings:
@@ -218,7 +218,7 @@ def walk():
if hasattr(r, 'action_mark_counted'):
try:
r.action_mark_counted()
print(f' ✓ Marked counted state={r.state}')
print(f' ✓ Marked counted - state={r.state}')
except Exception as ex:
gap('Receiver', 'action_mark_counted', str(ex))
else:
@@ -227,7 +227,7 @@ def walk():
if hasattr(r, 'action_mark_staged'):
try:
r.action_mark_staged()
print(f' ✓ Marked staged state={r.state}')
print(f' ✓ Marked staged - state={r.state}')
except Exception as ex:
gap('Receiver', 'action_mark_staged', str(ex))
# Smart button to racking inspection?
@@ -239,7 +239,7 @@ def walk():
'no smart button; receiver navigates manually')
# ------------------------------------------------------------------
# ROLE: Racking Crew open the linked racking inspection
# ROLE: Racking Crew - open the linked racking inspection
# ------------------------------------------------------------------
print('\n[ROLE: Racker] Open the racking inspection from receiving smart button')
if insps:
@@ -253,18 +253,18 @@ def walk():
if hasattr(insp, 'action_start'):
try:
insp.action_start()
print(f' ✓ Inspection started state={insp.state}')
print(f' ✓ Inspection started - state={insp.state}')
except Exception as ex:
gap('Racker', 'racking_inspection.action_start', str(ex))
if hasattr(insp, 'action_complete'):
try:
insp.action_complete()
print(f' ✓ Inspection completed state={insp.state}')
print(f' ✓ Inspection completed - state={insp.state}')
except Exception as ex:
gap('Racker', 'racking_inspection.action_complete', str(ex))
# ------------------------------------------------------------------
# ROLE: Operator runs the plating job step-by-step
# ROLE: Operator - runs the plating job step-by-step
# ------------------------------------------------------------------
print('\n[ROLE: Operator] Open the job, run each step')
if jobs:
@@ -272,7 +272,7 @@ def walk():
steps = job.step_ids.sorted('sequence')
if not steps:
gap('Operator', 'fp.job.step_ids',
'job has no steps recipe not generated')
'job has no steps - recipe not generated')
else:
print(f' Job {job.name} has {len(steps)} steps')
ran = 0
@@ -286,17 +286,17 @@ def walk():
gap('Operator', f'step.{step.name}', str(ex))
else:
gap('Operator', f'step.{step.name}',
f"state={step.state} operator can't start it")
f"state={step.state} - operator can't start it")
print(f' ✓ Ran {ran} of 3 first steps')
# ------------------------------------------------------------------
# ROLE: Inspector walk the QC checklist if customer requires QC
# ROLE: Inspector - walk the QC checklist if customer requires QC
# ------------------------------------------------------------------
print('\n[ROLE: Inspector] Look for an open QC check on the job')
QC = e['fusion.plating.quality.check']
if jobs:
job = jobs[0]
# Customer might not be flagged x_fc_requires_qc flip it for the test.
# Customer might not be flagged x_fc_requires_qc - flip it for the test.
wants = ('x_fc_requires_qc' in customer._fields
and customer.x_fc_requires_qc)
print(f' Customer requires QC: {wants}')
@@ -309,7 +309,7 @@ def walk():
print(f' ✓ QC check found: {check.name}')
# ------------------------------------------------------------------
# ROLE: Operator try to mark job done (will hit QC gate if applicable)
# ROLE: Operator - try to mark job done (will hit QC gate if applicable)
# ------------------------------------------------------------------
print('\n[ROLE: Operator] Click Mark Done on the job')
if jobs:
@@ -329,7 +329,7 @@ def walk():
pass
try:
job.with_context(fp_skip_qc_gate=True).button_mark_done()
print(f' ✓ Job marked done (with QC bypass) state={job.state}')
print(f' ✓ Job marked done (with QC bypass) - state={job.state}')
except Exception as ex:
gap('Operator', 'fp.job.button_mark_done', str(ex))
@@ -344,7 +344,7 @@ def walk():
f'{", ".join(deliveries.mapped("name") or ["(none)"])}')
# ------------------------------------------------------------------
# ROLE: Driver picks up the delivery
# ROLE: Driver - picks up the delivery
# ------------------------------------------------------------------
print('\n[ROLE: Driver] Find the linked fusion.plating.delivery')
if Del is not None and jobs:
@@ -355,14 +355,14 @@ def walk():
if hasattr(d, 'action_mark_delivered'):
try:
d.action_mark_delivered()
print(f' ✓ Marked delivered state={d.state}')
print(f' ✓ Marked delivered - state={d.state}')
except Exception as ex:
gap('Driver', 'delivery.action_mark_delivered', str(ex))
else:
print(' No delivery linked to job checking by SO')
print(' No delivery linked to job - checking by SO')
# ------------------------------------------------------------------
# ROLE: Accountant invoice the SO
# ROLE: Accountant - invoice the SO
# ------------------------------------------------------------------
print('\n[ROLE: Accountant] Generate invoice')
print(f' invoice_status={so.invoice_status}')
@@ -376,7 +376,7 @@ def walk():
except Exception as ex:
gap('Accountant', 'sale.order._create_invoices', str(ex))
elif so.invoice_status == 'no':
# qty_delivered is 0 service products invoice on ordered qty by
# qty_delivered is 0 - service products invoice on ordered qty by
# default. If "no" persists, the SO has no invoiceable lines yet
# (e.g. delivered_qty=0 + invoice_policy='delivery').
print(f' Note: SO not yet invoiceable (qty_delivered=0). '
@@ -388,7 +388,7 @@ def walk():
# ------------------------------------------------------------------
print('\n=========================== SUMMARY ===========================')
if not GAPS:
print('NO GAPS FOUND workflow walked end-to-end clean')
print('NO GAPS FOUND - workflow walked end-to-end clean')
else:
print(f'{len(GAPS)} GAP(S) FOUND:')
for role, where, msg in GAPS:

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Sub 12 Phase F end-to-end smoke test.
# Sub 12 Phase F - end-to-end smoke test.
#
# Run via odoo-shell:
# /usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin
@@ -16,7 +16,7 @@ def _resolve_test_partner(env):
"""Pick a partner with at least one sale order so the RMA can bind."""
so = env['sale.order'].search([('state', 'in', ('sale', 'done'))], limit=1)
if not so:
raise RuntimeError('No confirmed sale.order found seed one first.')
raise RuntimeError('No confirmed sale.order found - seed one first.')
return so.partner_id, so
@@ -46,12 +46,12 @@ def smoke():
# 2. Authorise
rma.action_authorise()
assert rma.state == 'authorised'
print(f' ✓ Authorised state={rma.state}, qr_code present={bool(rma.qr_code)}')
print(f' ✓ Authorised - state={rma.state}, qr_code present={bool(rma.qr_code)}')
# 3. Mark shipped
rma.action_mark_shipped_to_us()
assert rma.state == 'shipped_to_us'
print(f' ✓ Customer shipped state={rma.state}')
print(f' ✓ Customer shipped - state={rma.state}')
# 4. Auto-receive via fp.receiving create. The RMA-link create-hook
# walks the receiving from draft → counted → staged → closed in one
@@ -80,12 +80,12 @@ def smoke():
rma.resolution_type = 'rework'
rma.action_triage_complete()
assert rma.state == 'triaged'
print(f' ✓ Triage complete state={rma.state}, resolution=rework')
print(f' ✓ Triage complete - state={rma.state}, resolution=rework')
# 7. Start resolving
rma.action_start_resolving()
assert rma.state == 'resolving'
print(f' ✓ Resolving state={rma.state}')
print(f' ✓ Resolving - state={rma.state}')
# 8. NCR walk: open → containment → root cause → close
ncr.action_open()
@@ -95,7 +95,7 @@ def smoke():
ncr.disposition = 'rework'
ncr.root_cause = '<p>Smoke-test: rack contact loss during transit.</p>'
# 9. Spawn CAPA from NCR (uses severity gate high passes)
# 9. Spawn CAPA from NCR (uses severity gate - high passes)
spawn_action = ncr.action_spawn_capa()
capa = e['fusion.plating.capa'].browse(spawn_action.get('res_id'))
print(f' ✓ Spawned CAPA {capa.name} from NCR')
@@ -109,35 +109,35 @@ def smoke():
capa.action_start_verification()
capa.effectiveness_notes = '<p>Smoke-test: 30 days no recurrence.</p>'
capa.action_mark_effective()
print(f' ✓ CAPA marked effective state={capa.state}')
print(f' ✓ CAPA marked effective - state={capa.state}')
assert capa.state == 'effective'
# 11. Close NCR
ncr.action_close()
assert ncr.state == 'closed'
print(f' ✓ NCR closed state={ncr.state}')
print(f' ✓ NCR closed - state={ncr.state}')
# 11b. Release the auto-spawned Hold (rework path) so the RMA close
# gate doesn't block. action_close on RMA refuses if any Hold is
# still on_hold or under_review.
hold.action_send_to_rework()
print(f' ✓ Hold sent to rework state={hold.state}')
print(f' ✓ Hold sent to rework - state={hold.state}')
# 12. Resolve RMA (will spawn replacement job for rework)
rma.action_resolve()
print(f' ✓ RMA resolved state={rma.state}, replacement_job={rma.replacement_job_id.name if rma.replacement_job_id else None}')
print(f' ✓ RMA resolved - state={rma.state}, replacement_job={rma.replacement_job_id.name if rma.replacement_job_id else None}')
assert rma.state == 'resolved'
# 13. Close RMA
rma.action_close()
assert rma.state == 'closed'
print(f' ✓ RMA closed state={rma.state}')
print(f' ✓ RMA closed - state={rma.state}')
# 14. Stage_id sync sanity
print(f' ✓ NCR stage_id={ncr.stage_id.name if ncr.stage_id else "(none)"}')
print(f' ✓ RMA stage_id={rma.stage_id.name if rma.stage_id else "(none)"}')
# 15. Counts smoke (read directly controller needs http context).
# 15. Counts smoke (read directly - controller needs http context).
open_holds = e['fusion.plating.quality.hold'].search_count([
('state', 'in', ('on_hold', 'under_review')),
])

View File

@@ -21,7 +21,7 @@
<!-- Multi-company rule for Contract Reviews (Sub 4). -->
<record id="fp_contract_review_company_rule" model="ir.rule">
<field name="name">Fusion Plating: Contract Review multi-company</field>
<field name="name">Fusion Plating: Contract Review - multi-company</field>
<field name="model_id" ref="model_fp_contract_review"/>
<field name="global" eval="True"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>

View File

@@ -17,7 +17,7 @@
* Frontend side (this file): a service subscribes to that bus type
* on session start. When a payload arrives, dispatch an ir.actions.
* act_window opening the review's form so the user lands on the QA-005
* automatically. They can still close it and come back later the
* automatically. They can still close it and come back later - the
* WO-step gate (rule 5) is the backstop.
*/

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,6 +1,6 @@
/** @odoo-module **/
// Quality Dashboard action surface.
// Quality Dashboard - action surface.
// Spec: docs/superpowers/specs/2026-05-25-quality-dashboard-redesign-design.md
//
// Single OWL component that fetches one snapshot from
@@ -10,7 +10,7 @@
// - SectionCard × 6 in canonical order (cert, hold, ncr, rma, capa, check)
//
// BannerCard / BannerItem / SectionCard / SectionRow live in this same
// file as sibling sub-components not reused elsewhere yet.
// file as sibling sub-components - not reused elsewhere yet.
import { Component, useState, onWillStart, onMounted, onWillUnmount }
from "@odoo/owl";
@@ -96,7 +96,7 @@ export class FpQualityDashboard extends Component {
this.state.error = null;
} catch (e) {
console.warn("FpQualityDashboard: snapshot RPC failed", e);
this.state.error = "Couldn't refresh dashboard retry in 60s";
this.state.error = "Couldn't refresh dashboard - retry in 60s";
} finally {
this.state.loading = false;
}
@@ -104,7 +104,7 @@ export class FpQualityDashboard extends Component {
onOpenItem(item) {
// Build a form-view act_window from the item's open_action payload.
// ACL is enforced by Odoo on click if the user lacks access,
// ACL is enforced by Odoo on click - if the user lacks access,
// they get the standard access error (D15).
this.action.doAction({
type: "ir.actions.act_window",
@@ -117,7 +117,7 @@ export class FpQualityDashboard extends Component {
}
onOpenKanban(section) {
// Pass the xmlid string directly Odoo 19's action service
// Pass the xmlid string directly - Odoo 19's action service
// resolves it via the registry. Fallback to shipping the full
// act_window dict from the snapshot if this stops working.
this.action.doAction(section.open_kanban_xmlid);

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

@@ -1,11 +1,11 @@
// Quality Dashboard action surface.
// Quality Dashboard - action surface.
// Spec: docs/superpowers/specs/2026-05-25-quality-dashboard-redesign-design.md
//
// Tokens defined locally; light + dark via $o-webclient-color-scheme
// compile-time branch (project Rule 9 no runtime .o_dark_mode class).
// compile-time branch (project Rule 9 - no runtime .o_dark_mode class).
// Reuses base $plant-card-bg / $plant-bg / $plant-text / $plant-muted /
// $plant-card-border from _plant_tokens.scss (loaded earlier in the
// fusion_plating_shopfloor manifest fusion_plating_quality depends
// fusion_plating_shopfloor manifest - fusion_plating_quality depends
// on shopfloor so those tokens are visible).
$o-webclient-color-scheme: bright !default;

View File

@@ -1,5 +1,5 @@
// =============================================================================
// Fusion Plating Quality (QMS) backend styles
// Fusion Plating - Quality (QMS) backend styles
// Copyright 2026 Nexa Systems Inc.
// License OPL-1 (Odoo Proprietary License v1.0)
//
@@ -23,14 +23,14 @@
//
// Semantic status tints use `color-mix()` against the Bootstrap theme tokens
// so a danger badge is darker on light mode and brighter on dark mode
// automatically one rule, two looks.
// automatically - one rule, two looks.
//
// We never target `.o_dark`, `html.dark`, or `@media (prefers-color-scheme)`.
// =============================================================================
// -----------------------------------------------------------------------------
// Local helper semantic tint mixin
// Local helper - semantic tint mixin
// -----------------------------------------------------------------------------
@mixin fp-quality-tint($color-var, $amount: 12%) {
background-color: color-mix(in srgb, var(#{$color-var}) #{$amount}, transparent);
@@ -40,7 +40,7 @@
// -----------------------------------------------------------------------------
// Universal overdue indicator used across CAPAs, calibration, and AVL
// Universal overdue indicator - used across CAPAs, calibration, and AVL
// -----------------------------------------------------------------------------
.o_fp_overdue {
display: inline-block;
@@ -55,7 +55,7 @@
// -----------------------------------------------------------------------------
// NCR kanban severity tint on the left border
// NCR kanban - severity tint on the left border
// -----------------------------------------------------------------------------
.o_fp_ncr_kanban {
@@ -94,7 +94,7 @@
// -----------------------------------------------------------------------------
// CAPA kanban overdue tint, type pill
// CAPA kanban - overdue tint, type pill
// -----------------------------------------------------------------------------
.o_fp_capa_kanban {
@@ -127,7 +127,7 @@
// -----------------------------------------------------------------------------
// FAIR card result tint
// FAIR card - result tint
// -----------------------------------------------------------------------------
.o_fp_fair_card {
border-left-width: 4px;

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

@@ -27,7 +27,7 @@
class="o_fp_qd_banner o_fp_qd_banner_clear">
<div class="o_fp_qd_banner_clear_icon"></div>
<div class="o_fp_qd_banner_clear_text">
<strong>All caught up</strong> no critical items right now
<strong>All caught up</strong> - no critical items right now
</div>
</div>
<div t-else="" class="o_fp_qd_banner o_fp_qd_banner_urgent">
@@ -37,7 +37,7 @@
<span t-if="props.banner.total_matching > props.banner.items.length"
class="o_fp_qd_banner_overflow">
(showing <t t-esc="props.banner.items.length"/>
of <t t-esc="props.banner.total_matching"/>
of <t t-esc="props.banner.total_matching"/> -
see sections below for the rest)
</span>
</div>

View File

@@ -24,7 +24,7 @@ class TestDashboardSnapshotShape(TransactionCase):
def test_section_order_is_canonical(self):
snap = self._build()
types_present = [s['type'] for s in snap['sections']]
# Canonical order cert, hold, ncr, rma, capa, check.
# Canonical order - cert, hold, ncr, rma, capa, check.
# Some types may be absent if their model isn't installed; the
# PRESENT ones must appear in this relative order.
canonical = ['cert', 'hold', 'ncr', 'rma', 'capa', 'check']
@@ -55,7 +55,7 @@ class TestDashboardSnapshotShape(TransactionCase):
class TestDashboardSnapshotItems(TransactionCase):
"""Per-section items list ranking + cap + shape."""
"""Per-section items list - ranking + cap + shape."""
def _build(self):
from odoo.addons.fusion_plating_quality.controllers.fp_quality_dashboard \
@@ -167,7 +167,7 @@ class TestDashboardSnapshotBanner(TransactionCase):
def test_banner_all_clear_when_zero(self):
snap = self._build()
# Empty DB no overdue, no critical
# Empty DB - no overdue, no critical
self.assertTrue(snap['banner']['all_clear'])
self.assertEqual(snap['banner']['items'], [])
@@ -182,7 +182,7 @@ class TestDashboardSnapshotDefensive(TransactionCase):
def test_missing_partner_field_falls_through(self):
# Empty DB + helper must not raise even when x_fc_rush absent.
# (In this codebase x_fc_rush isn't a registered field only
# (In this codebase x_fc_rush isn't a registered field - only
# read defensively via the `in partner._fields` check. The
# snapshot must build cleanly.)
snap = self._build()

View File

@@ -24,14 +24,14 @@ class TestContractReviewEnforcementOnCreate(TransactionCase):
def setUp(self):
super().setUp()
# Customer A enforcement ON
# Customer A - enforcement ON
self.cust_enforced = self.env['res.partner'].create({
'name': 'Enforced Customer',
'is_company': True,
'customer_rank': 1,
'x_fc_contract_review_required': True,
})
# Customer B enforcement OFF (control)
# Customer B - enforcement OFF (control)
self.cust_unenforced = self.env['res.partner'].create({
'name': 'Unenforced Customer',
'is_company': True,
@@ -73,7 +73,7 @@ class TestContractReviewEnforcementOnCreate(TransactionCase):
)
def test_existing_review_is_not_replaced(self):
# Pre-create a review and pass it in vals create() must not
# Pre-create a review and pass it in vals - create() must not
# overwrite it (idempotency on copy/import flows).
existing_part = self.env['fp.part.catalog'].create({
'partner_id': self.cust_enforced.id,
@@ -97,7 +97,7 @@ class TestContractReviewEnforcementOnCreate(TransactionCase):
)
def test_batch_create_each_part_gets_own_review(self):
# Batch create each enforced part gets its own review.
# Batch create - each enforced part gets its own review.
parts = self.env['fp.part.catalog'].create([
{'partner_id': self.cust_enforced.id,
'part_number': 'BATCH-001', 'revision': 'A'},
@@ -169,7 +169,7 @@ class TestContractReviewEnforcementOnCreate(TransactionCase):
mock_send.called,
'bus.bus._sendone must be called on enforced create.',
)
# Inspect the call payload type=warning + sticky=True.
# Inspect the call payload - type=warning + sticky=True.
# Args: (self, target, type, payload)
call_args = mock_send.call_args
payload = call_args.args[3] if len(call_args.args) >= 4 else call_args.kwargs.get('notification')
@@ -194,7 +194,7 @@ class TestContractReviewEnforcementOnCreate(TransactionCase):
# -- Write must NOT re-trigger ----------------------------------
def test_write_does_not_retrigger_alert(self):
# Pre-existing part under unenforced customer no review yet.
# Pre-existing part under unenforced customer - no review yet.
part = self.env['fp.part.catalog'].create({
'partner_id': self.cust_unenforced.id,
'part_number': 'WRITE-001',
@@ -203,7 +203,7 @@ class TestContractReviewEnforcementOnCreate(TransactionCase):
self.assertFalse(part.x_fc_contract_review_id)
# Now flip the customer's flag and update the part. The create
# gate is .create()-only by design write/update must NOT
# gate is .create()-only by design - write/update must NOT
# auto-create a review or push a notification.
self.cust_unenforced.x_fc_contract_review_required = True
with patch.object(
@@ -212,7 +212,7 @@ class TestContractReviewEnforcementOnCreate(TransactionCase):
part.write({'revision_note': 'updated after enforcement enabled'})
self.assertFalse(
mock_send.called,
'write() must NOT push a contract-review notification '
'write() must NOT push a contract-review notification - '
'enforcement only applies on first creation.',
)
self.assertFalse(

View File

@@ -35,7 +35,7 @@
<field name="arch" type="xml">
<form string="Audit">
<header>
<!-- Phase D5 Audit is QM-only per spec section 2.C
<!-- Phase D5 - Audit is QM-only per spec section 2.C
(Manager has read; QM owns CRUD + close). -->
<button name="action_start" string="Start Audit" type="object"
class="oe_highlight" invisible="state != 'planned'"

View File

@@ -38,7 +38,7 @@
<field name="arch" type="xml">
<form string="Approved Vendor">
<header>
<!-- Phase D5 AVL state transitions are QM-only per spec
<!-- Phase D5 - AVL state transitions are QM-only per spec
section 2.C (Manager has read; QM owns Add/Approve/
Disqualify). Spec lists "Approve / Disqualify"; this
model uses Approve + Suspend + Reinstate + Remove,

View File

@@ -107,7 +107,7 @@
<t t-name="card">
<div class="o_fp_card" t-att-data-state="record.state.raw_value">
<strong class="o_fp_card_title">
<field name="code"/> <field name="name"/>
<field name="code"/> - <field name="name"/>
</strong>
<div class="small text-muted"><field name="equipment_type"/></div>
<div class="small mt-2">

View File

@@ -35,7 +35,7 @@
<field name="arch" type="xml">
<form string="CAPA">
<header>
<!-- Phase D5 every state-transition button is QM-only.
<!-- Phase D5 - every state-transition button is QM-only.
Per spec section 2.C, Manager has read+comment only;
QM owns CRUD + close + effectiveness verification. -->
<button name="action_start_analysis" string="Start Analysis" type="object"
@@ -67,7 +67,7 @@
<label for="name"/>
<h1><field name="name" readonly="1"/></h1>
</div>
<!-- Phase D5 editable fields readonly for non-QM. Per
<!-- Phase D5 - editable fields readonly for non-QM. Per
spec section 2.C, Manager retains read+comment only;
QM owns CRUD. Form stays visible (Manager needs to
read + chatter); only the inputs lock for non-QM. -->

View File

@@ -3,12 +3,12 @@
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Sub 4 Contract Review (QA-005 1:1 reproduction).
Sub 4 - Contract Review (QA-005 1:1 reproduction).
-->
<odoo>
<!-- ================================================================== -->
<!-- FORM VIEW laid out to mirror QA-005 Rev. 0 as closely as Odoo -->
<!-- FORM VIEW - laid out to mirror QA-005 Rev. 0 as closely as Odoo -->
<!-- backend forms permit. Printed PDF is the 1:1 artefact; this form -->
<!-- is the data-entry counterpart. -->
<!-- ================================================================== -->
@@ -63,7 +63,7 @@
</group>
<notebook>
<!-- ========= SECTION 2.0 Planning / Production Review ========= -->
<!-- ========= SECTION 2.0 - Planning / Production Review ========= -->
<page string="2.0 Planning / Production Review"
name="section_20">
<group>
@@ -118,7 +118,7 @@
</group>
</page>
<!-- ========= SECTION 3.0 Quality Review ========= -->
<!-- ========= SECTION 3.0 - Quality Review ========= -->
<page string="3.0 Quality Review" name="section_30">
<group>
<field name="s30_locked" invisible="1"/>

View File

@@ -32,10 +32,10 @@
<form string="Customer Specification">
<sheet>
<div class="oe_title">
<!-- Phase D5 Customer Spec is QM-only for edits per
<!-- Phase D5 - Customer Spec is QM-only for edits per
spec section 2.C (Manager has read + attach to
parts; QM owns CRUD as library curator). Form
stays visible only inputs lock for non-QM. -->
stays visible - only inputs lock for non-QM. -->
<label for="name"/>
<h1><field name="name"
/></h1>
@@ -116,7 +116,7 @@
</p>
<p>
The Specifications library holds every standard your
customers cite on POs industry standards (AMS 2404,
customers cite on POs - industry standards (AMS 2404,
ASTM B733, MIL-C-26074), prime-specific codes (Boeing
BAC 5680, Lockheed LMS-3045), and your own internal
references.
@@ -130,7 +130,7 @@
</p>
<p>
Add a new Specification the moment a new code shows up
on a customer PO there's no need to wait for a
on a customer PO - there's no need to wait for a
manager. Set the document URL so the controlled copy
is one click away during audits.
</p>

View File

@@ -38,7 +38,7 @@
<field name="arch" type="xml">
<form string="First Article Inspection Report">
<header>
<!-- Phase D5 FAIR Approve/Reject = sign-off equivalent;
<!-- Phase D5 - FAIR Approve/Reject = sign-off equivalent;
QM-only per spec section 2.C (FAIR/Nadcap signing is
restricted to Quality Manager regardless of who can
see / create the FAIR record). Submit-for-review and

View File

@@ -56,7 +56,7 @@
action="action_fp_doc_control"
sequence="50"/>
<!-- Phase 2 calibration into Materials & Tanks; specs+AVL into Quality & Documents. -->
<!-- Phase 2 - calibration into Materials & Tanks; specs+AVL into Quality & Documents. -->
<menuitem id="menu_fp_config_cal_equipment"
name="Calibration Equipment"
parent="fusion_plating.menu_fp_config_materials_tanks"
@@ -69,7 +69,7 @@
action="action_fp_cal_event"
sequence="60"/>
<!-- Promote-Customer-Spec (Phase F) Specifications is now central
<!-- Promote-Customer-Spec (Phase F) - Specifications is now central
to order entry (estimators add new BAC / AMS / customer codes
when they hit them). Moved out of Configuration (manager-only)
into Quality where the workflow lives. Single menu, accessible

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 4 Contract Review banner + tab on fp.part.catalog.
Sub 4 - Contract Review banner + tab on fp.part.catalog.
-->
<odoo>
@@ -13,7 +13,7 @@
<field name="inherit_id" ref="fusion_plating_configurator.view_fp_part_catalog_form"/>
<field name="arch" type="xml">
<!-- ===== Smart button Contract Review ===== -->
<!-- ===== Smart button - Contract Review ===== -->
<xpath expr="//div[@name='button_box']" position="inside">
<button name="action_start_contract_review"
type="object"
@@ -22,7 +22,7 @@
<div class="o_stat_info">
<span class="o_stat_value"
invisible="x_fc_contract_review_id">
-
</span>
<field name="x_fc_contract_review_state"
widget="badge"
@@ -48,7 +48,7 @@
<i class="fa fa-info-circle me-2"/>
<strong>New part created.</strong>
Please complete the Contract Review (QA-005) if applicable.
This is optional it can be dismissed and never blocks production.
This is optional - it can be dismissed and never blocks production.
<div class="mt-2">
<button name="action_start_contract_review"
type="object"

View File

@@ -4,7 +4,7 @@
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Sub 12 Phase B back-office views for the four categorisation models
Sub 12 Phase B - back-office views for the four categorisation models
(tag / reason / team / stage). All sit under Configuration → Quality.
-->
<odoo>
@@ -134,12 +134,12 @@
<field name="help" type="html">
<p>Shared kanban-stage namespace for NCR + RMA. Codes are
referenced by the state ↔ stage_id sync in
fp_quality_categorisation_links.py don't rename codes
fp_quality_categorisation_links.py - don't rename codes
without checking that file.</p>
</field>
</record>
<!-- Phase 2 categorisation goes into Quality & Documents;
<!-- Phase 2 - categorisation goes into Quality & Documents;
Quality Teams goes into Workforce. -->
<menuitem id="menu_fp_config_quality_tag"
name="Quality Tags"

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

@@ -4,7 +4,7 @@
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Sub 12 Phase D client action + menu for the Unified Quality Dashboard.
Sub 12 Phase D - client action + menu for the Unified Quality Dashboard.
-->
<odoo>

View File

@@ -117,7 +117,7 @@
<t t-name="card">
<field name="name" class="fw-bold"/>
<div>
<field name="part_ref"/>
<field name="part_ref"/> -
<field name="qty_on_hold"/> pcs
</div>
<div>

View File

@@ -4,7 +4,7 @@
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Sub 12 Phase C back-office views for fp.quality.point.
Sub 12 Phase C - back-office views for fp.quality.point.
-->
<odoo>

View File

@@ -4,7 +4,7 @@
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Sub 12 Phase D surface the new smart-button counts on:
Sub 12 Phase D - surface the new smart-button counts on:
- fp.job form
- sale.order form
- res.partner form
@@ -15,7 +15,7 @@
<odoo>
<!-- fp.job smart-button row lives in fusion_plating_jobs because the
button_box is added by that module (see Phase D notes quality
button_box is added by that module (see Phase D notes - quality
can't depend on jobs without creating a cycle). -->
<!-- =============================================== sale.order ===== -->
@@ -73,7 +73,7 @@
</field>
</record>
<!-- ====================================== NCR Spawn CAPA ===== -->
<!-- ====================================== NCR - Spawn CAPA ===== -->
<record id="view_fp_ncr_form_spawn_capa" model="ir.ui.view">
<field name="name">fp.ncr.form.spawn.capa</field>
<field name="model">fusion.plating.ncr</field>
@@ -89,7 +89,7 @@
</field>
</record>
<!-- ============================ CAPA Verify Effectiveness ===== -->
<!-- ============================ CAPA - Verify Effectiveness ===== -->
<record id="view_fp_capa_form_verify" model="ir.ui.view">
<field name="name">fp.capa.form.verify</field>
<field name="model">fusion.plating.capa</field>

Some files were not shown because too many files have changed in this diff Show More