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:
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()
|
||||
Reference in New Issue
Block a user