chore(plating): de-dash shipped code + intake-neutral customer emails
Replace em-dashes and en-dashes with hyphens across 789 shipped source files (py/xml/js/scss) so the delivered module reads as human-written; em-dashes had become a recognizable AI-generated tell. Internal .md dev notes are excluded. The WO-sticker mojibake strippers keep their dash search targets (now written — / –). No logic changes: comments and display strings only; validated with py_compile + lxml parse. Rewrite the 7 customer notification emails to be intake-neutral (ship-in / drop-off / pickup) and repair-aware, and fix the Shipped email documents line (packing slip vs bill of lading; certificate only when issued). Subjects use a hyphen separator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,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',
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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] \
|
||||
|
||||
@@ -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 6–12 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
-->
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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.'
|
||||
|
||||
@@ -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 '
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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 '
|
||||
|
||||
@@ -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 '
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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.0005–0.0010".',
|
||||
'blemishes on surface", "Thickness within 0.0005-0.0010".',
|
||||
)
|
||||
description = fields.Text(
|
||||
string='Inspection Guidance',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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']
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}')
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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.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>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 = {
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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]}')
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: '
|
||||
|
||||
@@ -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}')
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}')
|
||||
|
||||
|
||||
@@ -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}')
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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')),
|
||||
])
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — Mobile QC Checklist (OWL backend client action)
|
||||
// Fusion Plating - Mobile QC Checklist (OWL backend client action)
|
||||
// Copyright 2026 Nexa Systems Inc.
|
||||
// License OPL-1 (Odoo Proprietary License v1.0)
|
||||
//
|
||||
@@ -120,7 +120,7 @@ export class FpQcChecklist extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Value input — debounced write on blur. Pending result stays until
|
||||
// Value input - debounced write on blur. Pending result stays until
|
||||
// operator taps pass/fail.
|
||||
onValueInput(line, ev) {
|
||||
const v = parseFloat(ev.target.value);
|
||||
@@ -215,7 +215,7 @@ export class FpQcChecklist extends Component {
|
||||
return;
|
||||
}
|
||||
this.notification.add(
|
||||
`Uploaded — ${json.reading_count || 0} reading(s) extracted`,
|
||||
`Uploaded - ${json.reading_count || 0} reading(s) extracted`,
|
||||
{ type: "success" },
|
||||
);
|
||||
await this.refresh();
|
||||
|
||||
@@ -1,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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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')"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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. -->
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user