feat(step-kinds): curate to 11 + mandatory + admin-only creation
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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': """
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,11 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- 24 seeded Step Kinds — XML IDs use the original Selection
|
||||
keys so post-migrate can map old default_kind = 'cleaning'
|
||||
to env.ref('fusion_plating.step_kind_cleaning').
|
||||
<!-- Step Kind catalog.
|
||||
|
||||
noupdate=1 so user edits to defaults survive `-u`. -->
|
||||
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. -->
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- ACTIVE KINDS — visible in dropdown -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<record id="step_kind_other" model="fp.step.kind">
|
||||
<field name="code">other</field>
|
||||
<field name="name">Other</field>
|
||||
<field name="sequence">5</field>
|
||||
<field name="icon">fa-circle-o</field>
|
||||
</record>
|
||||
|
||||
<record id="step_kind_wet_process" model="fp.step.kind">
|
||||
<field name="code">wet_process</field>
|
||||
<field name="name">Wet Process (Clean / Rinse / Etch / Dry / etc.)</field>
|
||||
<field name="sequence">55</field>
|
||||
<field name="icon">fa-tint</field>
|
||||
</record>
|
||||
|
||||
<record id="step_kind_receiving" model="fp.step.kind">
|
||||
<field name="code">receiving</field>
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -157,34 +157,40 @@
|
||||
below it lets them override per-step. -->
|
||||
<div class="o_fp_edit_row" style="display: flex; gap: 16px; flex-wrap: wrap;">
|
||||
<div class="o_fp_edit_field" style="flex: 1; min-width: 240px;">
|
||||
<label>Step Type (Default Kind)</label>
|
||||
<label>Step Type (Default Kind) *</label>
|
||||
<!-- 2026-05-20: hard-coded option list
|
||||
retired. The dropdown now drives
|
||||
off `state.kindOptions` (fp.step.kind
|
||||
records with active=True), which is
|
||||
the curated catalog (Other,
|
||||
Receiving, Contract Review, Racking,
|
||||
Masking, Wet Process, Plating, Bake,
|
||||
Inspection, Final Inspection,
|
||||
Shipping). New kinds need a manager
|
||||
+ code work to wire downstream gates;
|
||||
see kinds_create lockdown. -->
|
||||
<select class="form-select"
|
||||
t-on-change="(ev) => { state.editDefaultKind = ev.target.value; }">
|
||||
<option value="" t-att-selected="!state.editDefaultKind">— Generic —</option>
|
||||
<option value="receiving" t-att-selected="state.editDefaultKind === 'receiving'">Receiving / Incoming Inspection</option>
|
||||
<option value="contract_review" t-att-selected="state.editDefaultKind === 'contract_review'">Contract Review (QA-005)</option>
|
||||
<option value="racking" t-att-selected="state.editDefaultKind === 'racking'">Racking</option>
|
||||
<option value="mask" t-att-selected="state.editDefaultKind === 'mask'">Masking</option>
|
||||
<option value="cleaning" t-att-selected="state.editDefaultKind === 'cleaning'">Cleaning</option>
|
||||
<option value="electroclean" t-att-selected="state.editDefaultKind === 'electroclean'">Electroclean</option>
|
||||
<option value="etch" t-att-selected="state.editDefaultKind === 'etch'">Etch / Activation</option>
|
||||
<option value="rinse" t-att-selected="state.editDefaultKind === 'rinse'">Rinse</option>
|
||||
<option value="strike" t-att-selected="state.editDefaultKind === 'strike'">Strike</option>
|
||||
<option value="plate" t-att-selected="state.editDefaultKind === 'plate'">Plating</option>
|
||||
<option value="replenishment" t-att-selected="state.editDefaultKind === 'replenishment'">Tank Replenishment</option>
|
||||
<option value="wbf_test" t-att-selected="state.editDefaultKind === 'wbf_test'">Water Break Free Test</option>
|
||||
<option value="dry" t-att-selected="state.editDefaultKind === 'dry'">Drying</option>
|
||||
<option value="bake" t-att-selected="state.editDefaultKind === 'bake'">Bake</option>
|
||||
<option value="demask" t-att-selected="state.editDefaultKind === 'demask'">De-Masking</option>
|
||||
<option value="derack" t-att-selected="state.editDefaultKind === 'derack'">De-Racking</option>
|
||||
<option value="inspect" t-att-selected="state.editDefaultKind === 'inspect'">Inspection</option>
|
||||
<option value="final_inspect" t-att-selected="state.editDefaultKind === 'final_inspect'">Final Inspection</option>
|
||||
<option value="ship" t-att-selected="state.editDefaultKind === 'ship'">Shipping</option>
|
||||
<t t-foreach="state.kindOptions || []" t-as="k" t-key="k.id">
|
||||
<option t-att-value="k.code"
|
||||
t-att-selected="k.code === state.editDefaultKind">
|
||||
<t t-esc="k.name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
<p class="o_fp_edit_hint">
|
||||
Drives workflow milestone triggers (e.g. <code>final_inspect</code> fires
|
||||
the Inspected status) and routing (e.g. <code>contract_review</code> opens
|
||||
QA-005 instead of the input wizard).
|
||||
Required. Drives operator routing
|
||||
(<code>contract_review</code> opens
|
||||
QA-005, <code>racking</code> opens
|
||||
rack picker, <code>bake</code> ties
|
||||
to bake-window state machine),
|
||||
customer-portal milestones
|
||||
(<code>receiving</code> / <code>plate</code>
|
||||
/ <code>final_inspect</code> /
|
||||
<code>ship</code>), and tablet UI
|
||||
(icon, station-type filter). Pick
|
||||
<strong>Other</strong> only when the
|
||||
step has no special behaviour.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -500,13 +506,19 @@
|
||||
<select class="form-select"
|
||||
t-on-change="(ev) => this.onKindChange(ev)"
|
||||
t-att-value="state.libraryEditor.default_kind">
|
||||
<option value="">Generic — no automatic behaviour</option>
|
||||
<t t-foreach="state.kindOptions || []" t-as="k" t-key="k.id">
|
||||
<option t-att-value="k.code" t-att-selected="k.code === state.libraryEditor.default_kind">
|
||||
<t t-esc="k.name"/>
|
||||
</option>
|
||||
</t>
|
||||
<option value="__new__">+ Add a new kind…</option>
|
||||
<!-- Manager-only inline create. The
|
||||
backend kinds_create endpoint
|
||||
also gates on this group, so
|
||||
hiding here is just to avoid
|
||||
showing a button that
|
||||
immediately errors. -->
|
||||
<option value="__new__"
|
||||
t-if="state.recipe and state.recipe.user_is_manager">+ Add a new kind…</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="o_fp_le_field">
|
||||
|
||||
Reference in New Issue
Block a user