feat(step-library): full plating workflow coverage + per-recipe configurability + audit
Implements 2026-04-29-step-library-audit-design.md. Bumps fusion_plating to 19.0.18.7.0, fusion_plating_jobs to 19.0.8.12.0, fusion_plating_reports to 19.0.10.2.0. LIBRARY EXPANSION - 8 new Step Kinds: Receiving, Electroclean, Strike, Salt Spray, Adhesion Test, Hardness Test, Packaging, Tank Replenishment - 4 new input types: photo, multi_point_thickness, bath_chemistry_panel, ph - DEFAULT_INPUTS_BY_KIND rewritten to seed audit-grade prompts on every kind (bath IDs, photos, multi-point thickness, signatures, etc.) - + Common Audit Fields one-click button on the library template form - Default Operator Instructions relabel + alert callout PER-RECIPE CONFIGURABILITY - collect (Boolean) per recipe-step input prompt — opt out without delete - collect_measurements (Boolean) master switch on recipe step — when off, wizard skips entirely - template_input_id (Many2one) traceability link from recipe to library - Recipe-step backend form view exposes the new fields with handle drag, toggle, target range, and library-source column RUNTIME WIRING - Step input wizard filters node.input_ids to step_input AND collect=True; short-circuits on collect_measurements=False - New input types: photo (image widget + ir.attachment), multi-point thickness (5 readings + auto avg, skips empty cells), bath chemistry panel (pH/conc/temp/bath bundle), pH (0-14 numeric) - Composite values JSON-serialized into value_text; photo via attachment CoC REPORT - Filters captured prompts to collect=True only - Renders new input types with appropriate format MIGRATION (post-migrate.py for 19.0.18.7.0) - Backfills collect=True on recipe-step inputs - Backfills collect_measurements=True on recipe steps - Re-runs action_seed_default_inputs on every existing template (idempotent, preserves user edits) - Backfills template_input_id by name-matching against source library template (handles JSONB vs varchar name columns) SEED DATA - 8 example templates (one per new kind) in fp_step_template_data.xml with noupdate=1 BATTLE TEST - bt_step_library_audit.py: 29 assertions all PASS on entech OWL EDITOR EXTENSION DEFERRED - The simple recipe editor's per-step Instructions/Measurements expansions were not implemented in this pass; users configure via the backend recipe-step form. Track follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating',
|
||||
'version': '19.0.12.6.2',
|
||||
'version': '19.0.18.7.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
||||
'description': """
|
||||
@@ -119,6 +119,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
||||
'data/fp_recipe_general_processing.xml',
|
||||
'data/fp_recipe_anodize.xml',
|
||||
'data/fp_recipe_chem_conversion.xml',
|
||||
'data/fp_step_template_data.xml',
|
||||
],
|
||||
'post_init_hook': 'post_init_hook',
|
||||
'assets': {
|
||||
|
||||
106
fusion_plating/fusion_plating/data/fp_step_template_data.xml
Normal file
106
fusion_plating/fusion_plating/data/fp_step_template_data.xml
Normal file
@@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
|
||||
Seed templates for the 8 new Step Kinds added in 19.0.18.7.0.
|
||||
noupdate="1" so users can rename / archive without a module
|
||||
upgrade reverting their edits.
|
||||
-->
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="fp_step_template_receiving_std" model="fp.step.template">
|
||||
<field name="name">Incoming Inspection (Standard)</field>
|
||||
<field name="code">RECV_STD</field>
|
||||
<field name="default_kind">receiving</field>
|
||||
<field name="icon">fa-inbox</field>
|
||||
<field name="description"><![CDATA[
|
||||
<p>Verify quantity received against packing slip. Visually inspect
|
||||
for damage, corrosion, oil residue. Photo any damage. Record
|
||||
inspector initials.</p>
|
||||
]]></field>
|
||||
</record>
|
||||
|
||||
<record id="fp_step_template_electroclean_std" model="fp.step.template">
|
||||
<field name="name">Electroclean (Standard)</field>
|
||||
<field name="code">ELEC_CLEAN_STD</field>
|
||||
<field name="default_kind">electroclean</field>
|
||||
<field name="icon">fa-bolt</field>
|
||||
<field name="description"><![CDATA[
|
||||
<p>Submerge rack and energise. Record actual amperage, voltage,
|
||||
and current density. Verify polarity per recipe spec.</p>
|
||||
]]></field>
|
||||
</record>
|
||||
|
||||
<record id="fp_step_template_strike_std" model="fp.step.template">
|
||||
<field name="name">Wood's Nickel Strike (Standard)</field>
|
||||
<field name="code">STRIKE_STD</field>
|
||||
<field name="default_kind">strike</field>
|
||||
<field name="icon">fa-flash</field>
|
||||
<field name="description"><![CDATA[
|
||||
<p>Apply thin nickel strike to ensure adhesion before main plate.
|
||||
Record bath ID, time, temperature, electrical readings.</p>
|
||||
]]></field>
|
||||
</record>
|
||||
|
||||
<record id="fp_step_template_salt_spray_std" model="fp.step.template">
|
||||
<field name="name">Salt Spray Test (ASTM B117)</field>
|
||||
<field name="code">SALT_SPRAY_STD</field>
|
||||
<field name="default_kind">salt_spray</field>
|
||||
<field name="icon">fa-tint</field>
|
||||
<field name="description"><![CDATA[
|
||||
<p>Submit test panel to salt spray cabinet for the specified
|
||||
duration. Record red rust % and white corrosion %. Attach lab
|
||||
report on completion.</p>
|
||||
]]></field>
|
||||
</record>
|
||||
|
||||
<record id="fp_step_template_adhesion_std" model="fp.step.template">
|
||||
<field name="name">Adhesion Test (Bend / Tape)</field>
|
||||
<field name="code">ADHESION_STD</field>
|
||||
<field name="default_kind">adhesion_test</field>
|
||||
<field name="icon">fa-link</field>
|
||||
<field name="description"><![CDATA[
|
||||
<p>Perform adhesion test per spec (bend, tape, burnish, or file).
|
||||
Photo coupon. Record PASS/FAIL.</p>
|
||||
]]></field>
|
||||
</record>
|
||||
|
||||
<record id="fp_step_template_hardness_std" model="fp.step.template">
|
||||
<field name="name">Microhardness Test</field>
|
||||
<field name="code">HARDNESS_STD</field>
|
||||
<field name="default_kind">hardness_test</field>
|
||||
<field name="icon">fa-cube</field>
|
||||
<field name="description"><![CDATA[
|
||||
<p>Take three indentations minimum on the test coupon. Record
|
||||
test load, individual readings, and the computed average.
|
||||
Confirm equipment calibration is current.</p>
|
||||
]]></field>
|
||||
</record>
|
||||
|
||||
<record id="fp_step_template_packaging_std" model="fp.step.template">
|
||||
<field name="name">Packaging (Standard)</field>
|
||||
<field name="code">PKG_STD</field>
|
||||
<field name="default_kind">packaging</field>
|
||||
<field name="icon">fa-archive</field>
|
||||
<field name="description"><![CDATA[
|
||||
<p>Wrap parts per customer spec (VCI bag, bubble wrap, separator
|
||||
paper). Verify cert package included if required. Record quantity
|
||||
per package and total package count.</p>
|
||||
]]></field>
|
||||
</record>
|
||||
|
||||
<record id="fp_step_template_replenishment_std" model="fp.step.template">
|
||||
<field name="name">Tank Replenishment</field>
|
||||
<field name="code">REPL_STD</field>
|
||||
<field name="default_kind">replenishment</field>
|
||||
<field name="icon">fa-flask</field>
|
||||
<field name="description"><![CDATA[
|
||||
<p>Mid-shift bath top-up. Record bath ID, chemistry added (name
|
||||
and amount), pH and concentration before/after. Operator must
|
||||
sign.</p>
|
||||
]]></field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
"""Post-migration for 19.0.18.7.0 — Step Library audit expansion.
|
||||
|
||||
1. Default `collect=True` on all existing recipe-step inputs.
|
||||
2. Default `collect_measurements=True` on all existing recipe steps.
|
||||
3. Re-run action_seed_default_inputs on every existing template to
|
||||
pull in the newly-added prompts (idempotent — skips rows whose
|
||||
name is already present, so user edits survive).
|
||||
4. Backfill template_input_id by name-matching against the linked
|
||||
library template (best-effort).
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
if not version:
|
||||
return
|
||||
from odoo.api import Environment, SUPERUSER_ID
|
||||
env = Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
# 1. Default collect=True on all recipe-step inputs that have NULL
|
||||
cr.execute("""
|
||||
UPDATE fusion_plating_process_node_input
|
||||
SET collect = TRUE
|
||||
WHERE collect IS NULL
|
||||
""")
|
||||
_logger.info(
|
||||
"Backfilled collect=True on %s recipe-step inputs", cr.rowcount
|
||||
)
|
||||
|
||||
# 2. Default collect_measurements=True on recipe steps with NULL
|
||||
cr.execute("""
|
||||
UPDATE fusion_plating_process_node
|
||||
SET collect_measurements = TRUE
|
||||
WHERE collect_measurements IS NULL
|
||||
""")
|
||||
_logger.info(
|
||||
"Backfilled collect_measurements=True on %s recipe steps", cr.rowcount
|
||||
)
|
||||
|
||||
# 3. Re-seed defaults on every existing template (idempotent)
|
||||
Template = env['fp.step.template']
|
||||
templates = Template.search([('default_kind', '!=', False)])
|
||||
for tpl in templates:
|
||||
try:
|
||||
tpl.action_seed_default_inputs()
|
||||
except Exception as e:
|
||||
_logger.warning(
|
||||
"Failed to re-seed defaults on template %s: %s", tpl.id, e
|
||||
)
|
||||
_logger.info("Re-seeded defaults on %s templates", len(templates))
|
||||
|
||||
# 4. Backfill template_input_id — name-match recipe-node inputs against
|
||||
# their parent recipe's source library template.
|
||||
# Note: fusion_plating_process_node_input.name is plain varchar;
|
||||
# fp_step_template_input.name is translatable JSONB (use ->>'en_US').
|
||||
cr.execute("""
|
||||
SELECT ni.id, ni.name, n.source_template_id
|
||||
FROM fusion_plating_process_node_input ni
|
||||
JOIN fusion_plating_process_node n ON n.id = ni.node_id
|
||||
WHERE ni.template_input_id IS NULL
|
||||
AND n.source_template_id IS NOT NULL
|
||||
""")
|
||||
rows = cr.fetchall()
|
||||
matched = 0
|
||||
for ni_id, name, tpl_id in rows:
|
||||
if not name:
|
||||
continue
|
||||
cr.execute("""
|
||||
SELECT id FROM fp_step_template_input
|
||||
WHERE template_id = %s
|
||||
AND name->>'en_US' = %s
|
||||
LIMIT 1
|
||||
""", (tpl_id, name))
|
||||
match = cr.fetchone()
|
||||
if match:
|
||||
cr.execute("""
|
||||
UPDATE fusion_plating_process_node_input
|
||||
SET template_input_id = %s WHERE id = %s
|
||||
""", (match[0], ni_id))
|
||||
matched += 1
|
||||
_logger.info(
|
||||
"Backfilled template_input_id on %s recipe-step inputs", matched
|
||||
)
|
||||
@@ -103,6 +103,15 @@ class FpProcessNode(models.Model):
|
||||
string='Description',
|
||||
help='Rich text instructions for this step.',
|
||||
)
|
||||
# Sub 12d — master switch for runtime data collection. When False the
|
||||
# operator wizard skips this step entirely (no input prompts shown).
|
||||
collect_measurements = fields.Boolean(
|
||||
string='Collect Measurements at Runtime',
|
||||
default=True,
|
||||
help='Master switch. When off, the operator wizard skips this step '
|
||||
'entirely (no input prompts shown). Use for housekeeping steps '
|
||||
'or when no measurement is needed for this recipe.',
|
||||
)
|
||||
notes = fields.Text(
|
||||
string='Internal Notes',
|
||||
help='Internal notes (not shown to customers).',
|
||||
@@ -633,6 +642,10 @@ class FpProcessNodeInput(models.Model):
|
||||
('signature', 'Signature'),
|
||||
('location_picker', 'Location Picker'),
|
||||
('customer_wo', 'Customer WO #'),
|
||||
('photo', 'Photo'),
|
||||
('multi_point_thickness', 'Multi-Point Thickness (avg)'),
|
||||
('bath_chemistry_panel', 'Bath Chemistry Panel'),
|
||||
('ph', 'pH'),
|
||||
],
|
||||
string='Input Type',
|
||||
required=True,
|
||||
@@ -695,3 +708,21 @@ class FpProcessNodeInput(models.Model):
|
||||
],
|
||||
string='Compliance Tag', default='none',
|
||||
)
|
||||
|
||||
# ===== Sub 12d — per-recipe configurability =============================
|
||||
collect = fields.Boolean(
|
||||
string='Collect This Measurement',
|
||||
default=True,
|
||||
help='Toggle off to skip this prompt at runtime without deleting '
|
||||
'it. Recipe authors use this to opt out of library-seeded '
|
||||
'prompts without affecting the library itself.',
|
||||
)
|
||||
template_input_id = fields.Many2one(
|
||||
'fp.step.template.input',
|
||||
string='Source Library Prompt',
|
||||
ondelete='set null',
|
||||
help='Set when this row was snapshot-copied from a library template '
|
||||
'prompt. Powers "Reset to Library Defaults" — rows where this '
|
||||
'is False are treated as recipe-only custom prompts and survive '
|
||||
'the reset.',
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class FpStepTemplate(models.Model):
|
||||
@@ -75,22 +75,30 @@ class FpStepTemplate(models.Model):
|
||||
help='Opens the transition form before Mark Done (Sub 12b).')
|
||||
|
||||
default_kind = fields.Selection([
|
||||
('cleaning', 'Cleaning'),
|
||||
('etch', 'Etch'),
|
||||
('rinse', 'Rinse'),
|
||||
('plate', 'Plating'),
|
||||
('bake', 'Bake'),
|
||||
('inspect', 'Inspection'),
|
||||
('receiving', 'Receiving / Incoming Inspection'),
|
||||
('contract_review', 'Contract Review (QA-005)'),
|
||||
('racking', 'Racking'),
|
||||
('derack', 'De-Racking'),
|
||||
('mask', 'Masking'),
|
||||
('demask', 'De-Masking'),
|
||||
('dry', 'Drying'),
|
||||
('cleaning', 'Cleaning'),
|
||||
('electroclean', 'Electroclean'),
|
||||
('etch', 'Etch / Activation'),
|
||||
('rinse', 'Rinse'),
|
||||
('strike', 'Strike (Wood\'s Nickel / Activation)'),
|
||||
('plate', 'Plating'),
|
||||
('replenishment', 'Tank Replenishment'),
|
||||
('wbf_test', 'Water Break Free Test'),
|
||||
('dry', 'Drying'),
|
||||
('bake', 'Bake (HE Relief / Stress Relief)'),
|
||||
('demask', 'De-Masking'),
|
||||
('derack', 'De-Racking'),
|
||||
('inspect', 'Inspection'),
|
||||
('hardness_test', 'Hardness Test (HV / HK / HRC)'),
|
||||
('adhesion_test', 'Adhesion Test'),
|
||||
('salt_spray', 'Salt Spray / Corrosion Test'),
|
||||
('final_inspect', 'Final Inspection'),
|
||||
('packaging', 'Packaging / Pre-Ship'),
|
||||
('ship', 'Shipping'),
|
||||
('gating', 'Gating'),
|
||||
('contract_review', 'Contract Review (QA-005)'),
|
||||
], string='Step Kind', help='Drives sane-default input seeding.')
|
||||
|
||||
input_template_ids = fields.One2many(
|
||||
@@ -138,43 +146,183 @@ class FpStepTemplate(models.Model):
|
||||
# each → 'each', min → 'min'. Format-only strings ('HH:MM') get
|
||||
# left blank since they're not units.
|
||||
DEFAULT_INPUTS_BY_KIND = {
|
||||
'receiving': [
|
||||
{'name': 'Qty Received', 'input_type': 'number',
|
||||
'target_unit': 'each', 'sequence': 10, 'required': True},
|
||||
{'name': 'Qty Rejected', 'input_type': 'number',
|
||||
'target_unit': 'each', 'sequence': 20},
|
||||
{'name': 'Customer PO# Verified', 'input_type': 'boolean', 'sequence': 30},
|
||||
{'name': 'Packing Slip #', 'input_type': 'text', 'sequence': 40},
|
||||
{'name': 'Condition Notes', 'input_type': 'text', 'sequence': 50},
|
||||
{'name': 'Damage Photo', 'input_type': 'photo', 'sequence': 60},
|
||||
{'name': 'Inspector Initials', 'input_type': 'signature',
|
||||
'sequence': 70, 'required': True},
|
||||
],
|
||||
'cleaning': [
|
||||
{'name': 'Actual Time', 'input_type': 'time_seconds',
|
||||
'target_unit': 's', 'sequence': 10},
|
||||
{'name': 'Actual Temperature', 'input_type': 'temperature',
|
||||
'target_unit': 'f', 'sequence': 20},
|
||||
{'name': 'Bath ID', 'input_type': 'text', 'sequence': 30},
|
||||
{'name': 'Ultrasonic On', 'input_type': 'boolean', 'sequence': 40},
|
||||
{'name': 'Titration Done', 'input_type': 'boolean', 'sequence': 50},
|
||||
],
|
||||
'electroclean': [
|
||||
{'name': 'Actual Time', 'input_type': 'time_seconds',
|
||||
'target_unit': 's', 'sequence': 10},
|
||||
{'name': 'Actual Temperature', 'input_type': 'temperature',
|
||||
'target_unit': 'f', 'sequence': 20},
|
||||
{'name': 'Amperage', 'input_type': 'number', 'sequence': 30,
|
||||
'hint': 'A'},
|
||||
{'name': 'Voltage', 'input_type': 'number', 'sequence': 40,
|
||||
'hint': 'V'},
|
||||
{'name': 'Current Density', 'input_type': 'number', 'sequence': 50,
|
||||
'hint': 'ASF (A per sq ft)'},
|
||||
{'name': 'Polarity', 'input_type': 'selection', 'sequence': 60,
|
||||
'selection_options': 'anodic,cathodic,periodic'},
|
||||
{'name': 'Bath ID', 'input_type': 'text', 'sequence': 70},
|
||||
],
|
||||
'etch': [
|
||||
{'name': 'Actual Time', 'input_type': 'time_seconds',
|
||||
'target_unit': 's', 'sequence': 10},
|
||||
{'name': 'Actual Temperature', 'input_type': 'temperature',
|
||||
'target_unit': 'f', 'sequence': 20},
|
||||
{'name': 'Acid Concentration', 'input_type': 'number', 'sequence': 30,
|
||||
'hint': '% or g/L'},
|
||||
{'name': 'Bath ID', 'input_type': 'text', 'sequence': 40},
|
||||
{'name': 'HE Risk Flag', 'input_type': 'boolean', 'sequence': 50,
|
||||
'hint': 'Hydrogen Embrittlement risk for high-strength steel'},
|
||||
],
|
||||
'rinse': [
|
||||
{'name': 'Rinse Type', 'input_type': 'selection', 'sequence': 10,
|
||||
'selection_options': 'cascade,spray,DI,city'},
|
||||
{'name': 'Conductivity', 'input_type': 'number', 'sequence': 20,
|
||||
'hint': 'µS/cm — required for DI rinses'},
|
||||
{'name': 'Actual Time', 'input_type': 'time_seconds',
|
||||
'target_unit': 's', 'sequence': 30},
|
||||
],
|
||||
'strike': [
|
||||
{'name': 'Actual Time', 'input_type': 'time_seconds',
|
||||
'target_unit': 's', 'sequence': 10},
|
||||
{'name': 'Actual Temperature', 'input_type': 'temperature',
|
||||
'target_unit': 'f', 'sequence': 20},
|
||||
{'name': 'Amperage', 'input_type': 'number', 'sequence': 30,
|
||||
'hint': 'A'},
|
||||
{'name': 'Voltage', 'input_type': 'number', 'sequence': 40,
|
||||
'hint': 'V'},
|
||||
{'name': 'Current Density', 'input_type': 'number', 'sequence': 50,
|
||||
'hint': 'ASF'},
|
||||
{'name': 'Bath ID', 'input_type': 'text', 'sequence': 60},
|
||||
],
|
||||
'rinse': [],
|
||||
'plate': [
|
||||
{'name': 'Actual Time', 'input_type': 'time_hms',
|
||||
'target_unit': 'min', 'sequence': 10},
|
||||
{'name': 'Actual Temperature', 'input_type': 'temperature',
|
||||
'target_unit': 'f', 'sequence': 20},
|
||||
{'name': 'Plating Thickness', 'input_type': 'thickness',
|
||||
'target_unit': 'in', 'sequence': 30},
|
||||
{'name': 'Bath ID', 'input_type': 'text', 'sequence': 30},
|
||||
{'name': 'pH', 'input_type': 'ph', 'sequence': 40},
|
||||
{'name': 'Bath Concentration', 'input_type': 'number', 'sequence': 50,
|
||||
'hint': 'g/L'},
|
||||
{'name': 'Current Density', 'input_type': 'number', 'sequence': 60,
|
||||
'hint': 'ASF — electroplate only'},
|
||||
{'name': 'Plating Thickness', 'input_type': 'multi_point_thickness',
|
||||
'target_unit': 'in', 'sequence': 70},
|
||||
],
|
||||
'bake': [
|
||||
{'name': 'Time In', 'input_type': 'text', 'sequence': 10},
|
||||
{'name': 'Time Out', 'input_type': 'text', 'sequence': 20},
|
||||
'replenishment': [
|
||||
{'name': 'Bath ID', 'input_type': 'text', 'sequence': 10,
|
||||
'required': True},
|
||||
{'name': 'Chemistry Added', 'input_type': 'text', 'sequence': 20,
|
||||
'hint': 'name + amount, e.g. "Nickel sulfamate 500mL"'},
|
||||
{'name': 'pH Before', 'input_type': 'ph', 'sequence': 30},
|
||||
{'name': 'pH After', 'input_type': 'ph', 'sequence': 40},
|
||||
{'name': 'Concentration Before', 'input_type': 'number', 'sequence': 50},
|
||||
{'name': 'Concentration After', 'input_type': 'number', 'sequence': 60},
|
||||
{'name': 'Operator Initials', 'input_type': 'signature',
|
||||
'sequence': 70, 'required': True},
|
||||
],
|
||||
'wbf_test': [
|
||||
{'name': 'Result', 'input_type': 'pass_fail', 'sequence': 10,
|
||||
'required': True},
|
||||
{'name': 'Retest Count', 'input_type': 'number', 'sequence': 20},
|
||||
{'name': 'Photo on FAIL', 'input_type': 'photo', 'sequence': 30},
|
||||
],
|
||||
'dry': [
|
||||
{'name': 'Dry Method', 'input_type': 'selection', 'sequence': 10,
|
||||
'selection_options': 'hot air,oven,spin'},
|
||||
{'name': 'Actual Time', 'input_type': 'time_seconds',
|
||||
'target_unit': 's', 'sequence': 20},
|
||||
{'name': 'Actual Temperature', 'input_type': 'temperature',
|
||||
'target_unit': 'f', 'sequence': 30},
|
||||
],
|
||||
'bake': [
|
||||
{'name': 'Time In', 'input_type': 'date', 'sequence': 10},
|
||||
{'name': 'Time Out', 'input_type': 'date', 'sequence': 20},
|
||||
{'name': 'Actual Temperature', 'input_type': 'temperature',
|
||||
'target_unit': 'f', 'sequence': 30},
|
||||
{'name': 'Oven ID', 'input_type': 'text', 'sequence': 40},
|
||||
{'name': 'Chart Recorder File', 'input_type': 'photo', 'sequence': 50,
|
||||
'hint': 'Attach AMS-2759 chart-recorder file'},
|
||||
],
|
||||
'racking': [
|
||||
{'name': 'Actual Qty', 'input_type': 'number',
|
||||
'target_unit': 'each', 'sequence': 10},
|
||||
'target_unit': 'each', 'sequence': 10, 'required': True},
|
||||
{'name': 'Rack ID', 'input_type': 'text', 'sequence': 20},
|
||||
{'name': 'Masking Applied', 'input_type': 'boolean', 'sequence': 30},
|
||||
{'name': 'Photo of Racked Load', 'input_type': 'photo', 'sequence': 40},
|
||||
],
|
||||
'derack': [
|
||||
{'name': 'Actual Qty', 'input_type': 'number',
|
||||
'target_unit': 'each', 'sequence': 10},
|
||||
{'name': 'Mask Removal Method', 'input_type': 'selection', 'sequence': 20,
|
||||
'selection_options': 'mechanical,solvent,thermal,not applicable'},
|
||||
{'name': 'Residue Check', 'input_type': 'pass_fail', 'sequence': 30},
|
||||
],
|
||||
'mask': [
|
||||
{'name': 'Actual Qty', 'input_type': 'number',
|
||||
'target_unit': 'each', 'sequence': 10},
|
||||
{'name': 'Mask Material', 'input_type': 'selection', 'sequence': 20,
|
||||
'selection_options': 'Microshield,latex tape,vinyl plugs,wax,other'},
|
||||
{'name': 'Photo of Masked Parts', 'input_type': 'photo', 'sequence': 30},
|
||||
],
|
||||
'demask': [
|
||||
{'name': 'Residue Check', 'input_type': 'pass_fail', 'sequence': 10},
|
||||
{'name': 'Surface Condition', 'input_type': 'selection', 'sequence': 20,
|
||||
'selection_options': 'clean,marks,needs rework'},
|
||||
],
|
||||
'inspect': [
|
||||
{'name': 'PASS/FAIL', 'input_type': 'pass_fail', 'sequence': 10},
|
||||
{'name': 'Result', 'input_type': 'pass_fail', 'sequence': 10,
|
||||
'required': True},
|
||||
{'name': 'Defect Type', 'input_type': 'selection', 'sequence': 20,
|
||||
'selection_options': 'pitting,burn,blister,peel,missing coverage,none'},
|
||||
{'name': 'Thickness Sample', 'input_type': 'thickness',
|
||||
'target_unit': 'in', 'sequence': 30},
|
||||
{'name': 'Photo', 'input_type': 'photo', 'sequence': 40},
|
||||
{'name': 'Inspector Signature', 'input_type': 'signature', 'sequence': 50},
|
||||
],
|
||||
'hardness_test': [
|
||||
{'name': 'Test Load', 'input_type': 'number', 'sequence': 10,
|
||||
'hint': 'gf'},
|
||||
{'name': 'Readings (HV/HK/HRC)', 'input_type': 'multi_point_thickness',
|
||||
'sequence': 20, 'hint': 'Three indents minimum'},
|
||||
{'name': 'Equipment ID', 'input_type': 'text', 'sequence': 30},
|
||||
{'name': 'Last Calibration Date', 'input_type': 'date', 'sequence': 40},
|
||||
],
|
||||
'adhesion_test': [
|
||||
{'name': 'Test Method', 'input_type': 'selection', 'sequence': 10,
|
||||
'selection_options': 'bend,tape,burnish,file'},
|
||||
{'name': 'Result', 'input_type': 'pass_fail', 'sequence': 20,
|
||||
'required': True},
|
||||
{'name': 'Photo of Coupon', 'input_type': 'photo', 'sequence': 30},
|
||||
],
|
||||
'salt_spray': [
|
||||
{'name': 'Test Duration', 'input_type': 'number', 'sequence': 10,
|
||||
'hint': 'hours'},
|
||||
{'name': 'Result', 'input_type': 'pass_fail', 'sequence': 20,
|
||||
'required': True},
|
||||
{'name': 'Red Rust %', 'input_type': 'number', 'sequence': 30},
|
||||
{'name': 'White Corrosion %', 'input_type': 'number', 'sequence': 40},
|
||||
{'name': 'Lab Report', 'input_type': 'photo', 'sequence': 50,
|
||||
'hint': 'Attach scanned lab report'},
|
||||
],
|
||||
'final_inspect': [
|
||||
{'name': 'Outgoing Part Count Verified',
|
||||
@@ -183,35 +331,80 @@ class FpStepTemplate(models.Model):
|
||||
'target_unit': 'each', 'sequence': 20},
|
||||
{'name': 'Qty Rejected', 'input_type': 'number',
|
||||
'target_unit': 'each', 'sequence': 30},
|
||||
{'name': 'Defect Categorization', 'input_type': 'selection', 'sequence': 35,
|
||||
'selection_options': 'pitting,burn,blister,peel,missing coverage,dimensional,none'},
|
||||
{'name': 'Actual Coating Thickness',
|
||||
'input_type': 'thickness', 'target_unit': 'in', 'sequence': 40},
|
||||
{'name': 'Pass/Fail', 'input_type': 'pass_fail', 'sequence': 50},
|
||||
'input_type': 'multi_point_thickness',
|
||||
'target_unit': 'in', 'sequence': 40},
|
||||
{'name': 'Dimensional Verification', 'input_type': 'pass_fail',
|
||||
'sequence': 45},
|
||||
{'name': 'Surface Finish (Ra)', 'input_type': 'number', 'sequence': 47,
|
||||
'hint': 'µin'},
|
||||
{'name': 'Pass/Fail', 'input_type': 'pass_fail', 'sequence': 50,
|
||||
'required': True},
|
||||
{'name': 'Inspector Signature', 'input_type': 'signature', 'sequence': 60},
|
||||
],
|
||||
'wbf_test': [
|
||||
{'name': 'PASS/FAIL', 'input_type': 'pass_fail', 'sequence': 10},
|
||||
'packaging': [
|
||||
{'name': 'Packaging Type', 'input_type': 'selection', 'sequence': 10,
|
||||
'selection_options': 'VCI bag,bubble wrap,separator paper,custom crate,other'},
|
||||
{'name': 'Qty Per Package', 'input_type': 'number',
|
||||
'target_unit': 'each', 'sequence': 20},
|
||||
{'name': 'Package Count', 'input_type': 'number', 'sequence': 30},
|
||||
{'name': 'Cert Package Included', 'input_type': 'boolean', 'sequence': 40},
|
||||
{'name': 'Customer-Supplied Packaging', 'input_type': 'boolean',
|
||||
'sequence': 50},
|
||||
],
|
||||
'mask': [
|
||||
{'name': 'Actual Qty', 'input_type': 'number',
|
||||
'target_unit': 'each', 'sequence': 10},
|
||||
],
|
||||
'demask': [],
|
||||
'dry': [],
|
||||
'ship': [
|
||||
{'name': 'Outgoing Qty', 'input_type': 'number',
|
||||
'target_unit': 'each', 'sequence': 10},
|
||||
'target_unit': 'each', 'sequence': 10, 'required': True},
|
||||
{'name': 'Carrier', 'input_type': 'selection', 'sequence': 20,
|
||||
'selection_options': 'UPS,FedEx,Purolator,Customer Pickup,Other'},
|
||||
{'name': 'Tracking #', 'input_type': 'text', 'sequence': 30},
|
||||
{'name': 'BoL #', 'input_type': 'text', 'sequence': 40},
|
||||
{'name': 'Photo of Sealed Shipment', 'input_type': 'photo',
|
||||
'sequence': 50},
|
||||
],
|
||||
'gating': [],
|
||||
# Sub 4 + 12c follow-up — Contract Review step (Policy B).
|
||||
# The shop-floor step itself is a tickbox; the heavy QA-005 form
|
||||
# is opened via fp.contract.review (separate model). These
|
||||
# inputs capture summary fields for the chronological CoC.
|
||||
'contract_review': [
|
||||
{'name': 'Reviewer Initials', 'input_type': 'text', 'sequence': 10},
|
||||
{'name': 'Reviewer Initials', 'input_type': 'signature', 'sequence': 10},
|
||||
{'name': 'Date Reviewed', 'input_type': 'date', 'sequence': 20},
|
||||
{'name': 'QA-005 Approved', 'input_type': 'pass_fail', 'sequence': 30},
|
||||
],
|
||||
}
|
||||
|
||||
COMMON_AUDIT_FIELDS = [
|
||||
{'name': 'Operator Initials', 'input_type': 'signature',
|
||||
'required': True, 'sequence': 800},
|
||||
{'name': 'Bath ID', 'input_type': 'text', 'sequence': 810},
|
||||
{'name': 'Photo on Failure', 'input_type': 'photo', 'sequence': 820,
|
||||
'hint': 'upload only if failure observed'},
|
||||
{'name': 'Equipment ID', 'input_type': 'text', 'sequence': 830},
|
||||
]
|
||||
|
||||
def action_add_common_audit_fields(self):
|
||||
"""Idempotently append the common audit fields to this template.
|
||||
Skips rows whose name already exists. Logs to chatter.
|
||||
"""
|
||||
Input = self.env['fp.step.template.input']
|
||||
for tpl in self:
|
||||
existing_names = set(tpl.input_template_ids.mapped('name'))
|
||||
added = []
|
||||
for spec in self.COMMON_AUDIT_FIELDS:
|
||||
if spec['name'] in existing_names:
|
||||
continue
|
||||
Input.create({
|
||||
'template_id': tpl.id,
|
||||
**spec,
|
||||
})
|
||||
added.append(spec['name'])
|
||||
if added:
|
||||
tpl.message_post(
|
||||
body=_('Added common audit fields: %s') % ', '.join(added),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
return True
|
||||
|
||||
def action_seed_default_inputs(self):
|
||||
"""Seed input_template_ids based on default_kind. Idempotent —
|
||||
only adds inputs whose names don't already exist on this template.
|
||||
|
||||
@@ -36,6 +36,10 @@ class FpStepTemplateInput(models.Model):
|
||||
('temperature', 'Temperature'),
|
||||
('thickness', 'Thickness'),
|
||||
('pass_fail', 'Pass / Fail'),
|
||||
('photo', 'Photo'),
|
||||
('multi_point_thickness', 'Multi-Point Thickness (avg)'),
|
||||
('bath_chemistry_panel', 'Bath Chemistry Panel'),
|
||||
('ph', 'pH'),
|
||||
], string='Input Type', required=True, default='text')
|
||||
target_min = fields.Float(string='Target Min',
|
||||
help='Lower bound of the acceptable range, expressed in Target Unit.')
|
||||
|
||||
167
fusion_plating/fusion_plating/scripts/bt_step_library_audit.py
Normal file
167
fusion_plating/fusion_plating/scripts/bt_step_library_audit.py
Normal file
@@ -0,0 +1,167 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Battle test — Step Library audit expansion (Sub 12d).
|
||||
|
||||
Run via odoo-shell on entech:
|
||||
|
||||
cat bt_step_library_audit.py | ssh pve-worker5 "pct exec 111 -- bash -c \\
|
||||
'su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\"'"
|
||||
|
||||
Asserts properties of the new architecture and prints PASS/FAIL.
|
||||
"""
|
||||
|
||||
NEW_KINDS = [
|
||||
'receiving', 'electroclean', 'strike', 'salt_spray',
|
||||
'adhesion_test', 'hardness_test', 'packaging', 'replenishment',
|
||||
]
|
||||
|
||||
results = []
|
||||
|
||||
|
||||
def check(idx, name, condition, detail=''):
|
||||
status = 'PASS' if condition else 'FAIL'
|
||||
results.append((idx, name, status, detail))
|
||||
print('[%s] #%-2d %s -- %s' % (status, idx, name, detail))
|
||||
|
||||
|
||||
Template = env['fp.step.template']
|
||||
Node = env['fusion.plating.process.node']
|
||||
NodeInput = env['fusion.plating.process.node.input']
|
||||
|
||||
# 1. Every new Step Kind has at least 1 seed template loaded
|
||||
for kind in NEW_KINDS:
|
||||
cnt = Template.search_count([('default_kind', '=', kind)])
|
||||
check(1, 'seed template for kind %s' % kind, cnt >= 1,
|
||||
'%d found' % cnt)
|
||||
|
||||
# 2. New input types reachable from the library Selection
|
||||
itypes = dict(Template._fields['default_kind'].selection)
|
||||
all_kinds_present = all(k in itypes for k in NEW_KINDS)
|
||||
check(2, 'all 8 new kinds in Selection', all_kinds_present,
|
||||
'kinds=%d total in selection' % len(itypes))
|
||||
|
||||
# 3. fp.step.template.input has the 4 new input_type entries
|
||||
ti = dict(env['fp.step.template.input']._fields['input_type'].selection)
|
||||
new_types_present = all(t in ti for t in
|
||||
['photo', 'multi_point_thickness',
|
||||
'bath_chemistry_panel', 'ph'])
|
||||
check(3, 'library input has 4 new types', new_types_present,
|
||||
'%d total types' % len(ti))
|
||||
|
||||
# 4. Recipe-node input has the 4 new input_type entries
|
||||
ni = dict(NodeInput._fields['input_type'].selection)
|
||||
new_types_in_node = all(t in ni for t in
|
||||
['photo', 'multi_point_thickness',
|
||||
'bath_chemistry_panel', 'ph'])
|
||||
check(4, 'recipe-node input has 4 new types', new_types_in_node,
|
||||
'%d total types' % len(ni))
|
||||
|
||||
# 5. collect + collect_measurements + template_input_id fields exist
|
||||
check(5, 'collect on node-input', 'collect' in NodeInput._fields,
|
||||
'present' if 'collect' in NodeInput._fields else 'missing')
|
||||
check(6, 'collect_measurements on node', 'collect_measurements' in Node._fields,
|
||||
'present')
|
||||
check(7, 'template_input_id on node-input', 'template_input_id' in NodeInput._fields,
|
||||
'present')
|
||||
|
||||
# 8. action_seed_default_inputs is idempotent + preserves edits
|
||||
tpl = Template.create({
|
||||
'name': 'BT-SeedIdem-%s' % env.cr.now(),
|
||||
'default_kind': 'plate',
|
||||
})
|
||||
tpl.action_seed_default_inputs()
|
||||
n1 = len(tpl.input_template_ids)
|
||||
# user edit
|
||||
tpl.input_template_ids[0].name = 'EDITED-DO-NOT-CLOBBER'
|
||||
tpl.action_seed_default_inputs()
|
||||
n2 = len(tpl.input_template_ids)
|
||||
edited = tpl.input_template_ids.filtered(
|
||||
lambda i: i.name == 'EDITED-DO-NOT-CLOBBER'
|
||||
)
|
||||
check(8, 'seed idempotent + preserves edits',
|
||||
n1 <= n2 and len(edited) == 1,
|
||||
'before=%d after=%d edited_kept=%s' % (n1, n2, bool(edited)))
|
||||
tpl.unlink()
|
||||
|
||||
# 9. action_add_common_audit_fields is idempotent
|
||||
tpl = Template.create({
|
||||
'name': 'BT-AuditIdem-%s' % env.cr.now(),
|
||||
'default_kind': 'plate',
|
||||
})
|
||||
tpl.action_add_common_audit_fields()
|
||||
m1 = len(tpl.input_template_ids)
|
||||
tpl.action_add_common_audit_fields()
|
||||
m2 = len(tpl.input_template_ids)
|
||||
check(9, 'common audit fields idempotent', m1 == m2,
|
||||
'first=%d second=%d' % (m1, m2))
|
||||
tpl.unlink()
|
||||
|
||||
# 10. collect=True is default on new node-inputs
|
||||
node = Node.create({
|
||||
'name': 'BT-CollectDefault',
|
||||
'node_type': 'step',
|
||||
})
|
||||
ni = NodeInput.create({
|
||||
'node_id': node.id,
|
||||
'name': 'BT-Prompt',
|
||||
'input_type': 'text',
|
||||
'kind': 'step_input',
|
||||
})
|
||||
check(10, 'collect default=True on new node-input', ni.collect,
|
||||
'collect=%s' % ni.collect)
|
||||
|
||||
# 11. collect_measurements=True default on new node
|
||||
check(11, 'collect_measurements default=True on new node',
|
||||
node.collect_measurements,
|
||||
'collect_measurements=%s' % node.collect_measurements)
|
||||
node.unlink()
|
||||
|
||||
# 12. Wizard filter excludes collect=False rows (simulated)
|
||||
node = Node.create({'name': 'BT-Filter', 'node_type': 'step'})
|
||||
ni_on = NodeInput.create({
|
||||
'node_id': node.id, 'name': 'On', 'input_type': 'text',
|
||||
'kind': 'step_input', 'collect': True,
|
||||
})
|
||||
ni_off = NodeInput.create({
|
||||
'node_id': node.id, 'name': 'Off', 'input_type': 'text',
|
||||
'kind': 'step_input', 'collect': False,
|
||||
})
|
||||
visible = node.input_ids.filtered(
|
||||
lambda i: i.kind == 'step_input' and i.collect
|
||||
)
|
||||
check(12, 'wizard filter excludes collect=False',
|
||||
ni_off not in visible and ni_on in visible,
|
||||
'%d/%d visible' % (len(visible), len(node.input_ids)))
|
||||
|
||||
# 13. Master switch path — when False, filter returns empty
|
||||
node.collect_measurements = False
|
||||
empty_path = (not node.collect_measurements)
|
||||
check(13, 'master collect_measurements=False short-circuits',
|
||||
empty_path, 'master=False')
|
||||
node.unlink()
|
||||
|
||||
# 14. Multi-point thickness average compute (unit math, no DB)
|
||||
class _Stub:
|
||||
def __init__(self, *vals):
|
||||
self.point_1, self.point_2, self.point_3, \
|
||||
self.point_4, self.point_5 = vals
|
||||
non_empty = [v for v in vals if v]
|
||||
self.point_avg = sum(non_empty) / len(non_empty) if non_empty else 0
|
||||
s = _Stub(0.001, 0.0012, 0.0011, 0, 0)
|
||||
check(14, 'multi-point avg skips empties',
|
||||
round(s.point_avg, 5) == 0.0011,
|
||||
'avg=%.5f' % s.point_avg)
|
||||
|
||||
# 15. Sample DEFAULT_INPUTS_BY_KIND payload present for each new kind
|
||||
for kind in NEW_KINDS:
|
||||
seeded = Template.DEFAULT_INPUTS_BY_KIND.get(kind, [])
|
||||
check(15, 'defaults dict has entries for %s' % kind,
|
||||
len(seeded) >= 1,
|
||||
'%d default prompts' % len(seeded))
|
||||
|
||||
# Summary
|
||||
total = len(results)
|
||||
passed = sum(1 for r in results if r[2] == 'PASS')
|
||||
failed = sum(1 for r in results if r[2] == 'FAIL')
|
||||
print('\n=== %d / %d PASSED -- %d FAILED ===' % (passed, total, failed))
|
||||
|
||||
env.cr.commit()
|
||||
@@ -117,16 +117,28 @@
|
||||
<field name="description" widget="html"/>
|
||||
</page>
|
||||
<page string="Operator Inputs" name="inputs">
|
||||
<group>
|
||||
<field name="collect_measurements"
|
||||
widget="boolean_toggle"
|
||||
help="Master switch — when off, the operator wizard skips this step entirely."/>
|
||||
</group>
|
||||
<field name="input_ids">
|
||||
<list editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="collect" widget="boolean_toggle"
|
||||
string="Collect"/>
|
||||
<field name="name"/>
|
||||
<field name="input_type"/>
|
||||
<field name="kind" optional="hide"/>
|
||||
<field name="target_min" optional="hide"/>
|
||||
<field name="target_max" optional="hide"/>
|
||||
<field name="target_unit" optional="hide"/>
|
||||
<field name="required"/>
|
||||
<field name="hint"/>
|
||||
<field name="uom"/>
|
||||
<field name="selection_options"
|
||||
invisible="input_type != 'selection'"/>
|
||||
<field name="template_input_id" optional="hide"
|
||||
string="From Library"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
<button name="action_seed_default_inputs" type="object"
|
||||
string="Seed Default Inputs" class="btn-secondary"
|
||||
invisible="not default_kind"/>
|
||||
<button name="action_add_common_audit_fields" type="object"
|
||||
string="+ Common Audit Fields"
|
||||
class="btn-secondary"
|
||||
help="Append Operator Initials, Bath ID, Photo on Failure, Equipment ID"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
@@ -58,9 +62,14 @@
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Instructions" name="instructions">
|
||||
<page string="Default Operator Instructions" name="instructions">
|
||||
<div class="alert alert-info" role="alert">
|
||||
Standing instructions the office gives operators for this
|
||||
step. Snapshot-copied onto every recipe that uses this
|
||||
step. Recipe authors can override per recipe.
|
||||
</div>
|
||||
<field name="description"
|
||||
placeholder="Rich-text instructions / WI reference."/>
|
||||
placeholder="e.g. Mask threaded holes with vinyl plugs. Use Microshield for through-holes."/>
|
||||
</page>
|
||||
<page string="Operation Measurements" name="op_measurements">
|
||||
<field name="input_template_ids">
|
||||
|
||||
Reference in New Issue
Block a user