fix(operator-wizard): surface office-authored instructions to operators
Critical UX gap discovered in production-environment battle test: when operator hits "Mark Done" and the input wizard fires, they only saw the measurement prompts list. The rich-text instructions written by the office (recipe_node.description) never reached the operator at the exact moment they need them. Fixed: wizard model gains instructions (Html, computed from step.recipe_node_id.description) + has_instructions flag. Form view renders the instructions in a prominent blue alert at the top of the wizard, above the Measurements list. Hidden when blank so operators on instruction-less steps don't see noise. Also: extend default_kind Selection on fusion.plating.process.node to match fp.step.template — both models now have the same 24 kinds. Without this, recipe authors could pick a kind in the library template form that the recipe-node Selection rejected with a ValueError. Battle test artifact: - Recipe "Hard Anodize Type III + Dye + Seal" (id=1863) — 23 steps, 105 measurement prompts, rich-text operator instructions per step - SO S00278 for ABC Manufactoring confirmed → fp.job 1236 / WH/JOB/00337 with all 23 steps materialized, 105 prompts visible to operators - Wizard test: step "11. Hard Anodize Type III" → 516 chars of instructions render + 7 input prompts in the form Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
353
fusion_plating/fusion_plating/scripts/bt_create_hard_anodize.py
Normal file
353
fusion_plating/fusion_plating/scripts/bt_create_hard_anodize.py
Normal file
@@ -0,0 +1,353 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Create a brand-new Hard Anodize Type III + Dye + Seal recipe with
|
||||
rich operator instructions + measurement prompts on every step,
|
||||
then create an SO for ABC Manufacturing and confirm it so the
|
||||
operator-facing job is ready to run.
|
||||
|
||||
After running, the user navigates to:
|
||||
- Recipe form (Process Recipes menu) — verify instructions present
|
||||
- Simple Recipe Editor — verify per-step Instructions + Measurements
|
||||
- Sale Orders — verify the new SO with line referencing the recipe
|
||||
- Plating Jobs — verify job created with all steps
|
||||
- Click Mark Done on any step → verify operator wizard shows
|
||||
instructions + measurement prompts
|
||||
"""
|
||||
|
||||
import json
|
||||
from odoo import fields
|
||||
|
||||
Node = env['fusion.plating.process.node']
|
||||
NodeInput = env['fusion.plating.process.node.input']
|
||||
Template = env['fp.step.template']
|
||||
|
||||
print('\n========== Build Hard Anodize Type III Recipe ==========\n')
|
||||
|
||||
# Clean up any prior run of this script (prior recipes + their variants + jobs + SOs)
|
||||
prior_recipes = Node.search([
|
||||
'|', ('name', '=', 'Hard Anodize Type III + Dye + Seal'),
|
||||
('name', 'ilike', 'Hard Anodize Type III + Dye + Seal — '),
|
||||
])
|
||||
if prior_recipes:
|
||||
print('Cleaning up %d prior recipe(s)...' % len(prior_recipes))
|
||||
# Cancel + delete jobs that reference these recipes
|
||||
prior_jobs = env['fp.job'].search([('recipe_id', 'in', prior_recipes.ids)])
|
||||
for j in prior_jobs:
|
||||
try:
|
||||
j.write({'state': 'cancel'})
|
||||
except Exception:
|
||||
pass
|
||||
prior_sos = env['sale.order'].search([
|
||||
('order_line.x_fc_process_variant_id', 'in', prior_recipes.ids),
|
||||
('state', 'in', ('draft', 'sent')),
|
||||
])
|
||||
prior_sos.unlink()
|
||||
prior_jobs.unlink()
|
||||
prior_recipes.unlink()
|
||||
env.cr.commit()
|
||||
|
||||
# ----- Recipe root -----
|
||||
recipe = Node.create({
|
||||
'name': 'Hard Anodize Type III + Dye + Seal',
|
||||
'code': 'HARD_ANO_T3',
|
||||
'node_type': 'recipe',
|
||||
'is_template': True,
|
||||
'description': '''<p><strong>Hard Anodize Type III per MIL-A-8625F</strong></p>
|
||||
<p>Aluminum substrate. Black sulfuric dye. Hot nickel acetate seal.</p>
|
||||
<p>Tolerances: 0.0015"–0.0020" coating thickness, 60kV breakdown
|
||||
voltage. Hardness ≥350HV300.</p>''',
|
||||
})
|
||||
print('Created recipe:', recipe.id, recipe.name)
|
||||
|
||||
# ----- Step definitions -----
|
||||
# Each spec: name, kind, description (operator instructions), extra_prompts (additional manually-authored)
|
||||
STEPS = [
|
||||
{
|
||||
'name': '1. Receiving',
|
||||
'kind': 'receiving',
|
||||
'description': '''<p><strong>Verify quantity and condition on arrival.</strong></p>
|
||||
<ul><li>Count parts against the packing slip</li>
|
||||
<li>Visually inspect for damage, corrosion, oil residue</li>
|
||||
<li>Photograph any damage or non-conformance</li>
|
||||
<li>Sign off below before parts move to staging</li></ul>''',
|
||||
},
|
||||
{
|
||||
'name': '2. Contract Review (QA-005)',
|
||||
'kind': 'contract_review',
|
||||
'description': '''<p><strong>Verify the order against quality requirements.</strong></p>
|
||||
<ul><li>Confirm spec matches PO (MIL-A-8625F Type III, Class 2 Black)</li>
|
||||
<li>Cross-check thickness, hardness, salt-spray requirements</li>
|
||||
<li>Open the QA-005 contract review form (separate dialog)</li>
|
||||
<li>Approve only after all line items reconciled</li></ul>''',
|
||||
},
|
||||
{
|
||||
'name': '3. Incoming Inspection',
|
||||
'kind': 'inspect',
|
||||
'description': '''<p><strong>First-piece inspection before processing.</strong></p>
|
||||
<ul><li>Verify part number against drawing</li>
|
||||
<li>Inspect surface for prior plating remnants, scratches, pitting</li>
|
||||
<li>Photograph any defects</li>
|
||||
<li>Record incoming dimensional spot-check</li></ul>''',
|
||||
},
|
||||
{
|
||||
'name': '4. Racking',
|
||||
'kind': 'racking',
|
||||
'description': '''<p><strong>Mount parts on titanium rack.</strong></p>
|
||||
<ul><li>Use Rack T-12 or similar (titanium, NOT aluminum)</li>
|
||||
<li>Ensure firm electrical contact at rack hooks</li>
|
||||
<li>Apply masking to threaded holes if required (see drawing)</li>
|
||||
<li>Photograph the loaded rack before submerging</li></ul>''',
|
||||
},
|
||||
{
|
||||
'name': '5. Alkaline Clean (Tank A-1)',
|
||||
'kind': 'cleaning',
|
||||
'description': '''<p><strong>Soak clean — Aluminum Etch Cleaner.</strong></p>
|
||||
<ul><li>Tank: A-1, Bath: ALKCLEAN-1</li>
|
||||
<li>Time: 4–6 minutes</li>
|
||||
<li>Temperature: 140–160°F</li>
|
||||
<li>Confirm titration done within last 24 hours</li></ul>''',
|
||||
},
|
||||
{
|
||||
'name': '6. Rinse — Cascade DI (Tank A-2)',
|
||||
'kind': 'rinse',
|
||||
'description': '''<p><strong>Triple cascade DI rinse.</strong></p>
|
||||
<ul><li>Tank A-2 (DI rinse, conductivity < 50 µS/cm)</li>
|
||||
<li>Time: 30 seconds, agitate</li>
|
||||
<li>Verify conductivity reading on bath log</li></ul>''',
|
||||
},
|
||||
{
|
||||
'name': '7. Etch (Tank A-3)',
|
||||
'kind': 'etch',
|
||||
'description': '''<p><strong>Caustic etch — sodium hydroxide bath.</strong></p>
|
||||
<ul><li>Tank: A-3, Bath: ETCH-1</li>
|
||||
<li>Time: 30–90 seconds (per drawing — heavy etch removes 0.0005"/side)</li>
|
||||
<li>Temperature: 130–150°F</li>
|
||||
<li>Concentration: 4–6 oz/gal NaOH</li>
|
||||
<li>HE-risk parts (high-strength) require post-bake — flag accordingly</li></ul>''',
|
||||
},
|
||||
{
|
||||
'name': '8. Rinse — Cascade DI (Tank A-4)',
|
||||
'kind': 'rinse',
|
||||
'description': '<p>Triple cascade DI rinse — Tank A-4. 30 sec agitate.</p>',
|
||||
},
|
||||
{
|
||||
'name': '9. Desmut / Deoxidize (Tank A-5)',
|
||||
'kind': 'etch',
|
||||
'description': '''<p><strong>Acid desmut to remove black smut from etch.</strong></p>
|
||||
<ul><li>Tank: A-5, Bath: DEOX-1 (HNO3-based)</li>
|
||||
<li>Time: 30–60 seconds</li>
|
||||
<li>Temperature: ambient</li>
|
||||
<li>Surface should be water-break-free after this step</li></ul>''',
|
||||
},
|
||||
{
|
||||
'name': '10. Rinse — Cascade DI (Tank A-6)',
|
||||
'kind': 'rinse',
|
||||
'description': '<p>Final pre-anodize rinse — Tank A-6. Conductivity must be < 50 µS/cm.</p>',
|
||||
},
|
||||
{
|
||||
'name': '11. Hard Anodize Type III (Tank A-9)',
|
||||
'kind': 'plate',
|
||||
'description': '''<p><strong>HARD ANODIZE — the primary process step.</strong></p>
|
||||
<ul><li>Tank: A-9, Bath: HARDANO-1 (15% sulfuric acid)</li>
|
||||
<li>Temperature: 28–32°F (chilled bath — confirm chiller is running)</li>
|
||||
<li>Current density: 24–36 ASF</li>
|
||||
<li>Voltage ramp: 0–80V over first 5 minutes</li>
|
||||
<li>Time at voltage: 60 minutes (gives ~0.002" coating)</li>
|
||||
<li>Record amperage every 15 minutes</li>
|
||||
<li>Check thickness midway with Fischerscope on witness coupon</li>
|
||||
<li>If color reading off, halt and call supervisor</li></ul>''',
|
||||
},
|
||||
{
|
||||
'name': '12. Rinse — Cold (Tank A-12)',
|
||||
'kind': 'rinse',
|
||||
'description': '<p>Cold cascade rinse to remove sulfuric residue. Tank A-12.</p>',
|
||||
},
|
||||
{
|
||||
'name': '13. Black Dye Bath (Tank A-14)',
|
||||
'kind': 'plate',
|
||||
'description': '''<p><strong>Sulfo Black BL dye absorption.</strong></p>
|
||||
<ul><li>Tank: A-14, Bath: DYE-BL-1</li>
|
||||
<li>Temperature: 130–150°F</li>
|
||||
<li>Time: 12–18 minutes</li>
|
||||
<li>Maintain pH 5.0–6.0</li>
|
||||
<li>Visually verify uniform black with no streaks before sealing</li></ul>''',
|
||||
},
|
||||
{
|
||||
'name': '14. Rinse — Warm (Tank A-15)',
|
||||
'kind': 'rinse',
|
||||
'description': '<p>Warm rinse before sealing. Tank A-15. ~110°F.</p>',
|
||||
},
|
||||
{
|
||||
'name': '15. Hot Nickel Acetate Seal (Tank A-16)',
|
||||
'kind': 'bake',
|
||||
'description': '''<p><strong>Nickel acetate seal — locks in dye, improves corrosion resistance.</strong></p>
|
||||
<ul><li>Tank: A-16, Bath: SEAL-NA-1</li>
|
||||
<li>Temperature: 195–205°F</li>
|
||||
<li>Time: 18–22 minutes</li>
|
||||
<li>pH: 5.5–6.0</li>
|
||||
<li>Attach AMS-2759 chart-recorder file as photo before unloading</li>
|
||||
<li>Quality of seal verified post-process by dye absorption test</li></ul>''',
|
||||
},
|
||||
{
|
||||
'name': '16. Hot DI Rinse (Tank A-17)',
|
||||
'kind': 'rinse',
|
||||
'description': '<p>Final hot DI rinse. Tank A-17. 180°F+. Drives off residual seal solution.</p>',
|
||||
},
|
||||
{
|
||||
'name': '17. Drying (Hot Air Knife)',
|
||||
'kind': 'dry',
|
||||
'description': '''<p><strong>Hot-air knife dry — leave parts on rack.</strong></p>
|
||||
<ul><li>Hot air knife @ 180°F</li>
|
||||
<li>Time: 5 minutes minimum</li>
|
||||
<li>Verify parts fully dry before unracking — water spotting is a defect</li></ul>''',
|
||||
},
|
||||
{
|
||||
'name': '18. De-Racking',
|
||||
'kind': 'derack',
|
||||
'description': '''<p><strong>Remove parts from rack carefully.</strong></p>
|
||||
<ul><li>Wear lint-free gloves</li>
|
||||
<li>Inspect each part for rack-mark touch-up needs</li>
|
||||
<li>Place on padded staging cart</li></ul>''',
|
||||
},
|
||||
{
|
||||
'name': '19. Final Visual Inspection',
|
||||
'kind': 'final_inspect',
|
||||
'description': '''<p><strong>Visual + dimensional + thickness QC.</strong></p>
|
||||
<ul><li>Visual: uniform black, no streaks, scratches, or burn</li>
|
||||
<li>Dimensional: spot-check 3 critical dims per part</li>
|
||||
<li>Thickness: 5-point Fischerscope reading per drawing locations</li>
|
||||
<li>Photograph any defects, route to NCR if rework needed</li>
|
||||
<li>Inspector signs off below</li></ul>''',
|
||||
},
|
||||
{
|
||||
'name': '20. Microhardness Test (Witness Coupon)',
|
||||
'kind': 'hardness_test',
|
||||
'description': '''<p><strong>HV300 hardness measurement on witness coupon.</strong></p>
|
||||
<ul><li>Equipment: LECO LM247AT microhardness tester (verify cal date current)</li>
|
||||
<li>Load: 300 gf, dwell 10s</li>
|
||||
<li>Take 3 indents, log each + average</li>
|
||||
<li>Spec: ≥350 HV300 (MIL-A-8625F Type III)</li></ul>''',
|
||||
},
|
||||
{
|
||||
'name': '21. Salt Spray (Witness, ASTM B117)',
|
||||
'kind': 'salt_spray',
|
||||
'description': '''<p><strong>336-hour salt spray on witness coupon.</strong></p>
|
||||
<ul><li>Submit 3 coupons to lab</li>
|
||||
<li>Test duration: 336 hours minimum</li>
|
||||
<li>Acceptance: zero red rust, < 5% white corrosion</li>
|
||||
<li>Attach lab certificate when received</li></ul>''',
|
||||
},
|
||||
{
|
||||
'name': '22. Packaging',
|
||||
'kind': 'packaging',
|
||||
'description': '''<p><strong>Wrap and stage for shipment.</strong></p>
|
||||
<ul><li>Wrap each part in VCI paper</li>
|
||||
<li>Place in foam-lined box, max 4 parts per box</li>
|
||||
<li>Include CoC + dimensional report inside the box</li>
|
||||
<li>Seal with company tape</li></ul>''',
|
||||
},
|
||||
{
|
||||
'name': '23. Shipping',
|
||||
'kind': 'ship',
|
||||
'description': '''<p><strong>Outbound — confirm carrier and BoL.</strong></p>
|
||||
<ul><li>Carrier per SO (UPS / FedEx / customer pickup)</li>
|
||||
<li>Print BoL, attach to package</li>
|
||||
<li>Photograph sealed shipment for proof-of-shipment</li>
|
||||
<li>Update tracking # in this SO</li></ul>''',
|
||||
},
|
||||
]
|
||||
|
||||
# ----- Build steps -----
|
||||
DEFAULTS = Template.DEFAULT_INPUTS_BY_KIND
|
||||
total_prompts = 0
|
||||
for idx, spec in enumerate(STEPS):
|
||||
step = Node.create({
|
||||
'name': spec['name'],
|
||||
# node_type='operation' is required for fp.job to materialize a
|
||||
# work-order step from this node. 'step' nodes are sub-elements
|
||||
# under an operation (e.g. child rinses), not job-step builders.
|
||||
'node_type': 'operation',
|
||||
'parent_id': recipe.id,
|
||||
'sequence': (idx + 1) * 10,
|
||||
'default_kind': spec['kind'],
|
||||
'description': spec['description'],
|
||||
'collect_measurements': True,
|
||||
})
|
||||
# Seed prompts based on kind
|
||||
for input_spec in DEFAULTS.get(spec['kind'], []):
|
||||
NodeInput.create({
|
||||
'node_id': step.id,
|
||||
'name': input_spec['name'],
|
||||
'input_type': input_spec.get('input_type', 'text'),
|
||||
'kind': 'step_input',
|
||||
'collect': True,
|
||||
'sequence': input_spec.get('sequence', 10),
|
||||
'required': input_spec.get('required', False),
|
||||
'hint': input_spec.get('hint', ''),
|
||||
'selection_options': input_spec.get('selection_options', ''),
|
||||
'target_unit': input_spec.get('target_unit', False),
|
||||
})
|
||||
total_prompts += 1
|
||||
|
||||
env.cr.commit()
|
||||
print('Built %d steps with %d total prompts' % (len(STEPS), total_prompts))
|
||||
|
||||
# ----- Create SO for ABC Manufacturing -----
|
||||
print('\n========== Create SO for ABC ==========\n')
|
||||
|
||||
abc = env['res.partner'].browse(943)
|
||||
part = env['fp.part.catalog'].browse(148) # ABC part 4321
|
||||
prod = env['product.product'].search([], limit=1)
|
||||
|
||||
so = env['sale.order'].create({
|
||||
'partner_id': abc.id,
|
||||
'x_fc_planned_start_date': fields.Date.today(),
|
||||
'x_fc_po_number': 'BT-PO-HARDANO-001',
|
||||
'client_order_ref': 'BT-PO-HARDANO-001',
|
||||
})
|
||||
sol = env['sale.order.line'].create({
|
||||
'order_id': so.id,
|
||||
'product_id': prod.id,
|
||||
'product_uom_qty': 10,
|
||||
'price_unit': 350.0,
|
||||
'name': 'Hard Anodize Type III + Black Dye + Seal',
|
||||
'x_fc_part_catalog_id': part.id,
|
||||
'x_fc_process_variant_id': recipe.id,
|
||||
})
|
||||
print('Created SO:', so.name, 'line', sol.id)
|
||||
|
||||
# Confirm — triggers fp.job creation
|
||||
so.action_confirm()
|
||||
print('Confirmed SO. State =', so.state)
|
||||
env.cr.commit()
|
||||
|
||||
# Find resulting job
|
||||
job = env['fp.job'].search([('origin', '=', so.name)], limit=1)
|
||||
print('\n========== Job created ==========\n')
|
||||
print('Job:', job.id, job.name, '| recipe variant:', job.recipe_id.name if job.recipe_id else None)
|
||||
job_steps = env['fp.job.step'].search([('job_id', '=', job.id)], order='sequence')
|
||||
print('Steps on job:', len(job_steps))
|
||||
|
||||
print('\n========== Verification ==========\n')
|
||||
prompts_visible = 0
|
||||
instructions_visible = 0
|
||||
for js in job_steps:
|
||||
rn = js.recipe_node_id
|
||||
if rn:
|
||||
ins = bool(rn.description)
|
||||
prompts = len(rn.input_ids.filtered(lambda i: i.collect))
|
||||
instructions_visible += int(ins)
|
||||
prompts_visible += prompts
|
||||
print(' [%2d] %s — instructions=%s prompts=%d kind=%s' % (
|
||||
js.sequence, js.name, '✓' if ins else '✗', prompts, rn.default_kind or '-'
|
||||
))
|
||||
|
||||
print('\nSummary:')
|
||||
print(' Steps with instructions:', instructions_visible, '/', len(job_steps))
|
||||
print(' Total prompts visible to operators:', prompts_visible)
|
||||
print('\n========== URLs to verify ==========')
|
||||
print('Recipe: /odoo/action-fusion_plating.action_fp_process_node/%d' % recipe.id)
|
||||
print('Sale Order:/odoo/sales/%d' % so.id)
|
||||
print('Job: /odoo/action-fusion_plating_jobs.action_fp_job/%d' % job.id)
|
||||
print(' → click any step → "Mark Done" → operator wizard should show:')
|
||||
print(' - Step description (instructions for operator)')
|
||||
print(' - All %d collect=True prompts as input fields' % prompts_visible)
|
||||
Reference in New Issue
Block a user