Reported 2026-05-20: on a 40+ step recipe (e.g. ENP-STEEL-BASIC),
scrolling down into the Selected steps pane scrolled the Step
Library off the top of the screen. Authors had to scroll back up
to grab a step, then scroll down to drop it.
Fix: position: sticky on .o_fp_library_panel, pinned to top: 1rem
(matches the editor's padding) inside the .o_fp_simple_editor
overflow container. align-items: start on the grid so the library
column doesn't stretch to match the recipe column's height
(prerequisite for sticky to behave).
The library itself can have 30+ entries (curated step kinds +
shop-defined library templates). max-height: calc(100vh - 8rem)
+ overflow-y: auto keeps it from blowing past the viewport — it
grows its own internal scrollbar instead.
Mobile (≤900px) reverts to static positioning so the stacked
layout stays sensible.
Module: fusion_plating 19.0.20.6.1 → 19.0.20.6.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Regression of an earlier fix. Operators reported the editor jumping
to the top of the page on every step save / insert / remove / promote.
Root cause: .o_fp_simple_editor is the overflow:auto scroll
container. loadAll() replaces state.steps with a fresh JSONRPC
payload — OWL tears down the t-foreach and rebuilds every row, which
snaps scrollTop back to 0. Every author action (Save Step, Add
Step, Remove, Promote, Demote, Reorder, Import Template) routes
through loadAll, so the symptom hit everywhere.
Fix: capture scrollTop before the RPC, restore in a double-rAF
after the response settles. rAF (microtask runs before paint in
OWL 2; we need the rebuilt DOM to exist). One choke point fix —
every caller benefits without per-handler changes.
Cheap: a single DOM lookup + an integer save/restore. No XML or
state-shape changes.
Module: fusion_plating 19.0.20.6.0 → 19.0.20.6.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Three bugs reported on 2026-05-20:
1. RESURRECTION. User deletes a substep in the Simple Editor (e.g.
Soak Clean (S-3) under Cleaner), then on the next -u fusion_plating
the substep comes back. Root cause: the recipe XML lived in the
manifest's `data` list with `noupdate="1"`. Odoo's noupdate=1 only
blocks UPDATE of existing records — when a record's ir.model.data
row is missing, the loader treats it as "not yet created" and
re-creates from XML. Every upgrade resurrected every user-deleted
seed node.
Fix: pull the recipe XML files out of `data` and load them once
via post_init_hook → _seed_starter_recipes_once. Sentinel checks
ir.model.data for each recipe's root xmlid; if present, skip
loading entirely. Result: deletions are permanent across all
future upgrades. Existing entech recipes untouched.
Files affected: fp_recipe_enp_alum_basic, fp_recipe_enp_steel_basic,
fp_recipe_enp_sp, fp_recipe_general_processing, fp_recipe_anodize,
fp_recipe_chem_conversion.
2. PROMOTE / DEMOTE. Simple Editor had no way to turn a substep into
a top-level operation, or to tuck an operation under another as a
substep. Authors had to delete + re-create. New endpoints:
* /fp/simple_recipe/step/promote → flips node_type 'step' →
'operation', re-parents to the recipe (or sub-process) root,
places right after the old parent operation.
* /fp/simple_recipe/step/demote → flips 'operation' → 'step',
re-parents under the preceding operation (or a caller-supplied
target_op_id). Blocks demoting an operation that has its own
children, with a helpful message.
UI: each row in the editor now carries an up-arrow (promote, only
shown on substeps) and a down-arrow (demote, only shown on
operations). Confirmation dialog explains what's about to happen.
3. DRAG SUBSTEPS. Last commit (2142a66b) disabled drag on substep
rows. Operators couldn't reorder substeps within an operation.
Re-enabled drag on substeps. The step_reorder endpoint now groups
incoming node_ids by parent_id and renumbers within each parent
(10, 20, 30…). Cross-parent drag still no-ops on parent change —
Promote/Demote buttons are the way to move between parents.
Drive-by:
- Added `from odoo import _` to the controller (missing import the
new endpoints surfaced).
- Edit-panel field wiring audited: all fields visible in the screen
(Step name, Default instructions, Step Type, Triggers Workflow,
Parallel Start, QA Sign-off, Collect measurements, Instruction
Images, custom prompts) persist correctly through step_write or
dedicated endpoints. No broken wires.
Tests: 15 total in TestSimpleRecipeFlatten (was 10). 5 new cover
promote happy-path, promote reject (non-substep), demote happy-path,
demote block on has_children, and reorder parent-scoping.
Module: fusion_plating 19.0.20.4.0 → 19.0.20.5.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to 821e768b. The previous fix flattened sub_process nodes
so all 16 operations of ENP-STEEL-BASIC became visible — but the
Tree Editor also shows the 26 `step` nodes that live under each
operation ("Ready For Blast / Blast", "Soak Clean / Electroclean /
Primary Rinse", etc.). The Simple Editor still hid those, so author
+ Tree Editor still disagreed by 26 rows.
New `_flatten_recipe_nodes(recipe)` helper walks DFS and surfaces
BOTH operations and their step children. Each operation is followed
immediately by its step children in sequence order so the editor
renders them as a contiguous block:
10. Ready For Steel Line
11. Cleaner [Steel Line]
↳ Soak Clean (S-3) [Steel Line › Cleaner]
↳ Electroclean (S-3) [Steel Line › Cleaner]
↳ Primary Rinse (S-4) [Steel Line › Cleaner]
15. Acid Dip (S-5) [Steel Line]
↳ Primary Rinse (S-6) [Steel Line › Acid Dip (S-5)]
...
Payload additions on each step:
- `node_type`: 'operation' | 'step'
- `is_substep`: True for steps (renders indented)
- `nested_under`: chained path (sub-process › operation for substeps,
sub-process for nested operations, '' for top-level operations)
UI: substep rows are indented 2.5rem, smaller font, no drag handle,
no numeric position. The "↳" indent glyph and a "[parent operation]"
chip make the parent-child relationship obvious. Substeps are not
draggable to keep the existing reorder semantics simple — Tree Editor
remains the home for structural changes.
Legacy `_flatten_recipe_operations` helper retained for back-compat
(it now delegates by filtering `node.node_type == 'operation'` from
the full walk).
ENP-STEEL-BASIC on entech: Simple Editor now shows 42 rows (was 10
before 821e768b, was 16 after 821e768b) — matches what the Tree
Editor displays exactly.
Tests: 10 total (was 7), 3 new cover the substep surfacing, path
chaining, and is_substep / node_type flags on the payload.
Module: fusion_plating 19.0.20.3.0 → 19.0.20.4.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug on ENP-STEEL-BASIC (2026-05-20): authoring used the Tree Editor
to build a recipe with a "Steel Line" sub_process holding 7 nested
operations (Cleaner, Acid Dip, Nickel Strike, E-Nickel Plate, etc.).
The Simple Editor's /fp/simple_recipe/load endpoint only walked
`recipe.child_ids`, so it returned 10 steps. The work order generator
(fp.job._generate_steps) walked the same tree depth-first and emitted
16 steps. Author and operator disagreed about what was in the recipe.
Fix: new `_flatten_recipe_operations(recipe)` helper walks the tree
depth-first, recurses into `recipe` and `sub_process`, emits each
`operation` exactly once, skips `step` children (they're sub-
instructions of operations). Mirrors the WO walker.
Step payload now carries a `nested_under` string — the chained sub-
process name(s) the operation lives inside (empty for top-level).
The Simple Editor XML renders that as a small "↳ Steel Line" badge
next to the step name so the author can see where each row came from
in the tree. Deep nesting chains with ' › ' (e.g. "Outer › Inner").
`step` children of `recipe` itself remain invisible — they were
silently skipped by the WO generator pre-19.0.18.8.0 anyway (only
operation nodes spawn fp.job.step rows). Restoring them here would
contradict that long-standing contract.
Edit/insert/reorder/remove endpoints unchanged: editing a nested
operation's name / description / tanks works (no parent change).
Drag-reorder within sub-process siblings still works. Drag across
sub-process boundaries isn't supported — opens the door for a Tree
Editor follow-up if needed, but the immediate "I can't see my
steps" complaint is resolved.
ENP-STEEL-BASIC on entech now shows all 16 operations in the Simple
Editor (was 10), with the 7 inside Steel Line tagged accordingly.
Tests: 7 new (TestSimpleRecipeFlatten) — flat recipes still work,
nested operations surface with correct path label, sub_process
nodes never appear as editor rows, step children of operations
stay hidden, deep-nested sub_processes chain path labels.
Module: fusion_plating 19.0.20.2.0 → 19.0.20.3.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug surfaced on WO-30043 (2026-05-20): operator walked every step
including a fully closed receiving record, then hit
"Quantity Received is blank — close the receiving record for
SO SO-30043 before completing this job." Receiving WAS closed.
Root cause: the 2026-05-18 cert-creation gate
(fp.job.button_mark_done) blocks on job.qty_received but nothing
populated it. fp.receiving carried the qty on its line records,
fp.job stayed at 0 indefinitely. Two disconnected records on the
same SO.
Fix: when fp.receiving._update_so_receiving_status runs (i.e. on
every state transition — counted / staged / closed / accepted /
resolved), also mirror each line's received_qty onto the matching
fp.job by (sale_order_id + part_catalog_id). Single-part SOs map
1-to-1; multi-part SOs spawn one job per line so the same join
still works.
Two defensive guards in the hook:
- Skip silently when fusion_plating_jobs not installed
(Job = env.get('fp.job') returns None).
- Skip silently when fp.job doesn't yet carry part_catalog_id /
qty_received (test scope, unusual install topology).
Drive-by during cleanup:
- fp_parent_numbered_mixin._fp_assign_parent_name: guard
so.x_fc_parent_number access with field-existence check. The
column lives in fusion_plating_jobs; downstream modules that
inherit the mixin (receiving) but don't depend on jobs were
hitting AttributeError on every fp.receiving.create at test
time. Falls through to the legacy sequence when the column
isn't there.
- fp_receiving_views.xml: legacy carrier_name Char field rendered
as a second carrier row labeled "Legacy Carrier" alongside the
proper x_fc_carrier_id M2O — operators saw two carrier fields
and got confused. Hide the legacy display (data stays in DB for
audit; migration 19.0.3.10.0 already matched it to a real
delivery.carrier).
Migration 19.0.3.19.0/post-migrate.py backfills qty_received from
closed receiving lines for any job stuck at 0 — fixes WO-30043
and two sibling jobs on entech.
Modules: fusion_plating 19.0.20.2.0, fusion_plating_receiving
19.0.3.19.0, fusion_plating_jobs 19.0.10.15.0.
All 19 tests green (TestCarrierFields 6, TestQtyReceivedPropagation 5
new, TestReceivingGate 8). Direct verification on entech: WO-30043
qty_received = 1, mark_done succeeds, delivery + cert auto-created.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per client direction: every order is a thickness RANGE (e.g.
"0.0005-0.0008 mils" or "5-10 mils"), never a single value. The
old picker model (fp.recipe.thickness with a single 'value' Float)
was modelling the wrong concept and overcrowding the order entry
UI. Replaced with one free-text Char field that auto-fills from
last-used or part default.
DELETED entirely:
- fp.recipe.thickness model (file + view + ACL + manifest entry)
- recipe.thickness_option_ids One2many (the picker source)
- "Thickness Options" inline list on the recipe form
- sale.order.line.x_fc_thickness_id (M2O picker)
- account.move.line.x_fc_thickness_id
- fp.delivery.x_fc_thickness_id
- fp.direct.order.line.thickness_id
ADDED:
- sale.order.line.x_fc_thickness_range (Char) — operator types range
- account.move.line.x_fc_thickness_range — for invoice rendering
- fp.delivery.x_fc_thickness_range — for packing slip
- fp.direct.order.line.thickness_range — for the wizard
- fp.part.catalog.x_fc_default_thickness_range — part default
AUTO-FILL CHAIN (sale.order.line + wizard line):
1. Operator already typed → keep
2. Most recent SO line for (this part, this customer) with a
non-empty thickness_range → copy that
3. part.x_fc_default_thickness_range → copy
4. Blank — operator types
Implemented as both an @api.onchange (interactive) AND a
create() override (programmatic — wizard, sale_mrp bridge,
imports). Same logic in both paths.
WIZARD push-to-defaults: when "Save as Default" toggle is ticked
on a wizard line, persist the line's thickness_range to
part.x_fc_default_thickness_range so future first-customer orders
get a sensible starting point.
REPORTS: customer_line_header.xml + report_fp_wo_sticker.xml now
print the Char range as-typed (no display_name lookup needed).
KEPT (admin documentation only — doesn't affect order entry):
- recipe.thickness_min, thickness_max, thickness_uom on the recipe
root: documents the recipe's CAPABILITY range. No UI gate; just
for spec authors to record what the chemistry can produce.
JOB GROUPING: fp.job auto-create groups SO lines by (recipe, part,
spec, thickness, serial). Updated to key on the thickness_range
Char (stripped) instead of the deleted thickness_id integer.
DB cleanup: --update=base ran on the upgrade, dropping the
fp_recipe_thickness table + the four x_fc_thickness_id columns.
Existing data was already nulled in earlier dev work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CRITICAL BUG: 5 of 6 seeded recipe files had <data noupdate="0">
which caused EVERY module upgrade to re-import the recipe and
overwrite any user customisations to the base recipe (renamed
steps, added child nodes, custom prompts on seeded steps).
Files fixed (now noupdate="1"):
- fp_recipe_enp_alum_basic.xml
- fp_recipe_enp_steel_basic.xml
- fp_recipe_enp_sp.xml
- fp_recipe_anodize.xml
- fp_recipe_chem_conversion.xml
(fp_recipe_general_processing.xml was already correctly noupdate=1.)
Companion entech-side action (not in this commit, executed via SQL
during the fix session): 200 ir.model.data rows for the affected
process_node + process_node_input records were updated to
noupdate=true so the next module upgrade will skip them entirely
and respect the user's current state.
Recovery for users whose base recipe edits were already lost:
the variants (part-cloned recipes that share the recipe name)
were untouched because they have no XML xmlid match. The
customisations are preserved in the variants and can be lifted
back to the base recipe via the simple/tree editor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Specifications menu (urgent — workflow blocker for estimators):
- Moved from Configuration → Quality & Documents (manager-only) up
to Plating → Quality (sequence 70). Now visible to estimator,
supervisor, and manager.
- Renamed "Customer Specs" → "Specifications" — the seeded library
includes industry standards (AMS, MIL, ASTM, BAC) not just
customer-private specs.
- Action display name updated: "Customer Specifications" → "Specifications".
- Added action.help HTML so the empty-state placeholder explains
the Specifications library purpose to first-time users.
- Old xmlid (menu_fp_config_customer_spec) preserved so existing
links / breadcrumbs / search references continue to resolve.
Other clarifying renames:
- Safety: "JHSC" / "JHSC Meetings" → "H&S Committee (JHSC)" /
"H&S Committee Meetings" — acronym was opaque to non-Canadian
H&S folks.
- Operations: "Move Log" → "Parts & Rack Move Log" — generic name
could be confused with chatter messages or stock moves.
- Configuration → Recipes & Steps: "Workflow States" →
"Job Workflow Stages" — generic name; clarifies these are job
state milestones (passed-stage tracking), not generic workflow.
- Compliance → General: child folder "Configuration" → "Reference
Data" — three levels of "Configuration" nesting (Plating>Config
vs Plating>Compliance>General>Config) was confusing.
No model / data changes. Pure menu metadata.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per client review: NADCAP-qualified recipes need manager-only edit
permission. Word-doc external approval workflow stays outside ERP;
this is the in-app enforcement.
- New field fp.process.node.is_locked (recipe root)
- write() override blocks non-manager edits when recipe root is_locked
Lock checks via recipe_root_id so child ops/steps are also protected
Manager bypass via group + env.su (sudo) bypass for system jobs
- Amber "LOCKED — Manager Edit Only" ribbon at top of recipe form
- Toggle on Specification & Bake page under "Change Control (NADCAP)"
- Spec doc updated with Decision 6.5 + backlog from client review:
approvals list, doc control auto-sync, oven recorder sync, SOP
word-doc workflow, final-inspection signoff on cert
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add fp.recipe.thickness model (replaces fp.coating.thickness, scoped to recipe root)
- Add spec metadata + bake-relief fields to fusion.plating.process.node (recipe root):
phosphorus_level, thickness_min/max/uom, thickness_option_ids,
requires_bake_relief + bake_window_hours/temperature/duration
- Add recipe_ids M2M + print_on_cert to fusion.plating.customer.spec
- Add applicable_spec_ids reverse M2M as inherit in fusion_plating_quality
(avoids circular dep — core can't reference customer.spec which lives in quality)
- Surface new fields on recipe form ("Specification & Bake" notebook page)
- Surface recipe linkage on customer spec form
Pure additive. Foundation for Phases B-E.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four message_post calls were passing strings with HTML tags as
plain `body=_(...)` instead of `body=Markup(_(...))`. Odoo escapes
non-Markup strings, so the chatter rendered "<b>QA Review failed</b>"
as literal text instead of bolding it.
Original bug surfaced via the Contract Review (QA-005) flow:
body: "<b>QA Review failed</b> by Garry Singh. Awaiting
client information.<br/><b>Reason:</b><br/>
<div data-oe-version=\"2.0\">Need to get updated
drawing...</div>"
Audit scan turned up three more identical patterns:
fusion_plating/models/fp_parent_numbered_mixin.py:118
"Issued <strong>%s</strong> to ..."
fusion_plating_jobs/models/sale_order.py:282
"Confirmed quote <strong>%s</strong> as <strong>%s</strong>."
fusion_plating_quality/models/fp_contract_review.py:430
"<b>QA Review failed</b> by ... <b>Reason:</b><br/>%(reason)s"
fusion_plating_quality/models/fp_contract_review.py:524
"<b>QA Review completed</b> by ... <b>Special Instructions
captured:</b><br/>%(notes)s"
Fixes:
- Wrapped each body=_(...) with Markup(_(...)) using the
Markup(template) % values pattern (auto-escapes the substituted
values; user-supplied free text stays safe).
- For Html-field substitutions (qa_failure_reason,
special_instructions), explicitly wrapped the value in Markup()
so already-formatted HTML editor content (with data-oe-version="2.0"
wrapper divs) flows through without being re-escaped.
- Added `from markupsafe import Markup` to the two files that
didn't already import it (mixin + contract_review).
Drift cleanup: pulled the 180-line newer fp_contract_review.py
from entech to the local repo (added action_qa_review_failed,
action_open_client_email_wizard, action_view_client_emails,
action_complete_after_info, awaiting_info state, qa_failure_reason
+ special_instructions Html fields, etc. that had been edited on
entech without being committed).
Tested by re-posting via odoo shell on review 10: body now stores
"<b>QA Review failed</b>..." with literal HTML tags instead of
the double-escaped "<b>..." entities. Old chatter records
with the bad escape stay as-is in the audit trail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- B1: Add Credit Note wizard path was blocked because invoice_origin
has copy=False and the wizard doesn't set fp_from_so_invoice. Now
the validator allows reversals when reversed_entry_id points at a
customer-facing move that itself went through the validator at
original creation time. account.move._fp_parent_sale_order also
walks self.reversed_entry_id._fp_parent_sale_order so the credit
note inherits the parent number (CN-<parent>).
- Bug 1: sale.order.unlink() now blocks deletion when x_fc_parent_number
is set (matches spec §6.2). Draft quotes remain freely deletable
per Odoo standard. Applies to all users including admins.
- Bug 2: out_receipt added to CUSTOMER_TYPES so POS-style receipts
hit the same SO-flow gate as out_invoice / out_refund.
- C1: WO grouping key changed from recipe.id to (recipe.id, part.id,
coating.id). Bundling lines with different parts under one WO put
first_line's part_number on the CoC header — silent compliance
mis-attestation. Now distinct parts always get distinct WOs even
when they share a recipe.
- C3: SQL whitelist (_FP_COUNTER_FIELD_RE) on _fp_assign_parent_name's
interpolated counter field name. No user input today; defence in
depth for future subclasses that might read the name from context.
Verified on entech: parent=30017, credit note = CN-30017,
multi-part SO produces 2 WOs (one per part), confirmed-SO unlink
blocked, out_receipt blocked, whitelist regex enforced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
write() override raises UserError if name or x_fc_doc_index is in
vals and differs from the stored value (bypass: context flag
fp_allow_name_rename=True for the SO-confirm rename + bulk WO
creation paths). unlink() override raises UserError for records
that have been issued a name; applies to all users including
admins — cancellation must go through the state machine.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-model counter fields on sale.order renamed to x_fc_pn_*_count
to avoid collision with pre-existing compute fields of the same
short name in bridge_mrp / receiving / configurator (silent
compute-override was suppressing the storage). 4 child models
(fp.certificate, fp.receiving, fusion.plating.delivery,
fusion.plating.pickup.request) now derive names as PFX-<parent>
with -NN suffix from the 2nd onward.
fusion.plating.pickup.request gains a sale_order_id field
(optional) so pickups created against an SO get parent-derived
names, while standalone pickups (pre-SO) fall back to PU/YYYY/NNNN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces x_fc_wo_group_tag grouping with resolved-recipe grouping.
Bare WO-<parent> when 1 recipe, WO-<parent>-NN zero-padded for N>1
ordered by min line sequence. fp.job inherits parent-numbered mixin
for the manual-add path; bulk SO-confirm sets names explicitly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Atomic counter via SELECT FOR UPDATE on the parent SO row. Composes
child names as PREFIX-PARENT (bare for first) or PREFIX-PARENT-NN
(zero-padded 2-digit, then unpadded past 99). Subclasses implement
three hooks: _fp_parent_sale_order, _fp_name_prefix, _fp_parent_counter_field.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Parent sequence starts at 30000. Quote sequence is Q + YYYYMM + non-resetting
counter starting at 200. Phase 1 Task 1 of the parent-number hierarchy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two compounding issues introduced by the persistence audit work:
1. fields.Text mismatch: sale.order.x_fc_internal_note and
x_fc_external_note are actually fields.Html. I declared the
sale.order.line related mirrors and the fp.job stored copies as
fields.Text, so setup_related raised:
TypeError: Type of related field
sale.order.line.x_fc_internal_note is inconsistent with
sale.order.x_fc_internal_note
Fixed by switching both Note fields on fp.job and sale.order.line
to fields.Html.
2. Module-load-order: Tier 3 fields (x_fc_delivery_method,
x_fc_ship_via, x_fc_invoice_strategy) are defined in
fusion_plating_jobs (related to sale.order via _inherit), but I
referenced them in fusion_plating core's fp_job_views.xml — which
loads BEFORE fusion_plating_jobs registers the fields. View
validator raised "Field x_fc_delivery_method does not exist".
Fixed by removing those 3 fields from the core view group and
adding them via xpath in fusion_plating_jobs's fp_job_form_inherit
(which loads after the fields are registered).
Both fixes deployed and verified — registry loads in 2s, all field
types match, related path resolves correctly. No data loss; the
fp.job rows that already had stored Text content for internal_note /
external_note will carry over into the Html field intact.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tier 3 of the SO->fp.job persistence audit. Three logistics/billing
fields surface on fp.job as related read-only (not stored) mirrors:
- x_fc_delivery_method - Local Delivery / Shipping Partner / Customer
Pickup. Cargo classification used by logistics planning.
- x_fc_ship_via - Carrier name (UPS, FedEx, customer pickup, etc.).
- x_fc_invoice_strategy - Deposit / Progress / Net Terms / COD-Prepay.
Read by the invoicing module's hooks; mirroring on the WO is for
manager visibility only.
These were intentionally chosen as related (not stored persisted)
because the SO is the authoritative source - the existing downstream
code (delivery + invoicing modules) already reads them off SO directly.
A stored copy would risk drift. Related auto-follows SO updates.
Same three fields also mirrored on sale.order.line as stored related
for per-line list visibility.
Closes the SO->fp.job persistence audit. All 10 operational fields
identified now flow through to the WO (7 stored + populated at confirm,
3 related read-only).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tier 2 of the SO->fp.job persistence audit. Four operational metadata
fields mirrored from sale.order:
- x_fc_internal_deadline (Date) - shop's internal target finish date,
ahead of the customer-facing deadline. Kept separate from
date_deadline (which scheduling code may adjust).
- x_fc_planned_start_date (Date) - customer-quoted planned start date.
Kept separate from date_planned_start (Datetime, capacity-adjusted).
- x_fc_internal_note (Text) - shop-internal notes from the order.
- x_fc_external_note (Text) - customer-facing notes, printed on
traveller / BoL / cert.
All four populate at SO confirm via _fp_auto_create_job, and surface
on sale.order.line as stored related fields for per-line visibility.
fp.job form view gets a Notes group alongside the Customer References
group from Tier 1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tier 1 of the SO->fp.job persistence audit. Three customer-reference
fields entered on sale.order's Plating tab were not flowing through
to fp.job (or SO lines), so the shop floor and printed paperwork
(traveller, BoL, cert) had to round-trip via sale_order_id every time.
Changes:
- fp.job: new x_fc_customer_job_number (Char, tracking), x_fc_po_number
(Char, tracking), x_fc_rush_order (Boolean, tracking). All three
populated by _fp_auto_create_job at SO confirm time.
- sale.order.line: x_fc_customer_job_number / x_fc_po_number added as
stored related fields off order_id so per-line list views show the
customer's references without navigating to the order header
(x_fc_rush_order was already on lines).
- fp.job form view: small Customer References group under the title
surfaces the three fields where the user expects them.
Verified end-to-end: SO -> SO line related fields -> fp.job direct
fields all carry the same value.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The qty gate I added refused Finish on steps where qty_at_step > 0,
to force operators to move parts forward first. But the first-step
seed in _compute_qty_at_step gives the earliest non-terminal step
a notional qty = job.qty — a UI hint, not actual parked parts.
Paperwork steps (Contract Review, Inspection-by-paperwork, etc.)
sit on that seed, and the gate was blocking Finish with a misleading
error.
Fixes:
- button_finish gate now checks for REAL incoming moves before
refusing. Seed-only qty (no incoming_move_ids filtered to non-
self-loop) is exempt.
- _fp_record_one_piece_auto_move detects seed-only qty and bulk-
moves ALL parts in one shot to the downstream step. Correct for
paperwork / first steps where parts don't physically wait
per-piece — one click finishes the paperwork and pushes the whole
batch forward.
For steps with REAL incoming moves (parts actually moved here via
a Move record), the original gate semantics still apply: qty == 1
auto-moves one part; qty > 1 raises with the "use Complete 1 → Next
or Move…" message.
Verified on entech: Contract Review with seed qty=6 now finishes
cleanly, bulk-moving all 6 parts to the next step in one move.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a supervisor edits a timelog's date_started/date_finished (or
deletes a stale timelog), the parent step's "Actual Min" column
was showing stale data — duration_actual is a regular Float set
once by button_finish.
Adds:
- fp.job.step._fp_resum_duration_actual: quiet helper that re-sums
duration_actual from time_log_ids.duration_minutes. Skip no-op
updates so write traffic is minimised.
- fp.job.step.timelog.create/write/unlink hooks: call the helper
on the affected parent step(s) so duration_actual stays
consistent. Write hook only fires when date_started/date_finished/
step_id changed (notes edits skip resync). step_id reassignment
resyncs both old and new parent.
- Existing action_recompute_duration_from_timelogs (manual button)
still posts a chatter entry for audit-trail use cases.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three coupled shop-floor corrections:
- fp.job._compute_display_name: renders "Work Order # 00011" in
form header, breadcrumbs, M2O dropdowns, and error messages.
DB name stays as WH/JOB/00011 - existing chatter/cert/delivery
references unchanged.
- fp.job.step.button_finish: refuses if qty_at_step > 0 AND a
downstream pending/ready step exists. Last runnable step is
exempt (parts complete in place). Manager bypass via
fp_skip_qty_gate=True context key.
- fp.job.step.action_complete_one_to_next: new per-row button
"Complete 1 -> Next" for streaming flow (large parts going
one-by-one). Records move(qty=1) to next step; if drain takes
qty_at_step to 0, auto-finishes source + auto-starts destination
via existing action_finish_and_advance.
- fp.job.step._fp_record_one_piece_auto_move: auto-move shim
wired into action_finish_and_advance. qty=1 + downstream =>
silently record move(1). qty>1 + downstream => raise pointing
at Complete 1 -> Next. Last step always allowed.
- 16 new TestQtyGate tests covering gate / shim / auto-finish /
last-step exemption / display rename / Move wizard zero-qty.
Spec: docs/superpowers/specs/2026-05-12-step-qty-gate-and-display-rename-design.md
Plan: docs/superpowers/plans/2026-05-12-step-qty-gate-and-display-rename.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three fixes from user feedback:
1. Chatter posting raw HTML
_AUDIT_BODY in migration 19.0.18.8.0 was a plain str with <p>
tags. message_post escaped it for safety, so the chatter pill
rendered '<p><strong>...</strong></p>' literally to the recipe
author. Wrapped in markupsafe.Markup so Odoo recognises it as
safe HTML. Going forward: ANY message_post body containing HTML
tags MUST be wrapped in Markup() — most callers already do this,
the migration script was the outlier.
2. Library template editor showed raw <p> tags
onOpenLibraryEdit was JSON-cloning the payload directly without
running description through the existing _htmlToText helper that
the per-step editor uses. Added the conversion. Save path
(onSaveLibraryEditor + library_save) already wraps via
_textToHtml so storage stays HTML-compatible.
3. Per-step inline form was missing critical fields — user had to
delete + re-add a step to change Type/workflow trigger/parallel/signoff
onToggleEdit now also captures default_kind, triggers_workflow_state_id,
parallel_start, requires_signoff into the edit state. onSaveStep
sends them in the write vals. Added _fpResetStepEdit helper to
keep open/cancel/save reset paths in sync.
New per-step form has:
* Step Type (Default Kind) dropdown — drives workflow milestone
triggers + step-kind routing (e.g. contract_review opens QA-005)
* Triggers Workflow State dropdown (Sub 14) — per-step override
* Parallel Start checkbox (Sub 13)
* Require QA Sign-off checkbox
step_write controller endpoint also gained a field whitelist —
was previously accepting any vals dict from the client (security
hole + opaque to maintainers).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three issues from user testing on entech:
1. RPC error: column fp_step_template.triggers_workflow_state_id
does not exist
Root cause: the field was declared in fusion_plating CORE, but
its target model fp.job.workflow.state lives in fusion_plating_jobs.
Odoo loads core BEFORE jobs (jobs depends on core), so when core's
field declaration runs, the comodel doesn't exist yet — and Odoo
silently skips creating the column.
Fix: moved the field to fusion_plating_jobs/models/fp_job.py via
_inherit. Now the column is added when jobs loads (after core),
and the FK target is resolvable.
2. No chatter on the Workflow State form
Added _inherit = ['mail.thread', 'mail.activity.mixin'] to
fp.job.workflow.state. Tracking enabled on name/code/sequence so
admins see who changed the milestone vocabulary. <chatter/> widget
added to the form view.
3. Form layout still showed cramped 2-col help text
The XML file on disk had my new alert-info card, but Odoo's DB
ir_ui_view still held the old arch. The -u didn't refresh it
(likely because the file's mtime didn't change between deploys).
Fix: bump version + the next deploy will run a SQL DELETE on the
ir_ui_view record so Odoo recreates it from XML on -u.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups on the workflow state work:
1) Form layout
The "How triggers combine" help text was crammed into a 2-column
group, taking ~25% of the available width. Pulled it out of the
group and rendered as a full-width <div class="alert alert-info">
below the trigger fields. Same fix applied to Notes — uses a
<separator> + bare <field> for full sheet width.
2) Simple Recipe Editor support
The trigger field was only exposed in the Tree Editor. Added it
to the Simple Editor's inline library form too:
* fp.step.template.triggers_workflow_state_id (new Many2one) —
per-template default, snapshot-copied to recipe nodes when
dropped into a recipe (added to _SNAPSHOT_FIELDS).
* /fp/simple_recipe/workflow_states/list — new endpoint to feed
the dropdown. Soft-fails when fusion_plating_jobs isn't
installed (returns []).
* Library editor JS — _fpEnsureWorkflowStatesLoaded helper
caches the catalog on first open (create + edit paths both
warm it). Save vals carry the trigger id.
* Library editor XML — dropdown rendered after the flag
checkboxes. Hidden when the catalog is empty so the form
doesn't show a useless "— None —" pick.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the generic Draft/Confirmed/In Progress/Done statusbar with
a shop-configurable list of plating-specific milestones. Bar advances
automatically as recipe steps complete; no manual button clicks.
What ships
==========
* New model: fp.job.workflow.state
Catalog of milestones (name, code, sequence, color, triggers).
Triggers can be:
- trigger_default_kinds: "receiving,inspect" matches by step.default_kind
- trigger_first_step_started: any wet/bake/mask/rack step started
- trigger_all_steps_done: every non-cancelled step in done/skipped
- block_when_quality_hold: held back while NCR/hold open
Plus per-recipe-node override (see below).
* Default 7-state seed (data/fp_workflow_state_data.xml):
Draft → Confirmed → Received → In Progress → Inspected → Shipped → Done
noupdate=1 so per-shop edits survive module upgrade.
* Recipe-side trigger field on fusion.plating.process.node:
triggers_workflow_state_id (Many2one, optional)
Wins over default_kind matching. Lets the recipe author pin a
specific step as a milestone trigger even when default_kind isn't
set or doesn't match. Exposed in the Recipe Tree Editor properties
panel (dropdown sourced from the catalog).
* fp.job.workflow_state_id (computed, stored)
Iterates the catalog in sequence order; lands at the highest passed
milestone. Recomputes on step state / kind / recipe_node / quality
hold changes. Replaces fp.job.state on the form's statusbar.
* Settings UI: Configuration > Workflow States
Standard list+form pages so admins can add / edit / deactivate
states. Manager-group write permission, supervisor read.
What this does NOT do
=====================
* Doesn't drop fp.job.state — that field still drives the internal
state machine (button_confirm, action_cancel, etc.). Only the
UI statusbar is reassigned.
* No migration for existing jobs — they auto-recompute on next read
because workflow_state_id is a stored compute with the right
api.depends. Existing WH/JOB/00342 will display its current
workflow state on next page load.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: operators kept asking why their work order said "Step 10"
for the first row. The 10-spacing was originally there to allow midpoint
inserts (insert sequence 15 between 10 and 20 without renumbering).
Tradeoff is operator confusion, and recipe authors rarely insert in the
middle anyway. Switching to 1-based contiguous sequences.
Files changed (every step-sequence allocation in the codebase):
fusion_plating_jobs/models/fp_job.py
_generate_steps_from_recipe — seq_counter starts at 1, increments by 1.
This is the path that builds fp.job.step records, so new jobs now show
Step 1, 2, 3, ... in the work order.
fusion_plating_bridge_mrp/models/mrp_production.py
Same change for the legacy MRP bridge so customers still on
mrp.production also get 1-based numbering.
fusion_plating/controllers/recipe_controller.py
- create_node: max_seq + 1
- reorder_nodes: idx + 1
- swap renumber: i (was i * 10)
- paste-import renumber: i (was i * 10)
- move_node: max_seq + 1
- _copy_subtree (recipe duplicate/import): i (was i * 10)
fusion_plating/controllers/simple_recipe_controller.py
- _sequence_for_position rewritten — always renumbers siblings to
keep them contiguous. Returns pos + 1 for the inserted node.
Old code used midpoint-with-fallback-to-renumber (10/20/30 spacing).
- step_reorder: i (was i * 10)
- library_input_add + step_add_input: existing_max + 1
What this DOESN'T do
Existing fp.job.step records keep their old sequences (10, 20, ...).
Re-confirm the SO to spawn a fresh job if you want the clean 1-based
numbering on a current test job. No data migration — we're in dev
and the user explicitly said test data is disposable.
What this DOES do
Every NEW job created from this commit forward shows Step 1, 2, 3, ...
Every NEW recipe step inserted via the simple editor / tree editor
also gets sequence 1, 2, 3, ...
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two coherent feature drops shipping together because their fp_job_step
edits overlap. Both target operator workflow correctness.
## Sub 13 — Sequential step enforcement (recipe + per-step)
Background:
Investigation on WH/JOB/00339 showed operators starting Incoming
Inspection while Contract Review was still in_progress. Audit:
98.7% of recipe operations system-wide had requires_predecessor_done
= false (the legacy per-step opt-in defaults off, recipe authors
rarely tick the box).
Architecture:
Recipe-level toggle + per-step opt-out (Option A from /investigate).
* fusion.plating.process.node.enforce_sequential — Boolean on the
recipe root. Default True. When True, every operation under this
recipe waits for earlier-sequence steps to finish before it can
start.
* fusion.plating.process.node.parallel_start — Boolean on operation
nodes. When True, this step bypasses the sequential gate (e.g.
paperwork or QA review that runs alongside production).
* Mirrored on fp.step.template (parallel_start) so library steps
carry the flag into snapshots.
* fp.job.enforce_sequential — related from recipe_id. Snapshotted
at job creation so a recipe author flipping the recipe's flag
AFTER job generation does NOT change behaviour mid-run.
* fp.job.step.parallel_start — related from recipe_node_id.
* Decision matrix (encapsulated in
fp.job.step._fp_should_block_predecessors):
recipe.enforce_sequential | step.parallel_start | step.req_pred_done | block?
--------------------------|---------------------|--------------------|------
True | False | any | YES
True | True | any | no
False | any | True | YES
False | any | False | no
* Manager bypass via context fp_skip_predecessor_check=True (existing).
Runtime gates:
* fp.job.step.button_start — calls _fp_should_block_predecessors;
raises UserError naming the blocking earlier step(s).
* fp.job.step.can_start — computed Boolean for view-side disable.
* Move wizard predecessor check
(fusion_plating_shopfloor/controllers/move_controller.py) — uses
the same helper so tablet + backend behave identically.
UI surface:
* Recipe form (fp_process_node_views.xml) — enforce_sequential
toggle on recipe root, parallel_start checkbox on operations.
* Step template form — parallel_start checkbox.
* Simple Recipe Editor (inline library form) — Parallel Start
checkbox + legacy flag demoted with muted styling + supervisor
group gate.
* Recipe Tree Editor (properties panel) — both flags exposed,
only-show on the right node_type.
* Controllers updated to allowlist + payload the new fields.
Migration:
fusion_plating/migrations/19.0.18.12.0/post-migrate.py — sets
enforce_sequential = TRUE on every existing recipe-root node.
Idempotent. User confirmed dev-stage data, so retroactive flip
is safe (no production jobs to disrupt).
Tests:
TestSequentialEnforcement (10 tests) covering:
* sequential mode blocks out-of-order start
* first step always startable
* predecessor finish/skip unlocks next
* parallel_start opts out of gate
* free-flow mode bypasses gate
* legacy requires_predecessor_done still honoured in free-flow
* manager bypass via context
* can_start compute reflects state correctly
* library template parallel_start snapshots into recipe-node
## Sub 12e — Record Inputs Wizard v3 (card layout, dark-mode aware)
Background:
v2 wizard was a 17-column wide editable table. Operators got lost
finding which value column applied to their row's type, horizontal
scroll required on tablets, composite types crammed into one row.
New layout:
* Each measurement renders as a stacked card (CSS Grid + display
transformation on the existing list widget — preserves inline
editing, no JS rewrite).
* Card header: prompt name (large, bold) + type/unit pills.
* Card body: ONLY the value widget for this row's type
(number / boolean / date / text / photo / multi-point / panel).
* Composite types (multi-point thickness 5x reading + avg, bath
panel 4 fields) get inline sub-grid inside the card.
* Empty state ("no measurement prompts") with friendly CTA.
Dark mode:
* SCSS branches at compile time on $o-webclient-color-scheme
(per fusion-plating/CLAUDE.md note).
* Tokens: 7 surface colours + 4 ink levels with light/dark hex
pairs, all behind var(--fp-*) custom properties for per-deploy
override.
* Registered in BOTH web.assets_backend AND web.assets_web_dark
so each bundle compiles its own palette.
Tablet polish:
@media (max-width: 900px) — collapse meta below prompt + bump
numeric input min-height to 56px.
Defensive:
* v2 view kept in the XML file (instant rollback by changing one
view_id ref).
* `:has(.o_invisible_modifier)` rule drops empty cells out of the
grid so Odoo's invisible="..." doesn't punch holes in layout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bucket 1 — Generation bug fix
- post-migrate.py for 19.0.18.8.0 promotes flat 'step' children of
recipes to 'operation' so fp.job._generate_steps() picks them up.
Filter is narrow: only direct children of node_type='recipe' get
flipped, tree-editor sub-steps (parent.node_type='operation') are
untouched. Idempotent. Posts an audit chatter note on each affected
recipe.
- Simple Editor controller hardcodes node_type='operation' on insert
+ snapshot-import path so future recipes start correct.
Bucket 2 — Inline library authoring
- 6 new JSONRPC routes (/fp/simple_recipe/library/load + save +
seed_defaults + input/{add,write,remove}, /fp/simple_recipe/tank/list).
- + New Step button in the right pane opens an inline form with name /
kind / icon / instructions / stations / flags / prompts table.
- Pencil icon on each library row reopens the same form prefilled.
- Step Kind picker leads with 'Generic — no automatic behaviour'.
- 'Seed defaults from kind' calls action_seed_default_inputs server-side
for kinds that have curated default prompts.
Bucket 3 — Back nav
- '← Recipes' button in the header (or '← Part' when opened from
Process Composer) mirrors recipe_tree_editor.js, with
clearBreadcrumbs:true to avoid stack pollution.
Verified on entech: LGPS1104's 19 'step' children now show as
'operation', migration chatter note posted on the recipe, asset cache
busted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Plating Jobs list was sorted priority-desc, deadline-asc, id-desc — which
mixed Done (closed) jobs in with Confirmed (open) jobs and made the list look
chaotic to managers. Done jobs from weeks ago surfaced above active work.
Two changes:
1. New stored compute fp.job.state_priority (Integer, indexed) ranks states
by managerial relevance: in_progress=0, confirmed=1, draft=2, on_hold=3,
done=4, cancelled=5. _order now leads with state_priority asc, then
priority desc, then date_deadline asc, then id desc. Active work bubbles
to the top automatically.
2. Plating Jobs action defaults to a new 'Open' filter
(state not in done, cancelled). Managers see only active work by default;
they untick the filter to see history. Added On Hold + Cancelled filters
too for full state coverage.
Verified on entech: top 10 jobs are now all in_progress, sorted by deadline
ascending. Existing 26-row list goes from chaotic to focused.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Battle-tested complete workflow on entech: ABC Manufacturing + Anodize
recipe (id=136) cloned to part-variant (id=1775) → SO S00276 confirmed →
fp.job 1234 with 17 steps → recorded 56 measurement values exercising all
13 input types (incl. all 4 new types) → CoC chronological report renders
69KB with all values incl. photo thumbnails.
Bugs found and fixed:
1. fp.process.node.input_ids missing copy=True — when a master recipe
was cloned per-part (the standard variant pattern), the operator
prompts on each step did NOT get copied to the variant. Result: jobs
built from variants ran with zero prompts even though the master had
them. Fixed: input_ids now copy=True so cloning auto-duplicates.
2. CoC chronological template read dest.input_ids where dest is
fp.job.step. Steps don't carry input_ids — that field lives on the
recipe node. Result: AttributeError aborted the entire CoC render.
Fixed: walk via dest.recipe_node_id.input_ids; preserves the existing
collect=True filter.
3. CoC chronological template used hasattr() in a t-value expression.
QWeb's expression engine doesn't expose Python builtins, raised
KeyError: 'hasattr'. Fixed: use 'collect' in i._fields instead.
Also enhanced photo rendering in CoC: was just "[Attachment]" placeholder;
now renders an actual <img> thumbnail (max 80px tall) plus the filename.
Battle-test script saved to fusion_plating/scripts/bt_e2e_anodize_v2.py
for re-runs / regression testing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements 2026-04-29-step-library-audit-design.md. Bumps fusion_plating
to 19.0.18.7.0, fusion_plating_jobs to 19.0.8.12.0, fusion_plating_reports
to 19.0.10.2.0.
LIBRARY EXPANSION
- 8 new Step Kinds: Receiving, Electroclean, Strike, Salt Spray,
Adhesion Test, Hardness Test, Packaging, Tank Replenishment
- 4 new input types: photo, multi_point_thickness, bath_chemistry_panel, ph
- DEFAULT_INPUTS_BY_KIND rewritten to seed audit-grade prompts on every
kind (bath IDs, photos, multi-point thickness, signatures, etc.)
- + Common Audit Fields one-click button on the library template form
- Default Operator Instructions relabel + alert callout
PER-RECIPE CONFIGURABILITY
- collect (Boolean) per recipe-step input prompt — opt out without delete
- collect_measurements (Boolean) master switch on recipe step — when off,
wizard skips entirely
- template_input_id (Many2one) traceability link from recipe to library
- Recipe-step backend form view exposes the new fields with handle drag,
toggle, target range, and library-source column
RUNTIME WIRING
- Step input wizard filters node.input_ids to step_input AND collect=True;
short-circuits on collect_measurements=False
- New input types: photo (image widget + ir.attachment), multi-point
thickness (5 readings + auto avg, skips empty cells), bath chemistry
panel (pH/conc/temp/bath bundle), pH (0-14 numeric)
- Composite values JSON-serialized into value_text; photo via attachment
CoC REPORT
- Filters captured prompts to collect=True only
- Renders new input types with appropriate format
MIGRATION (post-migrate.py for 19.0.18.7.0)
- Backfills collect=True on recipe-step inputs
- Backfills collect_measurements=True on recipe steps
- Re-runs action_seed_default_inputs on every existing template
(idempotent, preserves user edits)
- Backfills template_input_id by name-matching against source library
template (handles JSONB vs varchar name columns)
SEED DATA
- 8 example templates (one per new kind) in fp_step_template_data.xml
with noupdate=1
BATTLE TEST
- bt_step_library_audit.py: 29 assertions all PASS on entech
OWL EDITOR EXTENSION DEFERRED
- The simple recipe editor's per-step Instructions/Measurements
expansions were not implemented in this pass; users configure via the
backend recipe-step form. Track follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two distinct entities were both labelled 'Work Centre' (US/UK spelling
the only differentiator — confusing). Renamed by purpose, model IDs
unchanged so all 12+9 existing cross-refs keep working:
fusion.plating.work.center → 'Production Line'
Physical shop-layout grouping that owns tanks. Examples: 'Line 1 —
EN', 'Anodize Line', 'Prep Bay'. Has tank_ids (O2M),
supported_process_ids (M2M), capacity_per_day. The thing tanks live
in.
fp.work.centre → 'Routing Station'
Per-job-step routing entity (post-Sub-11 mrp.workcenter replacement).
Has 'kind' selection (wet_line / bake / mask / rack / inspect),
cost_per_hour for fp.job.step rollup, default_bath_id +
default_tank_id for release-ready validation. The thing a job step
routes through.
Conceptually a Production Line CONTAINS many Routing Stations (e.g.
'EN Line' production line has wet-line, bake, inspect routing
stations on it).
Updated:
- _description on both models
- string= on the name fields
- list/form/search view strings
- act_window names ('Production Lines' / 'Routing Stations')
- menu items in fp_menu.xml + fp_jobs_menu.xml
- doc comments in both model files explaining the distinction
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three targeted gates so operators no longer see admin/audit views:
- KPIs (menu_fp_dashboard) → supervisor+. Operators don't need
dashboards; their tablet shows what they need to do next.
- Move Log (menu_fp_job_step_move) → supervisor+. Operators see
their own moves on the tablet; this top-level menu is the
audit-of-everyone-else view.
- Replenishment Suggestions (menu_fp_replenishment_suggestions) →
supervisor+. Purchasing decision, not operator concern.
Other top-levels were already correctly gated:
- Sales / Configurator → estimator
- Shipping & Receiving → group_fp_receiving
- Compliance hub → supervisor+
- Configuration → manager
- Shop Floor / Quality → operator (correctly visible to floor staff)
- Operations parent stays open; child menus enforce per-action gates
Net effect: a fresh operator now sees ~5 top-level menus instead of
the previous ~10. Supervisors see ~8. Managers see all.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Within fp_menu.xml itself, the menu_fp_replenishment_rules entry
referenced menu_fp_config_materials_tanks which was defined later in
the same file. Odoo's data loader is strictly top-down within a file.
Reorganized by section: 1) root, 2) Configuration + 7 buckets,
3) Compliance hub, 4) Operations parent, 5) all child menus (referencing
parents already defined above).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fp_rack_tag_views.xml (and several other view files) reference the
new Phase-2 Configuration sub-folder menus (menu_fp_config_*) defined
in fp_menu.xml. Odoo's data loader is strictly sequential within a
module, so fp_menu.xml must come before any file that references
its bucket xmlids.
Caught by entech upgrade (ParseError on rack-tag menuitem).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>