diff --git a/fusion_plating/fusion_plating/models/fp_process_node.py b/fusion_plating/fusion_plating/models/fp_process_node.py
index 760c7e2b..33e732fc 100644
--- a/fusion_plating/fusion_plating/models/fp_process_node.py
+++ b/fusion_plating/fusion_plating/models/fp_process_node.py
@@ -348,22 +348,30 @@ class FpProcessNode(models.Model):
)
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',
)
diff --git a/fusion_plating/fusion_plating/scripts/bt_create_hard_anodize.py b/fusion_plating/fusion_plating/scripts/bt_create_hard_anodize.py
new file mode 100644
index 00000000..5dc6b0d1
--- /dev/null
+++ b/fusion_plating/fusion_plating/scripts/bt_create_hard_anodize.py
@@ -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': '''
Hard Anodize Type III per MIL-A-8625F
+Aluminum substrate. Black sulfuric dye. Hot nickel acetate seal.
+Tolerances: 0.0015"–0.0020" coating thickness, 60kV breakdown
+voltage. Hardness ≥350HV300.
''',
+})
+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': '''Verify quantity and condition on arrival.
+- Count parts against the packing slip
+- Visually inspect for damage, corrosion, oil residue
+- Photograph any damage or non-conformance
+- Sign off below before parts move to staging
''',
+ },
+ {
+ 'name': '2. Contract Review (QA-005)',
+ 'kind': 'contract_review',
+ 'description': '''Verify the order against quality requirements.
+- Confirm spec matches PO (MIL-A-8625F Type III, Class 2 Black)
+- Cross-check thickness, hardness, salt-spray requirements
+- Open the QA-005 contract review form (separate dialog)
+- Approve only after all line items reconciled
''',
+ },
+ {
+ 'name': '3. Incoming Inspection',
+ 'kind': 'inspect',
+ 'description': '''First-piece inspection before processing.
+- Verify part number against drawing
+- Inspect surface for prior plating remnants, scratches, pitting
+- Photograph any defects
+- Record incoming dimensional spot-check
''',
+ },
+ {
+ 'name': '4. Racking',
+ 'kind': 'racking',
+ 'description': '''Mount parts on titanium rack.
+- Use Rack T-12 or similar (titanium, NOT aluminum)
+- Ensure firm electrical contact at rack hooks
+- Apply masking to threaded holes if required (see drawing)
+- Photograph the loaded rack before submerging
''',
+ },
+ {
+ 'name': '5. Alkaline Clean (Tank A-1)',
+ 'kind': 'cleaning',
+ 'description': '''Soak clean — Aluminum Etch Cleaner.
+- Tank: A-1, Bath: ALKCLEAN-1
+- Time: 4–6 minutes
+- Temperature: 140–160°F
+- Confirm titration done within last 24 hours
''',
+ },
+ {
+ 'name': '6. Rinse — Cascade DI (Tank A-2)',
+ 'kind': 'rinse',
+ 'description': '''Triple cascade DI rinse.
+- Tank A-2 (DI rinse, conductivity < 50 µS/cm)
+- Time: 30 seconds, agitate
+- Verify conductivity reading on bath log
''',
+ },
+ {
+ 'name': '7. Etch (Tank A-3)',
+ 'kind': 'etch',
+ 'description': '''Caustic etch — sodium hydroxide bath.
+- Tank: A-3, Bath: ETCH-1
+- Time: 30–90 seconds (per drawing — heavy etch removes 0.0005"/side)
+- Temperature: 130–150°F
+- Concentration: 4–6 oz/gal NaOH
+- HE-risk parts (high-strength) require post-bake — flag accordingly
''',
+ },
+ {
+ 'name': '8. Rinse — Cascade DI (Tank A-4)',
+ 'kind': 'rinse',
+ 'description': 'Triple cascade DI rinse — Tank A-4. 30 sec agitate.
',
+ },
+ {
+ 'name': '9. Desmut / Deoxidize (Tank A-5)',
+ 'kind': 'etch',
+ 'description': '''Acid desmut to remove black smut from etch.
+- Tank: A-5, Bath: DEOX-1 (HNO3-based)
+- Time: 30–60 seconds
+- Temperature: ambient
+- Surface should be water-break-free after this step
''',
+ },
+ {
+ 'name': '10. Rinse — Cascade DI (Tank A-6)',
+ 'kind': 'rinse',
+ 'description': 'Final pre-anodize rinse — Tank A-6. Conductivity must be < 50 µS/cm.
',
+ },
+ {
+ 'name': '11. Hard Anodize Type III (Tank A-9)',
+ 'kind': 'plate',
+ 'description': '''HARD ANODIZE — the primary process step.
+- Tank: A-9, Bath: HARDANO-1 (15% sulfuric acid)
+- Temperature: 28–32°F (chilled bath — confirm chiller is running)
+- Current density: 24–36 ASF
+- Voltage ramp: 0–80V over first 5 minutes
+- Time at voltage: 60 minutes (gives ~0.002" coating)
+- Record amperage every 15 minutes
+- Check thickness midway with Fischerscope on witness coupon
+- If color reading off, halt and call supervisor
''',
+ },
+ {
+ 'name': '12. Rinse — Cold (Tank A-12)',
+ 'kind': 'rinse',
+ 'description': 'Cold cascade rinse to remove sulfuric residue. Tank A-12.
',
+ },
+ {
+ 'name': '13. Black Dye Bath (Tank A-14)',
+ 'kind': 'plate',
+ 'description': '''Sulfo Black BL dye absorption.
+- Tank: A-14, Bath: DYE-BL-1
+- Temperature: 130–150°F
+- Time: 12–18 minutes
+- Maintain pH 5.0–6.0
+- Visually verify uniform black with no streaks before sealing
''',
+ },
+ {
+ 'name': '14. Rinse — Warm (Tank A-15)',
+ 'kind': 'rinse',
+ 'description': 'Warm rinse before sealing. Tank A-15. ~110°F.
',
+ },
+ {
+ 'name': '15. Hot Nickel Acetate Seal (Tank A-16)',
+ 'kind': 'bake',
+ 'description': '''Nickel acetate seal — locks in dye, improves corrosion resistance.
+- Tank: A-16, Bath: SEAL-NA-1
+- Temperature: 195–205°F
+- Time: 18–22 minutes
+- pH: 5.5–6.0
+- Attach AMS-2759 chart-recorder file as photo before unloading
+- Quality of seal verified post-process by dye absorption test
''',
+ },
+ {
+ 'name': '16. Hot DI Rinse (Tank A-17)',
+ 'kind': 'rinse',
+ 'description': 'Final hot DI rinse. Tank A-17. 180°F+. Drives off residual seal solution.
',
+ },
+ {
+ 'name': '17. Drying (Hot Air Knife)',
+ 'kind': 'dry',
+ 'description': '''Hot-air knife dry — leave parts on rack.
+- Hot air knife @ 180°F
+- Time: 5 minutes minimum
+- Verify parts fully dry before unracking — water spotting is a defect
''',
+ },
+ {
+ 'name': '18. De-Racking',
+ 'kind': 'derack',
+ 'description': '''Remove parts from rack carefully.
+- Wear lint-free gloves
+- Inspect each part for rack-mark touch-up needs
+- Place on padded staging cart
''',
+ },
+ {
+ 'name': '19. Final Visual Inspection',
+ 'kind': 'final_inspect',
+ 'description': '''Visual + dimensional + thickness QC.
+- Visual: uniform black, no streaks, scratches, or burn
+- Dimensional: spot-check 3 critical dims per part
+- Thickness: 5-point Fischerscope reading per drawing locations
+- Photograph any defects, route to NCR if rework needed
+- Inspector signs off below
''',
+ },
+ {
+ 'name': '20. Microhardness Test (Witness Coupon)',
+ 'kind': 'hardness_test',
+ 'description': '''HV300 hardness measurement on witness coupon.
+- Equipment: LECO LM247AT microhardness tester (verify cal date current)
+- Load: 300 gf, dwell 10s
+- Take 3 indents, log each + average
+- Spec: ≥350 HV300 (MIL-A-8625F Type III)
''',
+ },
+ {
+ 'name': '21. Salt Spray (Witness, ASTM B117)',
+ 'kind': 'salt_spray',
+ 'description': '''336-hour salt spray on witness coupon.
+- Submit 3 coupons to lab
+- Test duration: 336 hours minimum
+- Acceptance: zero red rust, < 5% white corrosion
+- Attach lab certificate when received
''',
+ },
+ {
+ 'name': '22. Packaging',
+ 'kind': 'packaging',
+ 'description': '''Wrap and stage for shipment.
+- Wrap each part in VCI paper
+- Place in foam-lined box, max 4 parts per box
+- Include CoC + dimensional report inside the box
+- Seal with company tape
''',
+ },
+ {
+ 'name': '23. Shipping',
+ 'kind': 'ship',
+ 'description': '''Outbound — confirm carrier and BoL.
+- Carrier per SO (UPS / FedEx / customer pickup)
+- Print BoL, attach to package
+- Photograph sealed shipment for proof-of-shipment
+- Update tracking # in this SO
''',
+ },
+]
+
+# ----- 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)
diff --git a/fusion_plating/fusion_plating_jobs/__manifest__.py b/fusion_plating/fusion_plating_jobs/__manifest__.py
index 9d2f1389..7cce258d 100644
--- a/fusion_plating/fusion_plating_jobs/__manifest__.py
+++ b/fusion_plating/fusion_plating_jobs/__manifest__.py
@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
{
'name': 'Fusion Plating — Native Jobs',
- 'version': '19.0.8.12.0',
+ 'version': '19.0.8.13.0',
'category': 'Manufacturing/Plating',
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
'author': 'Nexa Systems Inc.',
diff --git a/fusion_plating/fusion_plating_jobs/wizards/fp_job_step_input_wizard.py b/fusion_plating/fusion_plating_jobs/wizards/fp_job_step_input_wizard.py
index f8e0bff7..c99ae184 100644
--- a/fusion_plating/fusion_plating_jobs/wizards/fp_job_step_input_wizard.py
+++ b/fusion_plating/fusion_plating_jobs/wizards/fp_job_step_input_wizard.py
@@ -59,11 +59,31 @@ class FpJobStepInputWizard(models.TransientModel):
job_id = fields.Many2one(
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(
'fp.job.step.input.wizard.line', 'wizard_id',
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
def default_get(self, fields_list):
defaults = super().default_get(fields_list)
diff --git a/fusion_plating/fusion_plating_jobs/wizards/fp_job_step_input_wizard_views.xml b/fusion_plating/fusion_plating_jobs/wizards/fp_job_step_input_wizard_views.xml
index 54bca05e..ef476b4f 100644
--- a/fusion_plating/fusion_plating_jobs/wizards/fp_job_step_input_wizard_views.xml
+++ b/fusion_plating/fusion_plating_jobs/wizards/fp_job_step_input_wizard_views.xml
@@ -11,6 +11,17 @@
+
+
+
+
+ Instructions for this step
+
+
+
Click Add a line to record one or