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:
@@ -348,22 +348,30 @@ class FpProcessNode(models.Model):
|
|||||||
)
|
)
|
||||||
default_kind = fields.Selection(
|
default_kind = fields.Selection(
|
||||||
[
|
[
|
||||||
('cleaning', 'Cleaning'),
|
('receiving', 'Receiving / Incoming Inspection'),
|
||||||
('etch', 'Etch'),
|
('contract_review', 'Contract Review (QA-005)'),
|
||||||
('rinse', 'Rinse'),
|
|
||||||
('plate', 'Plating'),
|
|
||||||
('bake', 'Bake'),
|
|
||||||
('inspect', 'Inspection'),
|
|
||||||
('racking', 'Racking'),
|
('racking', 'Racking'),
|
||||||
('derack', 'De-Racking'),
|
|
||||||
('mask', 'Masking'),
|
('mask', 'Masking'),
|
||||||
('demask', 'De-Masking'),
|
('cleaning', 'Cleaning'),
|
||||||
('dry', 'Drying'),
|
('electroclean', 'Electroclean'),
|
||||||
|
('etch', 'Etch / Activation'),
|
||||||
|
('rinse', 'Rinse'),
|
||||||
|
('strike', 'Strike (Wood\'s Nickel / Activation)'),
|
||||||
|
('plate', 'Plating'),
|
||||||
|
('replenishment', 'Tank Replenishment'),
|
||||||
('wbf_test', 'Water Break Free Test'),
|
('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'),
|
('final_inspect', 'Final Inspection'),
|
||||||
|
('packaging', 'Packaging / Pre-Ship'),
|
||||||
('ship', 'Shipping'),
|
('ship', 'Shipping'),
|
||||||
('gating', 'Gating'),
|
('gating', 'Gating'),
|
||||||
('contract_review', 'Contract Review (QA-005)'),
|
|
||||||
],
|
],
|
||||||
string='Step Kind',
|
string='Step Kind',
|
||||||
)
|
)
|
||||||
|
|||||||
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)
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Native Jobs',
|
'name': 'Fusion Plating — Native Jobs',
|
||||||
'version': '19.0.8.12.0',
|
'version': '19.0.8.13.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
||||||
'author': 'Nexa Systems Inc.',
|
'author': 'Nexa Systems Inc.',
|
||||||
|
|||||||
@@ -59,11 +59,31 @@ class FpJobStepInputWizard(models.TransientModel):
|
|||||||
job_id = fields.Many2one(
|
job_id = fields.Many2one(
|
||||||
related='step_id.job_id', string='Job', store=False, readonly=True,
|
related='step_id.job_id', string='Job', store=False, readonly=True,
|
||||||
)
|
)
|
||||||
|
# Sub 12d — surface the office-authored instructions to the operator
|
||||||
|
# at the exact moment they're recording values. Sourced from the
|
||||||
|
# recipe node's description (rich-text); empty when the recipe
|
||||||
|
# author left it blank.
|
||||||
|
instructions = fields.Html(
|
||||||
|
string='Operator Instructions',
|
||||||
|
compute='_compute_instructions',
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
has_instructions = fields.Boolean(
|
||||||
|
compute='_compute_instructions',
|
||||||
|
)
|
||||||
line_ids = fields.One2many(
|
line_ids = fields.One2many(
|
||||||
'fp.job.step.input.wizard.line', 'wizard_id',
|
'fp.job.step.input.wizard.line', 'wizard_id',
|
||||||
string='Inputs',
|
string='Inputs',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@api.depends('step_id', 'step_id.recipe_node_id', 'step_id.recipe_node_id.description')
|
||||||
|
def _compute_instructions(self):
|
||||||
|
for rec in self:
|
||||||
|
node = rec.step_id.recipe_node_id if rec.step_id else False
|
||||||
|
html = (node and node.description) or ''
|
||||||
|
rec.instructions = html
|
||||||
|
rec.has_instructions = bool(html and html.strip())
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def default_get(self, fields_list):
|
def default_get(self, fields_list):
|
||||||
defaults = super().default_get(fields_list)
|
defaults = super().default_get(fields_list)
|
||||||
|
|||||||
@@ -11,6 +11,17 @@
|
|||||||
<field name="step_id" readonly="1"/>
|
<field name="step_id" readonly="1"/>
|
||||||
<field name="job_id" readonly="1"/>
|
<field name="job_id" readonly="1"/>
|
||||||
</group>
|
</group>
|
||||||
|
<field name="has_instructions" invisible="1"/>
|
||||||
|
<div class="alert alert-info"
|
||||||
|
role="alert"
|
||||||
|
invisible="not has_instructions"
|
||||||
|
style="margin-bottom: 12px;">
|
||||||
|
<h4 style="margin-top: 0;">
|
||||||
|
<i class="fa fa-info-circle"/>
|
||||||
|
Instructions for this step
|
||||||
|
</h4>
|
||||||
|
<field name="instructions" nolabel="1" readonly="1"/>
|
||||||
|
</div>
|
||||||
<separator string="Measurements"/>
|
<separator string="Measurements"/>
|
||||||
<p class="text-muted" invisible="line_ids">
|
<p class="text-muted" invisible="line_ids">
|
||||||
Click <strong>Add a line</strong> to record one or
|
Click <strong>Add a line</strong> to record one or
|
||||||
|
|||||||
Reference in New Issue
Block a user