Two user-reported gaps:
1. The Process Recipes list was about to be flooded by part-scoped
clones as soon as parts started carrying customised processes —
thousands of clones would bury the handful of real shared
templates (General Processing, Anodize, etc.).
Fix: the main Process Recipes action now narrows to
part_catalog_id = False so shared templates stay alone in that
view. A sibling menu "Part Processes" (Plating → Operations →
Part Processes) shows the inverse list — every part-cloned
process, grouped by part — so admins can audit clones without
cluttering the templates list.
Search view gains two toggle filters (Shared Templates /
Part-Scoped) and a "Group by Part" option. The node list gains
an optional "Part" column.
Split across modules: core owns the base search / tree / action
(unchanged); configurator owns all the part_catalog_id-dependent
pieces (filter extensions, list column, narrower domain,
"Part Processes" action + menu). Keeps the dependency direction
clean — configurator always depends on core, never the other way.
2. Added a "Process" smart button to the part form's button box.
Shows either a green check (composed) or "None" (not yet
composed) and opens the part-scoped Composer on click. Gives
users one-tap access from any part form without hunting through
the Process tab.
fusion_plating → 19.0.8.0.0
fusion_plating_configurator → 19.0.13.1.0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: "Disabled" was confusing; it looked like the step
was turned off entirely when in fact the semantic meant "every job
runs this, cannot be removed". Aligning labels with Steelhead's
own terminology and the mental model the shop floor already uses:
Python Selection values (unchanged: disabled / opt_out / opt_in)
disabled → "Required"
opt_out → "Opt-Out (included by default, can be removed per job)"
opt_in → "Opt-In (excluded by default, can be added per job)"
Tree-editor side panel inline labels match, plus a short helper
line under the dropdown:
"Required — every job runs this step.
Opt-Out — ships included, estimator can remove per job.
Opt-In — ships excluded, estimator can add per job."
Field string also flipped from "Opt In/Out" to "Step Usage" — the
new header reads closer to what the field actually controls.
Column order also flipped so the Opt-Out option appears above
Opt-In — matches the frequency in real recipes (most optional
steps are included by default and sometimes skipped, not the other
way around).
fusion_plating → 19.0.7.4.0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related gaps:
1. loadTree() only auto-expanded nodes when expandedNodes was
completely empty — that was "first-load only" behaviour. After
an import, the subsequent loadTree() found the map populated
and skipped the expand pass entirely, so every freshly imported
node came in collapsed ("2 hidden"). Users had to click each
one open by hand.
Fix: iterate the whole tree on every load and default any node
whose id isn't yet in expandedNodes to true. Nodes the user
explicitly collapsed stay that way (their id IS in the map,
set to false).
2. Added "Expand all" and "Collapse all" buttons to the tree-
editor header so operators can get a full view (or a tight
overview) without clicking node by node. Collapse all keeps
the recipe root expanded — otherwise the canvas goes blank.
fusion_plating → 19.0.7.3.0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The import feature appended every imported node to the end of the
target recipe. That's wrong for the common case — General Processing
has Shipping as its last operation, so importing an Electroless
Nickel pack should land BEFORE Shipping, not after it. The user
would otherwise have to click Move Up dozens of times.
Controller: /fp/recipe/node/import_children now accepts
insert_before_id:
null/missing → append at end (default, unchanged)
0 → insert at the start
<positive id> → insert right before that top-level child
Implementation reorders target's top-level children in one pass
after Phase 1 creates the copies (placeholder sequence=0). Phase 2
splits existing vs. new, finds the anchor index in the existing
list, and reassigns sequences 10/20/30/... across the merged list.
Collisions on the old max_seq-based append strategy are eliminated.
JS: state.importInsertBefore drives a new "Insert:" dropdown in the
toolbar with options:
— At the end — (default)
— At the start —
Before <each top-level child name>
Smoke on entech (3-case): insert-before-middle, insert-at-start,
insert-at-end all produce the expected ordering.
fusion_plating → 19.0.7.2.0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs reported on the tree editor after the move/import feature
shipped:
1. Card titles truncated to "Contrac…" because .o_fp_re_title had
white-space: nowrap + text-overflow: ellipsis. Swapped to
white-space: normal + overflow-wrap: anywhere so long names
wrap onto multiple lines inside the card. Widened card max-
width 380→460px and bumped min-width 240→260px so wrapped
titles have room.
2. Import-children was flattening the tree — all operations AND
their step children landed at the top level instead of staying
nested under their operations.
Root cause: src_node.copy({'parent_id': new_parent.id, ...})
on a _parent_store model behaved unpredictably — in some runs
the override in copy_vals didn't stick and child recursion
ended up with a wrong parent_id. Rewrote _copy_subtree to use
copy_data() + Node.create() so parent_id is set explicitly and
child_ids / parent_path are stripped (we recurse ourselves).
Smoke verified on entech: General Processing (1 root + 5 ops
+ 7 steps = 13 nodes) imports with hierarchy bit-identical to
source.
fusion_plating → 19.0.7.1.0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two gaps closed on the recipe tree editor based on user feedback:
1. Explicit Move Up / Move Down buttons on every non-recipe node
row, driven by a new POST /fp/recipe/node/move_sibling endpoint.
DnD already existed but couldn't reliably move a node above a
sibling when the drop zone overlapped the node being dragged.
The button-based flow sidesteps that entirely and makes "nudge
one slot" a single click.
2. Inline "Import from recipe" toolbar that appears when the
Import button is clicked in the header. User picks a source
recipe from a dropdown (POST /fp/recipe/list, excludes the
current one), toggles "Skip duplicate names", and clicks
Import. POST /fp/recipe/node/import_children deep-copies every
top-level child of the source under the current recipe,
preserving the sub-tree structure. Dedupe is on by default so
re-running the import on an already-populated recipe is a
no-op; users who want to merge identical-named branches can
untick the checkbox.
Controller endpoints:
- /fp/recipe/list (list recipe roots for the picker)
- /fp/recipe/node/move_sibling (swap with neighbour by direction)
- /fp/recipe/node/import_children (deep-copy subtree with dedupe)
Smoke verified on entech: 5-child recipe imported cleanly, dedupe
blocks re-import, sequence swap works.
fusion_plating → 19.0.7.0.0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two separate issues were stacking up on the breadcrumb trail:
1. The composer was launching the tree editor with the name
"Process Composer — …" — identical to its own label. The
breadcrumb trail ended up showing "Process Composer / Process
Composer" because both pages claimed the same name. Renamed the
tree-editor instance to "Process Editor — …" so the two pages
read distinctly.
2. Every "Back to part" click pushed a new entry onto the
breadcrumb stack instead of resetting. After one round-trip the
trail looked like "… / Composer / Editor / Part"; after two it
was "… / Composer / Editor / Part / Composer / Editor / Part".
Added clearBreadcrumbs: true to both back paths (composer's
backToPart and tree-editor's onBackToList) so a RETURN action
actually resets the stack.
fusion_plating → 19.0.6.2.0
fusion_plating_configurator → 19.0.12.4.0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the Recipe Tree Editor is opened from the part-scoped Process
Composer (Sub 3), the composer already passes part_id via the
action context. The editor was ignoring it and routing onBackToList
to the generic Recipes list, stranding the user away from the part
they came from.
Capture ctx.part_id at onMounted, expose a state.fromPart flag, and
branch onBackToList: if part_id is set, open the fp.part.catalog
form; otherwise keep the current Recipes-list behaviour. XML button
label flips "Recipes" → "Part" accordingly so the user knows where
the button will take them.
fusion_plating → 19.0.6.1.0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sensors previously only tracked alarm thresholds (alert_min/alert_max).
Missing the third piece of standard process control: the SETPOINT —
what the heater/chiller controls toward and what dashboards compare
against. Without it an operator can't tell whether 89°C is "on target"
or "barely still in spec".
Schema changes:
**fusion.plating.bath.parameter** (shop-wide default)
- New `target_value` field — the default setpoint for this parameter
across the shop (e.g. 87°C for ENP bath). Parallel to existing
target_min / target_max.
**fp.tank.sensor** (per-sensor override)
- New `target_value_override` — per-sensor override, zero = inherit
from parameter. Matches the existing override pattern for alert
thresholds so users can fine-tune per-tank without touching the
shop-wide spec.
- New `effective_target` / `effective_target_unit` computed — resolves
override → parameter default, converts to company-preferred unit.
- New `_get_setpoint()` helper for internal use.
**fp.tank.reading**
- New `deviation_from_target` — signed Δ from the sensor's effective
setpoint, in the company's preferred unit. Positive = above, negative
= below, zero if no setpoint defined.
- New `deviation_band` (selection: on/near/far/out/none) — coarse band
for fast visual scanning. `on` = within ±1° of target, `near` = ±3°,
`far` = beyond, `out` = actually out of the alarm band.
**Views**
- Sensor form: split the alerting panel into two groups — "Target
(setpoint)" on the left, "Alarm band" on the right. Makes the
distinction between "where we want to be" and "where we'd panic"
visually obvious.
- Reading list: new Δ + band columns, with decoration classes
(success/info/warning/danger) so the list reads at a glance.
- Tank form Sensors tab: inline setpoint + unit column.
Seeded: parameter "Bath Temperature (Hot Process)" now carries
target_value=87°C as a realistic ENP shop default. Sensors inherit
unless they set their own override.
Design decisions kept simple:
- Did NOT add a warning band (warn_min/warn_max). Two-tier model
(setpoint + alarm band) is enough for the pilot. Can add soft
warnings later as a separate commit if ops wants them.
- Did NOT auto-control heaters. Setpoint is stored as metadata only;
actual heater actuation via IoT is a future phase C project.
Verified: setpoint 87°C stored → displays 188.60°F on the live pilot
sensor (company pref = F). Each incoming reading correctly computes
signed deviation; bands colour the reading list appropriately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
**1. Manager Desk: WO no longer jumps to "In Progress" on partial setup**
User-reported bug: when the manager picked a worker, the WO immediately
left the "Unassigned" column even though the bath/tank (or oven, rack,
masking material) wasn't set yet. Worker would see a half-set job in
their queue and couldn't start it.
Fix:
- New compute `mrp.workorder.x_fc_is_release_ready` — True only when
every field button_start would block on is filled in.
- Companion `x_fc_missing_for_release` — comma-list of what's still
missing (used by the UI as a hint chip).
- Manager controller swaps the column filter from
`assigned_user_id == False` to `is_release_ready == False`.
- A WO stays in "Setup Pending" (formerly Unassigned) until BOTH
worker + per-kind equipment are set; only then does it move to
"In Progress".
**Manager Desk template + SCSS**
The user also said "the manager doesn't know what task they're
assigning". WO row now shows:
• Colour-coded WO-kind badge (wet=blue, bake=red, mask=yellow,
rack=grey, inspect=green)
• Required-role icon + name
• Bath / oven / rack / masking-material chips (whatever's set)
• Yellow "Needs: ..." chip listing what's still missing
• Tank picker only shows for wet WOs (no point on a mask WO)
• Open-WO button to drill into the form for advanced edits
**2. Six enforcement gates patched (without breaking the workflow)**
Each gate fires AFTER the manager sets up the WO and the operator
hits Start/Finish — never on create — so the manager → worker → run
flow stays intact.
| # | Gate | Where |
|---|---|---|
| a | SO confirm requires `client_order_ref` (or x_fc_po_number) | sale_order.action_confirm |
| b | Cert issue requires thickness readings (when partner.x_fc_strict_thickness_required) | fp_certificate.action_issue |
| c | Delivery start_route requires assigned_driver_id | fp_delivery.action_start_route |
| d | Bath log create/save requires line_ids (no empty logs) | fp_bath_log create + @api.constrains |
| e | Quality hold: hold_reason + description now `required=True` | fp_quality_hold field schema |
| f | Receiving accept blocks qty mismatch (manager override allowed + logged) | fp_receiving.action_accept |
New partner flag `x_fc_strict_thickness_required` so commercial
customers don't get blocked but aerospace customers do.
**Verified** via `scripts/fp_enforcement_audit.py`: 18/22 ENFORCED
(2 "GAPS" + 2 "ERRs" are all test artifacts — admin bypass + NOT NULL
fires before my custom check; real gates are correct).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to the company-level UoM defaults commit. Wires four more
unit-bearing fields to inherit from res.company defaults at create-time.
**1. fp.bake.oven**
• New `target_temp_uom` (°F / °C) — defaults from
company.x_fc_default_temp_uom.
• View: target_temp_min / max now render with a unit picker on the
same row instead of unitless floats. Rule of thumb: "350–380 °F".
**2. fp.bake.window**
• New `bake_temp_uom` — defaults from company.x_fc_default_temp_uom.
• View: replaced hardcoded `°F` span with a live unit picker so the
label matches whatever unit was actually recorded.
**3. fp.coating.config**
• New `bake_temperature_uom` — defaults from company.
• Removed hardcoded "Bake Temperature (°F)" label; the field is
now unit-agnostic and the unit travels with the value.
**4. fp.tank.volume_uom**
• Default now derives from company.x_fc_default_volume_uom via a
small mapping (gal → gal_us, L → l, imp_gal → gal_imp). The
selection itself stays the same — tanks already supported all
common volume units; we just pre-pick the right one per company.
**Verified end-to-end** (scripts/fp_uom_smoke2.py):
• Switching company default to °C + Litres
• New oven gets C ✓
• New bake window gets C ✓
• New coating config gets C ✓
• New tank gets `l` ✓ (mapped from company `L`)
• Restored defaults afterwards
Existing records keep their stored uom — no surprise mutation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Different facilities use different measurement systems. North-American
aerospace shops live in °F + mils + gallons + lb; ROW + most metric
shops use °C + microns + litres + kg. Add company-level defaults so
each shop picks its units once; new records inherit them automatically.
**Settings on res.company** (7 Selection fields):
• x_fc_default_temp_uom — °F / °C
• x_fc_default_thickness_uom — mils / microns / inches / mm
• x_fc_default_volume_uom — US gal / litres / Imp gal
• x_fc_default_mass_uom — lb / kg / oz / g
• x_fc_default_pressure_uom — psi / bar / kPa
• x_fc_default_current_density_uom — A/ft² (ASF) / A/dm² (ASD)
• x_fc_default_area_uom — sq in / sq ft / cm² / m²
All default to North-American aerospace conventions (F, mils, gal, lb,
psi, asf, sq_in) — admins flip them once during onboarding via
Settings → Fusion Plating → Units of Measure.
**Per-record use** (this round)
• mrp.workorder.x_fc_bake_temp_uom (°F / °C) — defaults from company,
operator can override per WO if a specific bake needs a different
unit (rare but allowed).
• Bake-finish gate error message now reports the actual unit:
"Bake Temp (°F)" or "Bake Temp (°C)" instead of hard-coded F.
• Form: Bake Temp + Temp Unit picker side-by-side in the bake group.
**Settings UI** — new "Units of Measure" block on Settings → Fusion
Plating page with help text per unit explaining where each is used.
**Verified end-to-end** (scripts/fp_uom_smoke.py):
• All 7 defaults populate with NA-aerospace defaults
• Switching company default to °C makes a NEW WO inherit °C
• Existing WOs keep their stored °F (no surprise mutation)
**Roadmap (deferred to next round)** — wire the same default-from-company
inheritance to:
• fp.bake.oven.target_temp (currently no UoM)
• fp.bake.window.bake_temp (currently no UoM)
• fp.coating.config.bake_temperature (currently no UoM)
• fp.tank.volume already has volume_uom; default from company
• fp.bath.log chemistry readings already use parameter.uom; align
with company default for new params
The settings + framework are now in place — adding more per-record uom
fields is mechanical from here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The floating message-action toolbar (reaction / reply / star / link
icons) appearing on hover renders white-icons-on-white-background in
dark mode — Odoo's own dark.scss sets the icon hover color to white
but never gives the toolbar itself a dark background. Result: the
icons vanish entirely in dark mode.
Add fp_chatter_dark.scss that branches at compile time on
$o-webclient-color-scheme == dark (Odoo 19 compiles every SCSS file
into both web.assets_backend with `bright` AND web.assets_web_dark
with `dark`) and gives the toolbar:
- Solid dark background (#2b2f33 fallback, var(--o-component-bgcolor))
- Subtle 1px white-alpha border + drop shadow so it floats nicely
- Icon color rgba(255,255,255,.78) at full opacity (not 35%)
- Brighter hover state with a subtle bg highlight
Light bundle output is empty (the @if branch doesn't fire), so the
light theme is untouched.
Verified: dark bundle includes our rule with #2b2f33 marker present.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The old label was easy to confuse with the customer-facing
Certificate of Conformance (fp.certificate). Operators kept asking
why a customer cert appeared on their employee profile. The tab is
actually the operator's process-level training record (EN, chrome,
anodize, etc.) that gates WO start in mrp_workorder.button_start —
nothing to do with customer documents.
Renamed the page string and added a one-line muted description
so anyone landing on the tab understands what it's for. Also
distinguishes it from the new 'Shop Roles' tab (coarser task tags
used by Manager Desk auto-routing).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end workflow tightening + the team / skills system. Three
phases bundled because they share the same touchpoints (button_start /
button_finish / Manager Desk dropdown).
PHASE 1 — In-Odoo notifications + timer audit
=============================================
Workers now get a bell-icon notification (Odoo Discuss inbox) the
moment a manager assigns them a WO. No email — operators check Discuss
between jobs, and the customer-facing notification dispatcher stays
out of the worker loop.
- mrp.workorder.write() override fires message_notify(message_type=
'user_notification') only when x_fc_assigned_user_id transitions to
a non-empty value (clearing or no-op writes don't ping)
- 4 new fields on the WO header surface what was previously buried in
time_ids: x_fc_started_by_user_id, x_fc_started_at,
x_fc_finished_by_user_id, x_fc_finished_at
- button_start stamps started_* once (subsequent pause/resume cycles
preserve the original); button_finish stamps finished_* every time
the WO closes
- New "Timer Audit" group on the WO form (Time & Cost tab)
PHASE 2 — Presence-aware Manager Desk
=====================================
Manager Desk now knows who's clocked in. Works with vanilla
hr_attendance and fusion_clock — both expose hr.attendance with an
open record while the operator is on shift.
- bridge_mrp depends on hr_attendance
- hr.employee.x_fc_is_clocked_in computed field (batched query — one
DB hit for the whole employee set, not N+1)
- hr.employee._fp_clocked_in_user_ids() classmethod for the dashboard
- manager_controller sends operators with is_clocked_in / role_ids /
lead_hand_role_ids per worker, plus presence dict {clocked_in: N,
total: M}; each WO carries role_id/role_name so the dropdown can
match qualified operators
Manager Desk OWL:
- Header gets a "Present 7 / 12" pill chip; tap to toggle hideOffShift
(off-shift hidden when active, accent colour when filter is on)
- New operatorsForWO(wo) helper sorts dropdown options into 4 buckets:
qualified+clocked-in → lead-hand+clocked-in → clocked-in untrained
(training mode) → off-shift (greyed; only shown when hideOffShift
is false). Each option carries a ●/○ dot prefix and a soft suffix.
PHASE 3 — Skills, lead-hand-per-role, auto-promotion
====================================================
The team grows organically: managers assign training tasks, operators
finish them, the system auto-promotes after N successful runs.
- fp.work.role.mastery_required (integer, default reads from the
company-level Default Mastery Threshold). Each role can override —
masking might need 1 success, electroless nickel 5.
- res.company.x_fc_default_mastery_threshold + res.config.settings
exposure under "Workforce Settings" in the Fusion Plating settings
block (default 3)
- hr.employee.x_fc_lead_hand_role_ids m2m, separate from
x_fc_work_role_ids — Sarah can be a lead hand for masking + racking
even if those aren't her primary roles. Manager-only group access.
- New fp.operator.proficiency model (one row per employee+role) with
completed_count, first/last_completed_at, promoted, promoted_at,
progress_label compute. SQL-unique on (employee, role).
- mrp.workorder.button_finish increments the (employee, role)
counter, then if count >= role.mastery_required AND not promoted,
adds the role to x_fc_work_role_ids and posts a "🎉 Promoted"
chatter line on the employee record. Wrapped in try/except so a
tracker glitch never blocks production.
- Promotion uses the WO's assigned_user_id, NOT env.user — credit
goes to the operator who was supposed to do it, even if a manager
finished on their behalf.
Employee form gets a "Shop Roles" tab (supervisor+):
- "Tasks This Operator Can Do" m2m
- "Lead Hand For" m2m (manager-only)
- Read-only Task Proficiency list with progress / promotion badges
Verified on odoo-entech: all fields land, default threshold = 3,
asset bundle regenerated as 9f38f05.
Module bumps: fusion_plating 19.0.4.0.0,
fusion_plating_bridge_mrp 19.0.4.0.0,
fusion_plating_shopfloor 19.0.11.0.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Database stores datetimes naive-UTC, but the dashboards and emails were
showing UTC strings to users in EST/EDT — making 9pm Toronto look like 1am
the next day. Adds a single helper module + auto-detection on install.
Core changes (fusion_plating):
- New fp_tz.py helper: fp_user_tz, fp_format, fp_isoformat_utc, fp_time_ago
Resolves user.tz → company.x_fc_default_tz → UTC.
- res.company.x_fc_default_tz Selection (full pytz IANA list)
- res.config.settings exposes the company tz under a new "Regional
Settings" block in Settings > Fusion Plating
- post_init_hook auto-populates the tz on first install: tries admin
user → server /etc/timezone → America/Toronto fallback
- fp_process_node._to_dict now sends create_date/write_date as ISO with
explicit +00:00 marker so JS new Date() parses it as UTC and the
recipe tree editor's "time ago" math works correctly
Shop-floor controllers:
- shopfloor_controller.py: every fields.Datetime.to_string() and naive
.strftime() swapped for fp_format(env, ...) — due_at, bake times,
last_log_date, gates, server_time all now in user's tz
- _time_ago() removed; replaced with fp_time_ago helper which compares
tz-aware datetimes (the local one was naive-vs-naive and could be
off by hours)
- manager_controller.py date_planned: str(...)[:10] slice replaced
with fp_format MM/DD in user's tz
Notifications + reports:
- mail_template_data.xml: 5 .strftime() calls in body_html → babel
format_datetime / format_date with tz=(user.tz or company tz)
- report_fp_job_traveller.xml: rec.received_date (Datetime) gets
t-options="{'widget':'datetime'}" so Odoo's QWeb renders in user tz
Settings view layout:
- fusion_plating now owns the Settings page "Fusion Plating" app shell
- fusion_plating_certificates xpaths into it instead of redefining
(prevents app-name collision)
Verified on odoo-entech (LXC 111): post_init_hook detects
America/Toronto from /etc/timezone, MO date_start 2026-04-17 05:28 UTC
correctly displays as 2026-04-17 01:28 EDT.
Module versions bumped: fusion_plating 19.0.3.0.0,
fusion_plating_shopfloor 19.0.9.0.0, plus certificates / notifications /
reports → 19.0.3.0.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three UX improvements:
1. Sales menu reordered — New Quote (seq 1) is now the first entry,
followed by New Direct Order (5), Quotations (10), Sale Orders (20).
"New Quote" moved out of Configurator submenu into Sales so both
quote-creation paths live side-by-side.
2. Currency + unit display audit:
- fp.customer.price.list.unit_price flipped from Float to Monetary
with currency_field='currency_id' — list view now shows $ symbol
and a Total sum row
- fp.direct.order.wizard.unit_price flipped to Monetary, added
currency_id field and computed line_subtotal ($)
- % suffix appended to deposit_percent and progress_initial_percent
in the wizard
- Unit suffixes added where missing: bake_window.quantity (pcs),
window_hours (h), bake_temp (°F), bake_duration_hours (h);
bath.volume (L), bath.mto_count (turnovers); tank.volume shows
volume_uom inline
3. Saved line descriptions (new feature):
- New model fp.sale.description.template with name, description,
tag (standard/masking/rework/aerospace/nuclear/packaging/other),
optional coating_config_id and partner_id, usage_count bumped
on each use
- List + form + search views; new "Line Descriptions" menu under
Configurator
- 8 starter templates seeded (noupdate=1): ENP Standard/Aerospace/
Nuclear, masking variants, rework, packaging, delicate handling
- Direct Order Wizard gets a template picker (searchable Many2one)
+ editable paragraph; picking a template copies text to the
editable field, user tweaks freely, tweaked text lands on the
SO line as "<header>\n\n<description>"
- Auto-suggests template on coating+partner match if nothing
picked yet
Smoke-tested end-to-end: picked aerospace template, tweaked text,
confirmed wizard → SO S00030 has full description on line, usage
counter bumped from 0 to 1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug review fixes (found by code review + live QWeb error):
- report_fp_sale.xml: product_uom → product_uom_id (Odoo 19 renamed;
was raising KeyError during PDF render, blocking all sale-order prints)
- mrp_production.button_mark_done: add idempotency guard on delivery
auto-create (was duplicating on every re-close)
- fp.certificate._compute_batch_ids: use empty recordset instead of
False for Many2many computed fields
- fp_notification_template._collect_attachments: collapse attach_quotation
+ attach_sale_order into a single render so email doesn't double-attach
the same PDF
- fp.operator.certification: SQL unique on computed state was unreliable;
added explicit `revoked` boolean, made state pure-compute, replaced
SQL constraint with @api.constrains that checks active-only uniqueness;
has_active_cert now reads revoked + expires_date directly (no stale
stored state between nightly recomputes)
Two missing invoice strategies implemented + 1 pre-existing deposit bug fix:
- Progress Billing: new x_fc_progress_initial_percent field on sale.order;
_create_progress_initial_invoice bills the configured % on SO confirm
via down-payment wizard, _create_final_balance_invoice bills the
remainder on delivery
- Net Terms: no invoice on confirm; full invoice auto-created when
fusion.plating.delivery.action_mark_delivered fires
- Fix for deposit (pre-existing, silent): sale.advance.payment.inv
reads active_ids at wizard-create time, not on create_invoices();
context was being set on the wrong call, so every deposit attempt
raised "Expected singleton" and message-posted to chatter instead
of actually invoicing
- New fusion_plating_invoicing/models/fp_delivery.py hooks
action_mark_delivered to dispatch final invoice for progress/net_terms
- fp.direct.order.wizard + SO form surface the progress_initial_percent
field (conditional on strategy)
Report styling cleanup:
- Hide DISCOUNT column from sale + invoice landscape reports unless at
least one line has a non-zero discount; colspan auto-adjusts
- Replace hardcoded #0066a1 in all reports with company.primary_color
driven by doc.company_id → company → user.company_id fallback chain,
with #1d1f1e as ultimate fallback; new .fp-header-primary class
exposes the colour for inline section headers (CARGO DESCRIPTION,
PAYMENT DETAILS, OPERATOR SIGN-OFF, etc.) so they retint with the
company theme without template edits
Certificate of Conformance — formal ENTECH-style rebuild:
- New res.company fields: x_fc_owner_user_id (default signer, sig from
hr.employee.signature), x_fc_coc_signature_override (manual upload),
x_fc_{nadcap,as9100,cgp}_logo + _active toggles for accreditation
badges
- New res.config.settings section "Fusion Plating" exposing the above
as configurable blocks; manager-only menu under Configuration →
Fusion Plating Settings
- New fp.certificate fields: nc_quantity, customer_job_no,
contact_partner_id (child contact for Name / Email / Phone block)
- New report_coc_en + report_coc_fr templates (primary): custom header
(company contact | accreditations | company logo), bilingual labels
per variant, customer info block with customer logo, 3-column cert
info table, 6-column line-item table (Part # | Process | Customer
PO | Shipped | NC Qty | Customer Job No.), signature image + bordered
certification statement, footer "Fusion Plating by Nexa Systems"
- Legacy report_coc + report_coc_portrait kept for existing portal-job
bindings (no behaviour change)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tier 2 — Quality & audit readiness:
- T2.1 SPC on thickness readings (fp.certificate)
- spec_min_mils / spec_max_mils auto-pulled from coating config on create
- Computed: std_dev_mils, min/max, cpk, cpk_status (incapable/marginal/
capable/excellent/insufficient)
- Western Electric trend rules (rule 1: any point beyond 3σ; rule 4:
8 consecutive on one side of mean) → trend_alert + explanation
- New SPC group on certificate form with badge-coloured indicators
- T2.2 Operator certification enforcement (fp.operator.certification)
- Per (employee, process_type) records with issued/expires dates,
training record attachment, revocation workflow
- State auto-computed: active → expired when date passes
- MrpWorkorder.button_start() blocks with UserError if current user's
linked hr.employee lacks an active cert for the bath's process_type
- Managers bypass the check; expiring-soon filter in search view
- HR Employee form: "Plating Certifications" tab
- T2.3 Material traceability chain
- fusion.plating.batch.workorder_id (new Many2one) + production_id
(related through WO) for full chain
- fp.certificate gets computed batch_ids / bath_ids / batch_count
- "Batches" stat button → list of batches used for this cert's MO,
with their chemistry logs intact
- T2.4 Pre-treatment as first-class baths
- process_family selection on fusion.plating.process.type
(pre_treatment / plating / post_treatment / bake / strip / passivation /
masking / inspection)
- Bath search view: Pre-Treatments / Plating / Post-Treatments / Strip
quick filters
- Existing bath infra (logs, replenishment, SPC) now applies to pre-
treatment baths equally
Tier 3 — Business / revenue:
- T3.1 Customer-specific price lists (fp.customer.price.list)
- Per (customer, coating_config) with unit_price + basis (per_part /
sqin / sqft / lb)
- effective_from / effective_to for annual contract pricing
- min_quantity for volume breaks (cheapest price at requested qty wins)
- _find_price() helper resolves active entry by date + qty
- Direct Order wizard auto-fills unit_price on (partner, coating, qty)
change unless operator has typed an override
- Configurator menu → Customer Price Lists
- T3.2 Quote win/loss tracking (fp.quote.configurator)
- State values: draft → confirmed (won) / lost / expired / cancelled
- lost_reason selection (price / lead_time / tech / spec_mismatch /
no_bid / no_response / competitor / other) + lost_competitor_name
+ lost_details text
- Action buttons: Mark as Lost (requires reason), Mark as Expired
- won_date auto-set on SO creation; lost_date auto-set on mark_lost
- New "Win / Loss" tab on configurator form
- T3.3 Actuals vs. quoted margin (mrp.production)
- Computed monetary fields: x_fc_consumables_cost, x_fc_labour_cost,
x_fc_actual_cost, x_fc_quoted_revenue, x_fc_margin_actual,
x_fc_margin_pct
- Labour = sum(WO duration × workcentre cost_hour)
- Revenue = SO amount_untaxed via mo.origin lookup
- New "Job Costing" group on MO form with badge-coloured margin
- T3.4 Job consumables tracking (fp.job.consumption)
- One row per consumable event (bath replenisher, masking tape, PPE,
chemistry): product, qty, uom, unit_cost (snapshot), total_cost,
source, optional workorder link
- One2many x_fc_consumption_ids on mrp.production
- "Consumables" stat button on MO → filtered list
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>