From ac1db177e13e1fa2ad9e7dc60b31850a86298f9a Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Wed, 20 May 2026 08:08:31 -0400 Subject: [PATCH] feat(step-kinds): curate to 11 + mandatory + admin-only creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator-reported foot-gun: Step Kind dropdown had 24 options, most of which were visual-only (cleaning, electroclean, etch, rinse, strike, dry, wbf_test, hardness_test, adhesion_test, salt_spray, packaging, etc.) and didn't drive any gate or milestone. Picking the wrong one meant nothing happened; picking Generic (left default) meant nothing happened. Authors couldn't tell which choice mattered. Curation: 24 → 11 active kinds. Each remaining kind has a concrete downstream behaviour (gate, portal milestone, hardware tie-in, or "explicitly no behaviour" for Other): other Other (catch-all, default — no special behaviour) receiving Received portal milestone contract_review QA-005 form gate + button_finish lock racking Rack-assignment dialog + button_finish lock mask Visual mask kind (covers Masking + De-Masking) wet_process Visual wet kind (NEW, covers cleaning, rinse, etch, strike, dry, electroclean, wbf_test) plate Plated portal milestone (last plate step closes) bake Bake-window state machine + Baked milestone inspect Intermediate inspection milestone final_inspect Inspected (terminal) portal milestone ship Shipped milestone (back-compat; delivery-state driven is preferred) Retired kinds (active=False, hidden from dropdown): cleaning, electroclean, etch, rinse, strike, dry, wbf_test, demask, derack, replenishment, hardness_test, adhesion_test, salt_spray, packaging, gating. Kept in DB for audit / history but not selectable. Mandatory enforcement: - fp.step.kind_id on fusion.plating.process.node and fp.step.template is now required=True with ondelete='restrict' and a default that resolves to the 'other' kind. Existing NULL rows are backfilled by the pre-migrate before the NOT NULL constraint hits the schema. - Dropdown no longer offers a blank / "Generic" option. New steps land on 'other' instead of NULL. Admin-only catalog: - /fp/simple_recipe/kinds/create endpoint now refuses requests from non-managers (group_fusion_plating_manager). Returns a clear message explaining why ("each kind drives gates / milestones / routing — pick Other if none fits, or ask a manager to wire up a new kind"). - "+ Add a new kind…" sentinel option in the library form is hidden unless state.recipe.user_is_manager. Backend gate is the authority; the UI hide is just to stop showing a button that will error. - The Step Type dropdown in the inline step-edit panel switched from a 24-line hard-coded XML option list to a t-foreach over state.kindOptions (the same kinds/list endpoint payload). One source of truth — retire / add a kind in the catalog and every picker reflects the change. Migration impact (entech): 5 templates + 579 nodes backfilled via name-match heuristic. 15 kinds flipped to active=False. Distribution of the 579 backfilled nodes: racking 105, other 97, bake 91, wet_process 90, mask 74, inspect 44, plate 32, final_inspect 25, receiving 10, contract_review 9, ship 2. Drive-by: - Migration uses _ensure_kind() that also registers ir.model.data for the new xmlids so the subsequent data XML load doesn't create duplicate kind records. - Stored related default_kind on fusion.plating.process.node / fp.step.template is written alongside kind_id in every SQL UPDATE so legacy `node.default_kind == 'foo'` comparisons stay accurate (the ORM doesn't recompute stored related fields after direct SQL writes). Module: fusion_plating 19.0.20.5.0 → 19.0.20.6.0. 15 existing tests still green. Co-Authored-By: Claude Opus 4.7 (1M context) --- fusion_plating/fusion_plating/__manifest__.py | 2 +- .../controllers/simple_recipe_controller.py | 31 ++- .../fusion_plating/data/fp_step_kind_data.xml | 39 ++- .../migrations/19.0.20.6.0/post-migrate.py | 41 +++ .../migrations/19.0.20.6.0/pre-migrate.py | 259 ++++++++++++++++++ .../fusion_plating/models/fp_process_node.py | 18 +- .../fusion_plating/models/fp_step_template.py | 11 +- .../static/src/js/simple_recipe_editor.js | 14 +- .../static/src/xml/simple_recipe_editor.xml | 64 +++-- 9 files changed, 439 insertions(+), 40 deletions(-) create mode 100644 fusion_plating/fusion_plating/migrations/19.0.20.6.0/post-migrate.py create mode 100644 fusion_plating/fusion_plating/migrations/19.0.20.6.0/pre-migrate.py diff --git a/fusion_plating/fusion_plating/__manifest__.py b/fusion_plating/fusion_plating/__manifest__.py index 7c8edac0..dee9ea3b 100644 --- a/fusion_plating/fusion_plating/__manifest__.py +++ b/fusion_plating/fusion_plating/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating', - 'version': '19.0.20.5.0', + 'version': '19.0.20.6.0', 'category': 'Manufacturing/Plating', 'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.', 'description': """ diff --git a/fusion_plating/fusion_plating/controllers/simple_recipe_controller.py b/fusion_plating/fusion_plating/controllers/simple_recipe_controller.py index 8819bfb4..70fee115 100644 --- a/fusion_plating/fusion_plating/controllers/simple_recipe_controller.py +++ b/fusion_plating/fusion_plating/controllers/simple_recipe_controller.py @@ -161,6 +161,11 @@ class SimpleRecipeController(http.Controller): [recipe.process_type_id.id, recipe.process_type_id.name] if recipe.process_type_id else False ), + # 2026-05-20 — drives the visibility of admin-only affordances + # in the Simple Editor (e.g. "+ New kind…" inline create). + 'user_is_manager': request.env.user.has_group( + 'fusion_plating.group_fusion_plating_manager' + ), } def _step_payload(self, step): @@ -499,12 +504,32 @@ class SimpleRecipeController(http.Controller): @http.route('/fp/simple_recipe/kinds/create', type='jsonrpc', auth='user') def kinds_create(self, name, code=''): - """Sub 14b — Inline create for "+ New kind…" in the library - form. Auto-derives a code from the name if blank.""" + """Inline create for "+ New kind…" in the library form. + Auto-derives a code from the name if blank. + + 2026-05-20 lockdown: manager group only. Kinds drive gates, + milestones, and operator routing — a user-created kind with no + corresponding behaviour is a silent foot-gun. The dropdown is + the curated catalog; adding a new kind requires manager + approval and follow-up code work to wire the new code into the + downstream behaviour map. + """ Kind = request.env['fp.step.kind'] if not name or not name.strip(): return {'ok': False, 'error': 'name_required'} - # check_access via create attempt — supervisors+ allowed (ACL). + if not request.env.user.has_group( + 'fusion_plating.group_fusion_plating_manager' + ): + return { + 'ok': False, 'error': 'forbidden', + 'message': ( + 'Only Plating Managers can add new Step Kinds. The ' + 'catalog is curated because each kind drives gates, ' + 'milestones, and operator routing. Pick "Other" if ' + 'no existing kind fits — or ask a manager to add the ' + 'new kind once the downstream behaviour is wired up.' + ), + } if not code: code = name.strip().lower().replace(' ', '_').replace('/', '_') existing = Kind.search([('code', '=', code)], limit=1) diff --git a/fusion_plating/fusion_plating/data/fp_step_kind_data.xml b/fusion_plating/fusion_plating/data/fp_step_kind_data.xml index a6da0c30..1203e68b 100644 --- a/fusion_plating/fusion_plating/data/fp_step_kind_data.xml +++ b/fusion_plating/fusion_plating/data/fp_step_kind_data.xml @@ -1,11 +1,42 @@ - + noupdate=1 so user edits to defaults survive `-u`. + + 2026-05-20 curation (19.0.20.6.0): + - Cut from 24 → 12 active kinds. The dropped ones + (cleaning, electroclean, etch, rinse, strike, dry, + wbf_test, demask, derack, replenishment, hardness_test, + adhesion_test, salt_spray, packaging, gating) are kept + in this XML for history but flipped active=False by the + migration script so they no longer appear in the + dropdown — and bulk-remapped onto the new `other` / + `wet_process` kinds. + - New: `other` (catch-all, default) and `wet_process` + (covers all bath-based steps). + - `mask` covers Masking + De-Masking, `racking` covers + Racking + De-Racking — operators differentiate by the + step name. --> + + + + + + + other + Other + 5 + fa-circle-o + + + + wet_process + Wet Process (Clean / Rinse / Etch / Dry / etc.) + 55 + fa-tint + receiving diff --git a/fusion_plating/fusion_plating/migrations/19.0.20.6.0/post-migrate.py b/fusion_plating/fusion_plating/migrations/19.0.20.6.0/post-migrate.py new file mode 100644 index 00000000..3f327dfe --- /dev/null +++ b/fusion_plating/fusion_plating/migrations/19.0.20.6.0/post-migrate.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) +# +# 2026-05-20 Step Kind curation — post-migrate. +# +# Runs AFTER the schema settles. Marks the 15 retired kinds inactive so +# they no longer appear in the dropdown. We keep them in the DB rather +# than deleting because: +# - ir.model.data rows would dangle and break a future re-import +# - audit trail / reports may still reference them by code +# - users who undo the curation get one switch back to active=True +# +# Pre-migrate has already re-mapped every template + node pointing at +# these kinds, so flipping active=False has no operator-facing data +# impact — it only hides them from pickers. + +import logging + +_logger = logging.getLogger(__name__) + +_RETIRED_CODES = [ + 'cleaning', 'electroclean', 'etch', 'rinse', 'strike', 'dry', + 'wbf_test', 'demask', 'derack', 'replenishment', 'hardness_test', + 'adhesion_test', 'salt_spray', 'packaging', 'gating', +] + + +def migrate(cr, version): + cr.execute(""" + UPDATE fp_step_kind + SET active = false + WHERE code = ANY(%s) + AND active = true + """, (_RETIRED_CODES,)) + n = cr.rowcount + if n: + _logger.info( + 'Step Kind curation: retired %d kinds (active=False): %s', + n, _RETIRED_CODES, + ) diff --git a/fusion_plating/fusion_plating/migrations/19.0.20.6.0/pre-migrate.py b/fusion_plating/fusion_plating/migrations/19.0.20.6.0/pre-migrate.py new file mode 100644 index 00000000..a9eb65a5 --- /dev/null +++ b/fusion_plating/fusion_plating/migrations/19.0.20.6.0/pre-migrate.py @@ -0,0 +1,259 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) +# +# 2026-05-20 Step Kind curation — pre-migrate. +# +# Runs BEFORE the model schema is applied so `kind_id` can become +# required=True without choking on existing NULL rows. Three jobs: +# +# 1. Ensure the new `other` and `wet_process` kinds exist in the DB. +# The data XML hasn't loaded yet at pre-migrate time, so we SQL +# them in directly. The XML on the install path will see them and +# skip via noupdate. +# +# 2. Re-map every template + recipe-node pointing at a RETIRED kind +# to its new home: +# cleaning, electroclean, etch, rinse, strike, dry, wbf_test +# → wet_process +# plate +# → wet_process (but we KEEP `plate` separately for the +# Plated milestone trigger; only auto-remap when the +# caller explicitly wants to retire it. Plate stays +# active.) +# demask → mask +# derack → racking +# replenishment, hardness_test, adhesion_test, salt_spray, +# packaging, gating +# → other +# +# 3. Backfill every NULL kind_id via name-matching heuristic. Anything +# that doesn't match → 'other'. +# +# After this script the schema can safely add NOT NULL to kind_id. + +import logging + +_logger = logging.getLogger(__name__) + + +# -- Remap table — retired-kind code -> new-kind code ---------------------- +# IMPORTANT: `plate` stays active (its own milestone trigger). Only the +# wet-bath specialisations roll up into wet_process. +_REMAP = { + 'cleaning': 'wet_process', + 'electroclean': 'wet_process', + 'etch': 'wet_process', + 'rinse': 'wet_process', + 'strike': 'wet_process', + 'dry': 'wet_process', + 'wbf_test': 'wet_process', + 'demask': 'mask', + 'derack': 'racking', + 'replenishment': 'other', + 'hardness_test': 'other', + 'adhesion_test': 'other', + 'salt_spray': 'other', + 'packaging': 'other', + 'gating': 'other', +} + +# -- Name-match heuristic for NULL backfill -------------------------------- +# Each rule: (substring to match in lower(name), target kind code). First +# match wins. Order matters — more specific patterns come first. +_NAME_HEURISTIC = [ + # Most specific + ('qa-005', 'contract_review'), + ('contract review', 'contract_review'), + ('final inspect', 'final_inspect'), + ('final inspection', 'final_inspect'), + ('post plate inspect', 'final_inspect'), + # Bake / oven + ('bake', 'bake'), + ('oven', 'bake'), + ('he relief', 'bake'), + ('embrittlement', 'bake'), + ('stress relief', 'bake'), + # Receiving / shipping + ('receiv', 'receiving'), + ('incoming inspect', 'receiving'), + ('ship', 'ship'), + ('pack', 'ship'), + # Racking + ('de-rack', 'racking'), + ('deracking', 'racking'), + ('derack', 'racking'), + ('rack', 'racking'), + # Masking + ('de-mask', 'mask'), + ('demask', 'mask'), + ('unmask', 'mask'), + ('mask', 'mask'), + # Inspection + ('inspect', 'inspect'), + # Plating + ('plate', 'plate'), + ('plating', 'plate'), + ('nickel', 'plate'), + ('chrome', 'plate'), + ('anodi', 'plate'), + # Wet processes (broad) + ('soak clean', 'wet_process'), + ('electroclean', 'wet_process'), + ('clean', 'wet_process'), + ('rinse', 'wet_process'), + ('etch', 'wet_process'), + ('activ', 'wet_process'), + ('strike', 'wet_process'), + ('desmut', 'wet_process'), + ('zincate', 'wet_process'), + ('acid', 'wet_process'), + ('dry', 'wet_process'), + ('water break', 'wet_process'), + ('wbf', 'wet_process'), + # Gating / ready / wait — soft sequencers, no behaviour + ('ready for', 'other'), + ('ready to', 'other'), +] + + +def migrate(cr, version): + # 1. Ensure `other` and `wet_process` exist. Use SQL directly so + # we don't depend on the XML having loaded yet. + _ensure_kind(cr, 'other', 'Other', 'fa-circle-o', 5) + _ensure_kind(cr, 'wet_process', 'Wet Process (Clean / Rinse / Etch / Dry / etc.)', 'fa-tint', 55) + + # 2. Build a code → id map for ALL kinds present in DB. + cr.execute("SELECT id, code FROM fp_step_kind") + by_code = {code: kid for kid, code in cr.fetchall()} + if 'other' not in by_code: + _logger.error('pre-migrate: `other` kind missing after _ensure_kind — aborting') + return + other_id = by_code['other'] + + # 3. Re-map references to retired kinds. + # `default_kind` is a stored related on `kind_id.code` — updating + # kind_id via SQL doesn't auto-recompute the stored copy, so we + # write both columns together. + for retired_code, new_code in _REMAP.items(): + retired_id = by_code.get(retired_code) + new_id = by_code.get(new_code) or other_id + if not retired_id: + continue # not in this DB — nothing to remap + cr.execute(""" + UPDATE fp_step_template + SET kind_id = %s, default_kind = %s + WHERE kind_id = %s + """, (new_id, new_code, retired_id)) + tpl_n = cr.rowcount + cr.execute(""" + UPDATE fusion_plating_process_node + SET kind_id = %s, default_kind = %s + WHERE kind_id = %s + """, (new_id, new_code, retired_id)) + node_n = cr.rowcount + if tpl_n or node_n: + _logger.info( + 'Step Kind curation: remapped %d template(s) + %d node(s) ' + 'from %s → %s', tpl_n, node_n, retired_code, new_code, + ) + + # 4. Backfill NULL kind_id on both tables via name heuristic. + # `name` is jsonb on fp_step_template (translatable in Odoo 19) but + # plain varchar on fusion_plating_process_node. Sniff the column + # type so the right expression is used. + for table in ('fp_step_template', 'fusion_plating_process_node'): + cr.execute(""" + SELECT data_type FROM information_schema.columns + WHERE table_name = %s AND column_name = 'name' + """, (table,)) + row = cr.fetchone() + col_type = (row[0] if row else '') or '' + if 'json' in col_type.lower(): + name_expr = "COALESCE(name->>'en_US', name::text)" + else: + name_expr = 'name' + cr.execute(f""" + SELECT id, {name_expr} AS name_str + FROM {table} + WHERE kind_id IS NULL + """) + rows = cr.fetchall() + if not rows: + continue + # In-process classification to avoid pummelling the DB with + # one UPDATE per row. + per_kind = {} # kind_id → list of row ids + for rid, raw_name in rows: + target_code = _classify_by_name(raw_name) + target_id = by_code.get(target_code) or other_id + per_kind.setdefault(target_id, []).append(rid) + # Build a kid → code lookup so we can write default_kind together. + by_id = {kid: code for code, kid in by_code.items()} + for kid, ids in per_kind.items(): + cr.execute( + f"UPDATE {table} SET kind_id = %s, default_kind = %s " + f"WHERE id = ANY(%s)", + (kid, by_id.get(kid, 'other'), ids), + ) + _logger.info( + 'Step Kind curation: backfilled %d %s row(s) — ' + 'distribution: %s', + len(rows), table, + {next(c for c, i in by_code.items() if i == k): len(v) + for k, v in per_kind.items()}, + ) + + +def _classify_by_name(name): + """Return a step-kind code based on a name match. Falls back to 'other'.""" + if not name: + return 'other' + lower = name.lower() + for needle, code in _NAME_HEURISTIC: + if needle in lower: + return code + return 'other' + + +def _ensure_kind(cr, code, name, icon, sequence): + """Create the kind via SQL if it doesn't exist yet. Idempotent. + + fp_step_kind.name is a jsonb (translatable) column in Odoo 19, so + we wrap the string in jsonb_build_object('en_US', ...). + + Also registers the ir.model.data entry so the subsequent XML data + load (which runs AFTER pre-migrate) sees the xmlid as already + bound and skips creation — otherwise we get duplicate records. + """ + cr.execute("SELECT id FROM fp_step_kind WHERE code = %s", (code,)) + row = cr.fetchone() + if row: + kid = row[0] + else: + cr.execute(""" + INSERT INTO fp_step_kind (code, name, sequence, icon, active, + create_uid, create_date, write_uid, write_date) + VALUES (%s, jsonb_build_object('en_US', %s::text), %s, %s, true, + 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') + RETURNING id + """, (code, name, sequence, icon)) + kid = cr.fetchone()[0] + _logger.info('Step Kind curation: created kind %s (id=%s)', code, kid) + + # Bind the xmlid so XML noupdate=1 finds the record on next load. + xmlid_name = 'step_kind_%s' % code + cr.execute(""" + SELECT id FROM ir_model_data + WHERE module = 'fusion_plating' AND name = %s + """, (xmlid_name,)) + if cr.fetchone(): + return + cr.execute(""" + INSERT INTO ir_model_data (module, name, model, res_id, noupdate, + create_uid, create_date, write_uid, write_date) + VALUES ('fusion_plating', %s, 'fp.step.kind', %s, true, + 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') + """, (xmlid_name, kid)) + _logger.info('Step Kind curation: bound xmlid fusion_plating.%s -> id %s', + xmlid_name, kid) diff --git a/fusion_plating/fusion_plating/models/fp_process_node.py b/fusion_plating/fusion_plating/models/fp_process_node.py index cc7d7905..e4e724bd 100644 --- a/fusion_plating/fusion_plating/models/fp_process_node.py +++ b/fusion_plating/fusion_plating/models/fp_process_node.py @@ -493,9 +493,23 @@ class FpProcessNode(models.Model): help='Sub 12b — opens the transition form before Mark Done.', ) # Sub 14b — User-extensible Step Kinds (was Selection of 24). + # 2026-05-20: required + ondelete='restrict' — kind drives gates, + # workflow milestones, and operator routing. Optional was a foot-gun + # (operators silently picked Generic / nothing). Pre-migrate + # 19.0.20.6.0 backfills every existing row before this NOT NULL + # constraint hits the schema. kind_id = fields.Many2one( - 'fp.step.kind', string='Step Kind', ondelete='set null', index=True, - help='Pick from the catalog or create a new kind.', + 'fp.step.kind', string='Step Kind', + ondelete='restrict', index=True, + required=True, + default=lambda self: self.env['fp.step.kind'].search( + [('code', '=', 'other')], limit=1, + ).id or False, + help='Drives operator routing (auto-open Contract Review form / ' + 'Rack assignment dialog / Bake window), customer-portal ' + 'milestones (Received / Plated / Inspected / Shipped), and ' + 'tablet UI (icon, station filter). Pick "Other" only when ' + 'the step has no special behaviour.', ) # Back-compat: code-string accessor that all legacy # `node.default_kind == "cleaning"` comparisons keep using. diff --git a/fusion_plating/fusion_plating/models/fp_step_template.py b/fusion_plating/fusion_plating/models/fp_step_template.py index fd171722..5823812b 100644 --- a/fusion_plating/fusion_plating/models/fp_step_template.py +++ b/fusion_plating/fusion_plating/models/fp_step_template.py @@ -89,11 +89,18 @@ class FpStepTemplate(models.Model): help='Opens the transition form before Mark Done (Sub 12b).') # Sub 14b — User-extensible Step Kinds (was Selection of 24). + # 2026-05-20: required — same rationale as on fusion.plating.process.node + # (kind drives every downstream gate / milestone / routing decision). kind_id = fields.Many2one( 'fp.step.kind', string='Step Kind', ondelete='restrict', index=True, tracking=True, - help='Pick from the catalog or create a new kind. Drives sane-' - 'default input seeding.', + required=True, + default=lambda self: self.env['fp.step.kind'].search( + [('code', '=', 'other')], limit=1, + ).id or False, + help='Drives sane-default input seeding plus downstream gates / ' + 'milestones / routing when authors instantiate the template. ' + 'Pick "Other" only when the step has no special behaviour.', ) # Back-compat shim — every legacy `tpl.default_kind == "cleaning"` # call site keeps working without a refactor. Stored=True so existing diff --git a/fusion_plating/fusion_plating/static/src/js/simple_recipe_editor.js b/fusion_plating/fusion_plating/static/src/js/simple_recipe_editor.js index f8fd9b40..5b68f83a 100644 --- a/fusion_plating/fusion_plating/static/src/js/simple_recipe_editor.js +++ b/fusion_plating/fusion_plating/static/src/js/simple_recipe_editor.js @@ -383,7 +383,10 @@ export class FpSimpleRecipeEditor extends Component { name: name.trim(), }); if (!data.ok) { - alert(data.error || "Could not create Step Kind."); + // 2026-05-20 — backend forbids non-managers from + // creating kinds. Surface the explanatory message + // instead of a generic error code. + alert(data.message || data.error || "Could not create Step Kind."); return; } // Drop the cached list so the next ensure() refetches it. @@ -697,11 +700,18 @@ export class FpSimpleRecipeEditor extends Component { // Sub 14 — make sure the workflow-state catalog is cached so // the dropdown in the inline form has options to render. await this._fpEnsureWorkflowStatesLoaded(); + // 2026-05-20 — Step Type dropdown is now driven by the + // fp.step.kind catalog (curated to 12 active kinds). Cache the + // list before opening the panel so the select renders with + // options instead of being empty. + await this._fpEnsureKindOptionsLoaded(); this.state.editingStepId = stepId; this.state.editName = step.name || ""; this.state.editInstructions = this._htmlToText(step.description || ""); // Settings the user can now change WITHOUT delete + re-add. - this.state.editDefaultKind = step.default_kind || ""; + // Default to 'other' when no kind is set — kind_id is required + // on the model so we never want a blank value to round-trip. + this.state.editDefaultKind = step.default_kind || "other"; this.state.editTriggersWorkflowStateId = step.triggers_workflow_state_id || false; this.state.editParallelStart = !!step.parallel_start; diff --git a/fusion_plating/fusion_plating/static/src/xml/simple_recipe_editor.xml b/fusion_plating/fusion_plating/static/src/xml/simple_recipe_editor.xml index 7657730c..11a491dc 100644 --- a/fusion_plating/fusion_plating/static/src/xml/simple_recipe_editor.xml +++ b/fusion_plating/fusion_plating/static/src/xml/simple_recipe_editor.xml @@ -157,34 +157,40 @@ below it lets them override per-step. -->
- + +

- Drives workflow milestone triggers (e.g. final_inspect fires - the Inspected status) and routing (e.g. contract_review opens - QA-005 instead of the input wizard). + Required. Drives operator routing + (contract_review opens + QA-005, racking opens + rack picker, bake ties + to bake-window state machine), + customer-portal milestones + (receiving / plate + / final_inspect / + ship), and tablet UI + (icon, station-type filter). Pick + Other only when the + step has no special behaviour.

@@ -500,13 +506,19 @@