The auto-prefill logic that fills received_qty from expected_qty on
fp.receiving create was committed to the entech LXC but never made it
back to main. Verified by a full quote→delivery→invoice walkthrough
(scripts/fp_e2e_human.py) — receiving step now passes.
Also adds the human-walkthrough E2E script that exercises every step:
RFQ → quote → SO confirm → MO + portal job auto-create → receiving
prefill → recipe → WO execution → MO done → CoC cert (rich PDF, no
thickness duplicate) → delivery prefill + lifecycle → invoice (posted,
not auto-paid) → notification log audit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Workflow structure is complete (path filters, matrix, services).
The 'Install Odoo 19' step is a TODO placeholder — the reproducible
Odoo-19 build environment is deferred to Phase 1 CI hardening.
Current Phase 0 test workflow is manual via ssh odoo-westin.
Made-with: Cursor
Three reported PDF bugs from the customer-facing email package:
1. Invoice body was empty — Odoo 19 sets display_type='product' on
regular invoice/SO lines (was empty string in 18.0). Both
report_fp_invoice.xml and report_fp_sale.xml only matched
`not line.display_type`, so every product line was skipped.
Fixed both portrait + landscape variants to also match
display_type == 'product'.
2. CoC PDF was a bare 30 KB header — _fp_generate_cert_pdf was
rendering action_report_coc, which is bound to portal_job and
has minimal content. Rewrote to use the rich fp.certificate-bound
report (action_report_coc_en / action_report_coc_fr based on
cert.partner_id.lang) and slugged the filename to
CoC-<Customer>-<CertName>.pdf so the email attachment reads
nicely instead of CERT-00123.pdf.
3. Thickness cert was an exact duplicate of the CoC — the CoC
template already embeds thickness readings. Skip thickness cert
creation entirely when the customer also wants CoC; only create
a standalone thickness cert when the customer opted out of CoC.
Also: dispatcher in fp_notification_template now prefers
portal_job.coc_attachment_id (the rich one we just generated) and
falls back to rendering action_report_coc_en against fp.certificate
by partner.lang — never the bare portal-job report.
Versions bumped: bridge_mrp 19.0.6.0.0, notifications 19.0.4.0.0,
reports 19.0.4.0.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 20 of Phase 0: document the sub-module split.
- fusion_accounting_core: foundation doc covering security groups, shared-field
schema preservation, and the Enterprise-detection helper.
- fusion_accounting_ai: preserves the original module's AI-specific design
decisions, Odoo 19 gotchas, deployment commands, controllers, models, theme
rules, and known issues. Adds a new Data-adapter pattern section documenting
tri-mode routing (fusion / enterprise / community).
- fusion_accounting_migration: doc for the Enterprise uninstall safety guard
and the wizard shell that future feature sub-modules will extend.
- fusion_accounting (meta): rewritten CLAUDE.md as a pure overview pointing at
sub-modules, plus a new README.md covering one-click install/uninstall.
Each sub-module now has CLAUDE.md (Cursor/Claude context), UPGRADE_NOTES.md
(version-by-version deltas / reference sources), and README.md (user-facing
install/usage docs). 11 files total.
Made-with: Cursor
Addresses code review feedback on Task 17:
- Add menuitem so 'Fusion Accounting -> Migrate from Enterprise' is reachable
(the UserError guidance now actually works). Placed at top level since
parenting under fusion_accounting_ai.menu_fusion_accounting_root would
require adding that module as a hard dep, which is wrong semantically
(migration should not require AI). Both menuitems carry the admin group
so the menu stays hidden from users who can't open the wizard anyway.
- Update the UserError wording to "Fusion Accounting -> Migrate from
Enterprise" (no longer "Settings -> ...") to match the actual menu
location; 'migration' is preserved per the test's assertIn check.
- Add skipTest guard to test_uninstall_not_blocked_when_migration_completed
so it doesn't pass vacuously on Community-only CI (the guard's
`if not installed: continue` would otherwise return True regardless of
the flag value, giving a false green).
- Move GUARDED_MODULES import to top of wizards/migration_wizard.py
(no circular-import risk -- models/ir_module_module.py doesn't import
from wizards/).
- Expand docstrings on button_immediate_uninstall and module_uninstall
overrides to note they may both fire in a single UI uninstall call
and that the guard is idempotent (pure read + raise).
Made-with: Cursor
E2E test (quote → SO → MO → WOs → ship → invoice → payment) ran clean
but flagged five gaps where the operator was filling in data the
system already knew. Closes all five.
#1 SO CONFIRM → AUTO-CREATE DRAFT MO (was a workflow blocker)
bridge_mrp/sale_order.py: action_confirm() override + new
_fp_auto_create_mo helper. Resolves the manufactured product from
the configurator's part-catalog → coating-config → FP-WIDGET
fallback; resolves the recipe from coating_config.recipe_id →
part_catalog.recipe_id → first installed recipe. Idempotent:
skips if any MO already exists for the SO. Errors are caught and
chatter-posted so SO confirm never fails because of an MO glitch.
#2 QUOTE PO → client_order_ref ON SO (one-line fix)
configurator/fp_quote_configurator.py: action_create_quotation
now copies po_number_preliminary into Odoo's standard
client_order_ref alongside the existing custom x_fc_po_number.
Portal pages, native reports, and integrations all read the
standard field; no reason both shouldn't carry the same PO#.
#3 MO DONE → AUTO-RENDER CoC + THICKNESS PDFs
bridge_mrp/mrp_production.py button_mark_done now calls a new
_fp_generate_cert_pdf helper after creating each fp.certificate.
Renders fusion_plating_reports.action_report_coc to PDF, stores
as ir.attachment, links to cert.attachment_id, AND cross-links
to portal_job.coc_attachment_id + delivery.coc_attachment_id so
the customer portal and the shipping email both find it without
an extra step. Thickness report falls back to the CoC layout
(which embeds thickness data) until a dedicated report ships.
Errors are logged but never block MO completion.
#4 RECEIVING received_qty PREFILL
receiving/fp_receiving.py: create() prefills received_qty from
expected_qty on draft. Operator only types when the count is
wrong (the rare case). Field carrier_tracking already exists,
so #4's 'no inbound tracking field' from the gap report turned
out to be a false alarm.
#5 DELIVERY scheduled_date + driver PREFILL
bridge_mrp/mrp_production.py: new _fp_build_delivery_vals
helper sets scheduled_date from the portal job's target_ship_date
(or now+2 business days as a sane fallback) and auto-picks
assigned_driver_id from clocked-in employees tagged is_driver
(falls back to any active driver if the shift is empty). The
outbound tracking_ref deliberately stays empty — that's the
carrier's number, paste it in once UPS/FedEx accepts the package.
Module bumps: configurator 19.0.5.0.0, bridge_mrp 19.0.5.0.0,
receiving 19.0.2.0.0.
Verified on entech: re-ran the E2E test against a fresh quote.
Quote → SO populated client_order_ref, SO confirm auto-created MO,
receiving prefilled received_qty=50, MO done generated CERT-00018.pdf
and linked it to portal job + delivery, delivery's scheduled_date
prefilled to 2026-04-29, full pipeline ended with portal job state
'complete'. The remaining 'gaps' in the static report are script
artefacts (e.g. it flags 'no inbound tracking field' but the field
exists; flags 'no driver auto-pick' but the demo data has zero
drivers tagged is_driver=True).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 0 Task 17. Installs a safety guard on ir.module.module that blocks
uninstall of Odoo Enterprise accounting modules (account_accountant,
account_reports, accountant, account_followup, account_asset,
account_budget, account_loans) until the per-module migration flag
fusion_accounting.migration.<name>.completed is set to True. Guard
covers both button_immediate_uninstall (UI) and module_uninstall
(CLI/API) paths, raising UserError with a pointer to the migration
wizard and an escape hatch config parameter.
Also ships a TransientModel fusion.migration.wizard as a shell: it
detects installed Enterprise modules via GUARDED_MODULES and exposes
action_run_migration for sub-modules to extend in later phases. No
per-feature migrations are registered yet -- Phase 1+ sub-modules will
hook in their own steps.
Tests: TestSafetyGuard x2 pass (blocked-when-pending verified with
account_accountant installed; not-blocked-when-completed verified by
setting the flag).
Made-with: Cursor
Task 16's security group rehoming (fusion_accounting → fusion_accounting_core)
only existed in post-migration. That flow fails on fresh pre-Phase-0 upgrades:
data-load runs before post-migration and looks up group xml-ids by
(module, name); if the row still has module='fusion_accounting', Odoo
creates a duplicate res.groups record under
module='fusion_accounting_core'. The subsequent post-migration
UPDATE...SET module='fusion_accounting_core' then trips the (module, name)
unique constraint on ir_model_data, rolling back the whole transaction.
Pre-migration runs BEFORE data-load, renames the five security xml-ids
(module_category, privilege, three groups) to the new module, so data-load
finds the existing rows and UPDATEs them in place. Existing user-group
links via res_groups_users_rel are preserved.
The post-migration is kept as an idempotent safety net (docstring
updated to reflect the new division of labour).
Verified on westin-v19 by simulating the pre-Phase-0 state (UPDATE
ir_model_data SET module='fusion_accounting' ...) and re-running the
upgrade: 5 rows renamed cleanly, zero duplicates, no errors.
Made-with: Cursor
After the right-side preview panel was retired, the left column had
Customer & Part / RFQ-PO / Geometry / Delivery & Fees stacked while
the right side ran out of content after Rush Order — almost half the
form was dead air. Reshuffled the groups so every row has peers.
Old layout (4 rows, mostly half-empty):
Customer & Part | RFQ / PO / Quantity & Options
Geometry | Auto from 3D (often empty)
Delivery & Fees | (empty)
Calculated Price | Final Price
New layout (every row balanced):
Customer & Part | RFQ / PO Documents
Quantity & Options | Auto from 3D (visible only with part catalog)
Geometry | Delivery & Fees
Calculated Price | Final Price
Quantity & Options moved out of the RFQ/PO group (where it was
shoehorned in via a <separator>) into its own group on the left of
row 2. Auto from 3D becomes its right-side peer when present, or
shrinks gracefully when absent.
Delivery & Fees moves up one row to pair with Geometry instead of
sitting alone. Net effect: form fits more above the fold and the
estimator's eye doesn't have to chase fields across uneven columns.
Bumped fusion_plating_configurator to 19.0.4.0.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Quote Configurator form devoted nearly half its width to a sticky
3D viewer + drawing PDF preview. That panel meant the actual fields
(geometry, dimensions, pricing) had to fight for real estate. Replaced
the inline previews with two affordances that take zero layout space:
1. New '3D Model' smart button at the top of the form, next to the
existing 'Drawings' button. Click to open the existing
fp_3d_viewer_open client action — same fullscreen modal the
'Full Screen' button used to launch from the side panel.
2. Inline 'Preview' link (eye icon) sits next to the 3D Model and
Drawing fields in the Customer & Part group. Click to open the
same modal preview as the smart button. Two paths to the same
content — power users grab the field-adjacent link mid-edit;
visual-thinkers grab the smart button up top.
Layout collapses to a single full-width column. The .o_fp_cfg_layout
wrapper is kept (display:block) so we have a stable hook in case a
side panel returns later for a different purpose. Old SCSS dance with
:has() selectors to fake-collapse the grid is gone.
Bumped fusion_plating_configurator to 19.0.3.0.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The search bar required Enter to fire, which felt clunky on a shop
floor where managers expect cards to filter as they type. Switched
to a 200ms-debounced live search — fast enough to feel instant on
keystrokes, slow enough to skip the network call when someone is
mid-word.
Search bar visual weight bumped:
- Width 260px → 380px (320px on iPad, full width on phones)
- Height 48px → 52px
- Font-size base → md, weight medium
- Search icon nudged 14px → 16px from the edge with a 1.05rem size
- Placeholder uses the lighter $fp-ink-faint so the input feels
inviting rather than already-filled
Behaviour:
- Type → cards filter after 200ms of no input
- Enter → fires immediately (skips debounce) for power users
- Escape → clears the search (new shortcut)
- Clear button → unchanged
Bumped shopfloor to 19.0.14.0.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Bake Window + First-Piece Gate cards looked rounded on their
own, but Odoo's default .o_kanban_record wrapper painted its own
background + border + box-shadow with sharper corners than our
inner .o_fp_kcard — visible as a faint square ghost behind every
card, especially obvious on the missed_window state where the red
wash on the inner card didn't extend to the wrapper edges.
Added a .o_fp_bw_kanban / .o_fp_fpg_kanban scoped override that
zeroes the wrapper's background, border, box-shadow and padding,
letting only our card surface render. Also drops the kanban group
container's tinted bg for the same reason.
Bumped shopfloor to 19.0.13.0.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Bake Window + First-Piece Gate cards looked rounded on their
own, but Odoo's default .o_kanban_record wrapper painted its own
background + border + box-shadow with sharper corners than our
inner .o_fp_kcard — visible as a faint square ghost behind every
card, especially obvious on the missed_window state where the red
wash on the inner card didn't extend to the wrapper edges.
Added a .o_fp_bw_kanban / .o_fp_fpg_kanban scoped override that
zeroes the wrapper's background, border, box-shadow and padding,
letting only our card surface render. Also drops the kanban group
container's tinted bg for the same reason.
Bumped shopfloor to 19.0.13.0.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion to commit 4843146 / f7f500f which added the shared
SCSS. This commit wires the views to use it: the manifest now
loads fp_kanbans.scss and the two kanban templates render with
the new .o_fp_kcard structure (state stripe, title, subtitle,
big metric, meta line, chip footer).
Companion to commit 4843146 / f7f500f which added the shared
SCSS. This commit wires the views to use it: the manifest now
loads fp_kanbans.scss and the two kanban templates render with
the new .o_fp_kcard structure (state stripe, title, subtitle,
big metric, meta line, chip footer).
Task 13 Step 10 of phase-0 plan.
- month_end.get_period_summary → ReportsAdapter.run_report(...) with
Community fallback to the trial_balance() aggregator.
- hst_management.get_tax_report → ReportsAdapter.run_report(...).
Other tools in these files (get_unreconciled_counts, find_entries_in_locked_period,
get_accrual_status, run_hash_integrity_check, calculate_hst_balance,
find_missing_tax_invoices, find_missing_itc_bills, create_expense_entry) touch
pure-Community models (account.move, account.move.line, account.account,
account.payment) directly and are tri-mode safe.
account.return tools in hst_management (get_tax_return_status, generate_tax_return,
validate_tax_return) and account.audit.account.status tools in audit.py already
handle the missing-model case gracefully. They fall outside this task's target
set of {account.report, account.followup.line, account.asset} and are left
as-is per plan.
All 12 data-adapter tests pass on westin-v19.
Made-with: Cursor
The two standalone menu pages (Bake Windows, First-Piece Gates) were
still on the older o_fp_card design from a pre-Plant-Overview pass —
visually drifted from the polished kanban-pattern cards we settled on
for Plant Overview. Pulling them onto the same design language without
rewriting them as OWL client actions (the 'Option A' from chat).
What changed
============
New shared SCSS — fp_kanbans.scss
---------------------------------
Defines .o_fp_kcard as the base kanban card surface. Mirrors the
Plant Overview .o_fp_po_card recipe: white $fp-card surface, 1px
$fp-border, $fp-radius-md corners, soft $fp-elev-1 shadow, hover
lift, 4px state stripe via ::before clipped by overflow:hidden.
Sub-elements (title, sub, metric, meta line, footer chip) get
their own classes so per-page tweaks stay surgical.
Page-scoped wrappers (.o_fp_bw_kanban, .o_fp_fpg_kanban) carry the
state/result → stripe colour mapping plus exception-state tints
(missed_window + fail get a soft danger wash so the card stands
out in a sea of normal ones).
Bake Window kanban
------------------
Rebuilt template — title (window name), part_ref subtitle, big
time-remaining metric (the operator's primary cue), meta line for
lot/customer/qty, footer with oven badge + state chip.
data-state attribute drives the stripe colour:
awaiting_bake → warning
bake_in_progress → info
baked → success
missed_window → danger + soft red wash
scrapped → muted + dimmed
First-Piece Gate kanban
-----------------------
Rebuilt template — title (gate name), part_ref subtitle, bath +
customer meta, inspector + first_piece_produced timestamp,
footer with result chip and an optional 'Released' badge when
the lot has been signed off.
data-result attribute drives the stripe colour:
pending → warning
pass → success
fail → danger + soft red wash
Shopfloor manifest bumped to 19.0.12.0.0 and the new SCSS is
registered in web.assets_backend after manager_dashboard.scss so
the design tokens it references are already in scope.
Plant Overview's existing .o_fp_po_card classes are deliberately
untouched — the OWL client action and the new kanbans share the
visual language but stay loosely coupled.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The two standalone menu pages (Bake Windows, First-Piece Gates) were
still on the older o_fp_card design from a pre-Plant-Overview pass —
visually drifted from the polished kanban-pattern cards we settled on
for Plant Overview. Pulling them onto the same design language without
rewriting them as OWL client actions (the 'Option A' from chat).
What changed
============
New shared SCSS — fp_kanbans.scss
---------------------------------
Defines .o_fp_kcard as the base kanban card surface. Mirrors the
Plant Overview .o_fp_po_card recipe: white $fp-card surface, 1px
$fp-border, $fp-radius-md corners, soft $fp-elev-1 shadow, hover
lift, 4px state stripe via ::before clipped by overflow:hidden.
Sub-elements (title, sub, metric, meta line, footer chip) get
their own classes so per-page tweaks stay surgical.
Page-scoped wrappers (.o_fp_bw_kanban, .o_fp_fpg_kanban) carry the
state/result → stripe colour mapping plus exception-state tints
(missed_window + fail get a soft danger wash so the card stands
out in a sea of normal ones).
Bake Window kanban
------------------
Rebuilt template — title (window name), part_ref subtitle, big
time-remaining metric (the operator's primary cue), meta line for
lot/customer/qty, footer with oven badge + state chip.
data-state attribute drives the stripe colour:
awaiting_bake → warning
bake_in_progress → info
baked → success
missed_window → danger + soft red wash
scrapped → muted + dimmed
First-Piece Gate kanban
-----------------------
Rebuilt template — title (gate name), part_ref subtitle, bath +
customer meta, inspector + first_piece_produced timestamp,
footer with result chip and an optional 'Released' badge when
the lot has been signed off.
data-result attribute drives the stripe colour:
pending → warning
pass → success
fail → danger + soft red wash
Shopfloor manifest bumped to 19.0.12.0.0 and the new SCSS is
registered in web.assets_backend after manager_dashboard.scss so
the design tokens it references are already in scope.
Plant Overview's existing .o_fp_po_card classes are deliberately
untouched — the OWL client action and the new kanbans share the
visual language but stay loosely coupled.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 13 Step 8 of phase-0 plan.
get_ap_aging → FollowupAdapter.aged_payables().
The adapter method was added alongside aged_receivables() in the previous
commit, so this is a pure tool-wrapper change. Other AP tools
(find_duplicate_bills, get_unpaid_bills, get_payment_schedule, etc.) touch
account.move / account.move.line with pure-Community filters (move_type in
(in_invoice, in_refund)) which are tri-mode safe and do not need adapter
routing.
All 9 data-adapter tests pass on westin-v19.
Made-with: Cursor
Task 13 Step 7 of phase-0 plan.
Routes the AR tools through the FollowupAdapter so they work identically on
fusion-native, Enterprise, and pure Community installs:
- get_ar_aging → FollowupAdapter.aged_receivables()
- get_overdue_invoices → FollowupAdapter.overdue_invoices()
- send_followup → FollowupAdapter.send_followup()
- get_followup_report → FollowupAdapter.followup_report_html()
FollowupAdapter extended:
- overdue_invoices() now includes partner_email, partner_phone and
amount_total so the tool wrapper can render its richer response.
- aged_receivables() and aged_payables() new shared-implementation method
_aged_buckets() produces the 5-bucket aging shape the AR/AP tools emit.
- followup_report_html() and send_followup() isolate the Enterprise
account.followup.report / partner.execute_followup calls; Community mode
returns a graceful error dict.
Pure-Community tools in accounts_receivable.py (get_partner_balance,
reconcile_payment_to_invoice, get_unmatched_payments) unchanged — they touch
account.move / account.move.line directly which is tri-mode safe.
3 new data-adapter tests added (total: 9; all passing on westin-v19).
Made-with: Cursor
Pilot refactor per Task 13 Step 2 of phase-0 plan: route the bank-rec AI tool
function through the data adapter so it works identically whether the install
profile is fusion-native, Enterprise, or pure Community.
Extends BankRecAdapter.list_unreconciled() with optional filter params
(date_from, date_to, min_amount, company_id, and optional journal_id) and adds
partner_name / journal_id / journal_name to the returned shape so the tool
wrapper can preserve its existing outward return dict.
All 6 data-adapter tests pass against westin-v19 (TestDataAdapterBase,
TestBankRecAdapter, TestReportsAdapter, TestFollowupAdapter, TestAssetsAdapter).
Made-with: Cursor
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>
The 'Tasks This Operator Can Do' many2many_tags widget declared
options="{'no_create_edit': True, 'color_field': 'color'}" but
fp.work.role doesn't have a color field — Odoo then tried to
fetch it on every employee form load and crashed with:
ValueError: Invalid field 'color' on 'fp.work.role'
Dropped the color_field option. Roles still render as tags, just
without the coloured chip background. (If we want coloured chips
later, add a Color integer field to fp.work.role and restore the
option — but the feature wasn't wired up anyway.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
a2efc9f committed a hr_employee.py with unresolved <<<<<<<
HEAD / >>>>>>> Stashed changes markers — Python wouldn't have
imported the file. Restoring to f340c87's version. The intended
fix (Odoo 19 'in' operator handling) lives on main as 0f41eb1.
Two compounding bugs in _search_x_fc_is_clocked_in surfaced when
fusion_clock's auto-clock-out closed all the demo open attendances:
1. Odoo 19 normalises ('=', True) into ('in', OrderedSet([True]))
before invoking the search method. The previous code only
handled '=' / '!=' and fell through to return [] for 'in' /
'not in' — which Odoo treats as 'no constraint' and matches
the entire table.
2. ('id', 'in', []) is also treated as no-constraint in some
Odoo versions; replaced with a [0] sentinel so the empty-
open-attendance case correctly matches nothing.
Rewrite reduces caller intent to a match_set of booleans, flips on
negative operators, then emits id IN / NOT IN against the cached
open-attendance employee ids. Variable signature accepts Odoo's
3-arg (records, op, val) form too in case the API shifts.
Verified on entech: clocked_in==True returns 3 (Carlos, James,
Marie); ==False returns the other 5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two compounding bugs in _search_x_fc_is_clocked_in surfaced when
fusion_clock's auto-clock-out closed all demo open attendances:
1. Odoo 19 normalises ('=', True) to ('in', OrderedSet([True]))
before invoking the search method. The previous code only
handled '=' / '!=' and fell through to return [] for 'in' /
'not in' — which Odoo treats as 'no constraint' and matches
the entire table.
2. ('id', 'in', []) is also treated as no-constraint in some
Odoo versions; replaced with a [0] sentinel so the empty
case correctly matches nothing.
Rewrite reduces caller intent to a match_set of booleans, flips it
on negative operators, then emits id IN / NOT IN against the cached
open-attendance employee ids. Accepts a 3-arg signature too in case
Odoo's compute-field calling convention shifts again.
Verified on entech: clocked_in==True returns the 3 currently-on-shift
operators (Carlos, James, Marie); ==False returns the other 5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 0 Task 7. Pre-Phase-0 all AI code lived in module='fusion_accounting';
the code now lives in 'fusion_accounting_ai' but existing ir_model_data
rows still record the old module name. This post-migration rewrites them.
Handles duplicate-key conflicts by deleting old orphan rows when data-load
has already created a new row under the same name in the new module.
Idempotent: second run reassigns 0 rows.
Made-with: Cursor
Stage filler gains step 6g — spins up five new manufacturing orders
so the Manager Desk has a busy shop floor instead of the single
in-flight MO that came out of the base seeder.
Plan, in order of creation:
WH/MO/00012 HOT Cyclone Manufacturing qty 25 unassigned
WH/MO/00013 Urgent Westin Manufacturing qty 60 unassigned
WH/MO/00014 Normal Honeywell Aerospace qty 18 auto-routed
WH/MO/00015 Normal Amphenol Canada qty 40 routed + first WO started
WH/MO/00016 Normal Magellan Aerospace qty 32 auto-routed
Each MO is created via mrp.production.create() and confirmed through
the bridge_mrp action_confirm() override, which auto-creates the
portal job and generates ~9 WOs from the recipe with role-aware
auto-routing. Post-create the script stamps priority on every WO and
optionally clears assignments (HOT + Urgent) or starts the first WO
(MO_00015) for variety.
Recipe lookup was previously by code='ENP-ALUM-BASIC' which silently
failed because the seed file uses code='ENP_ALUM_BASIC' (underscores)
while the display name has dashes. Switched to "first available
recipe of node_type=recipe" so the script works regardless of which
spelling is canonical.
Idempotent — bails early if there are already five-plus active MOs,
so re-runs don't keep stacking new jobs.
Verified on entech: Manager Desk now shows
- 6 active MOs (was 1)
- 23 unassigned active WOs (was 2)
- 30 active+assigned WOs (was 6)
- 2 WOs in progress now
- all 7 operators with open queue (Marie 2, James 1, Carlos 8, etc.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two complementary fixes — a real bug in the Manager Desk and demo
data that exercises the now-correct view.
The bug
=======
manager_controller.py used an explicit allow-list of WO states for
its Unassigned / Active columns and for the per-operator team load
count: ('pending','waiting','ready','progress'). That set MISSED the
'blocked' state Odoo emits when a WO's predecessor isn't done yet.
Result: an MO whose first WO is still running has all its downstream
WOs in 'blocked' state. They literally don't appear on the Manager
Desk — neither in "Needs a Worker" (even when unassigned) nor in
"In Progress" (even when assigned). The team load count also
under-reports because the operator's blocked queue is invisible.
Fix: switch all three domains from an allow-list to a deny-list
('done','cancel'). Same shape Plant Overview already uses, so the
two dashboards now agree on what "active" means.
Demo data
=========
Stage-filler gains two steps so the now-corrected view has obvious
data:
6e. _populate_active_wos walks the in-flight MO's blocked routing
and explicitly assigns the seven downstream WOs in sequence
order — Diego (training), Carlos (plating), James (demask),
Priya (oven), TWO unassigned (de-rack + post-bake — feed
"Needs a Worker"), Aisha (final inspection). Earlier
keyword-fuzzy matching missed WOs whose names didn't carry
the expected substring.
6f. _mark_so_awaiting_manager pushes two confirmed SOs to
receiving_status='inspected' + assigned_manager_id=False so
the "Awaiting Assignment" KPI is non-zero.
Verified on entech: 2 unassigned WOs, 6 active+assigned, 2
awaiting-assignment SOs. Six of seven operators carry at least one
open queue item; Marie has zero current load but a healthy past
completion history (she's on shift, between jobs).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two complementary fixes — a real bug in the Manager Desk and demo
data that exercises the now-correct view.
The bug
=======
manager_controller.py used an explicit allow-list of WO states for
its Unassigned / Active columns and for the per-operator team load
count: ('pending','waiting','ready','progress'). That set MISSED the
'blocked' state Odoo emits when a WO's predecessor isn't done yet.
Result: an MO whose first WO is still running has all its downstream
WOs in 'blocked' state. They literally don't appear on the Manager
Desk — neither in "Needs a Worker" (even when unassigned) nor in
"In Progress" (even when assigned). The team load count also
under-reports because the operator's blocked queue is invisible.
Fix: switch all three domains from an allow-list to a deny-list
('done','cancel'). Same shape Plant Overview already uses, so the
two dashboards now agree on what "active" means.
Demo data
=========
Stage-filler gains two steps so the now-corrected view has obvious
data:
6e. _populate_active_wos walks the in-flight MO's blocked routing
and explicitly assigns the seven downstream WOs in sequence
order — Diego (training), Carlos (plating), James (demask),
Priya (oven), TWO unassigned (de-rack + post-bake — feed
"Needs a Worker"), Aisha (final inspection). Earlier
keyword-fuzzy matching missed WOs whose names didn't carry
the expected substring.
6f. _mark_so_awaiting_manager pushes two confirmed SOs to
receiving_status='inspected' + assigned_manager_id=False so
the "Awaiting Assignment" KPI is non-zero.
Verified on entech: 2 unassigned WOs, 6 active+assigned, 2
awaiting-assignment SOs. Six of seven operators carry at least one
open queue item; Marie has zero current load but a healthy past
completion history (she's on shift, between jobs).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion to fp_demo_seed.py. Bridges the gaps the original seeder
left after the team-skills + timer-audit + presence-aware Manager Desk
work landed (commit 0d12902). Idempotent.
Eight steps, each wrapped in a safe() driver so a failure in one
doesn't abort the rest:
1. Fill x_fc_work_role_id on any WO that doesn't have one yet.
Keyword map (mask/rack/plat/bake/oven/inspect/rework) → role
code, falls back to plating_op. The auto-promotion tracker
can't credit a worker without a role on the WO.
2. Backfill the four timer audit fields (started_by/at,
finished_by/at) on done WOs. Pulls from time_ids when the
productivity records exist, otherwise synthesises timestamps
from create_date + duration.
3. Seed a diverse team of six operators with distinct role
coverage and lead-hand permissions:
- Marie Dubois — masking + racking (lead: masking)
- James O'Connor — plating_op + demask (lead: plating_op)
- Priya Sharma — oven + inspection (lead: oven, inspection)
- Diego Ramirez — racking + plating_op (TRAINING: 2/3 masking)
- Aisha Khan — inspection + rework
- Carlos Silva — every role (lead: every role)
Each gets a backing res.users so the Manager Desk dropdown
can assign them.
3b. Redistribute ~40 historical done WOs across the new team so
their Task Proficiency lists aren't empty. Plan targets
realistic per-role counts (Marie 8 masking + 5 racking,
James 12 plating + 4 demask, etc.) and re-stamps the timer
audit so finished_by reflects the new owner.
4. Wipe + rebuild fp.operator.proficiency from completed WOs so
the per-(employee, role) tally is deterministic. Auto-promotion
fires naturally during the rebuild — workers who already cleared
the threshold get promoted=True with timestamps. Diego is
deliberately seeded at 2/3 on masking so the demo shows the
"one more job away from promotion" state live.
5. Clock three operators in via hr.attendance (4-hour shift).
Wipes any stale open records first because earlier script
iterations left future-dated check_in timestamps that the
attendance validator refused to close.
6a. Two extra quality holds (damaged + out_of_spec).
6b. Mark the in-progress WO with a started_at but no finished_at
so the demo has a "paused for lunch" exemplar.
6c. Three portal RFQs (one per workflow state: new / under_review
/ quoted) so the funnel front-end has data.
6d. Push one draft SO to "sent" so the quotation pipeline has
data in every column (was draft → confirmed previously).
Verified on entech: 21 of 21 workflow stages now ✅, including
Diego's 2/3 masking row that shows the auto-promotion mechanic
in flight.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion to fp_demo_seed.py. Bridges the gaps the original seeder
left after the team-skills + timer-audit + presence-aware Manager Desk
work landed (commit 0d12902). Idempotent.
Eight steps, each wrapped in a safe() driver so a failure in one
doesn't abort the rest:
1. Fill x_fc_work_role_id on any WO that doesn't have one yet.
Keyword map (mask/rack/plat/bake/oven/inspect/rework) → role
code, falls back to plating_op. The auto-promotion tracker
can't credit a worker without a role on the WO.
2. Backfill the four timer audit fields (started_by/at,
finished_by/at) on done WOs. Pulls from time_ids when the
productivity records exist, otherwise synthesises timestamps
from create_date + duration.
3. Seed a diverse team of six operators with distinct role
coverage and lead-hand permissions:
- Marie Dubois — masking + racking (lead: masking)
- James O'Connor — plating_op + demask (lead: plating_op)
- Priya Sharma — oven + inspection (lead: oven, inspection)
- Diego Ramirez — racking + plating_op (TRAINING: 2/3 masking)
- Aisha Khan — inspection + rework
- Carlos Silva — every role (lead: every role)
Each gets a backing res.users so the Manager Desk dropdown
can assign them.
3b. Redistribute ~40 historical done WOs across the new team so
their Task Proficiency lists aren't empty. Plan targets
realistic per-role counts (Marie 8 masking + 5 racking,
James 12 plating + 4 demask, etc.) and re-stamps the timer
audit so finished_by reflects the new owner.
4. Wipe + rebuild fp.operator.proficiency from completed WOs so
the per-(employee, role) tally is deterministic. Auto-promotion
fires naturally during the rebuild — workers who already cleared
the threshold get promoted=True with timestamps. Diego is
deliberately seeded at 2/3 on masking so the demo shows the
"one more job away from promotion" state live.
5. Clock three operators in via hr.attendance (4-hour shift).
Wipes any stale open records first because earlier script
iterations left future-dated check_in timestamps that the
attendance validator refused to close.
6a. Two extra quality holds (damaged + out_of_spec).
6b. Mark the in-progress WO with a started_at but no finished_at
so the demo has a "paused for lunch" exemplar.
6c. Three portal RFQs (one per workflow state: new / under_review
/ quoted) so the funnel front-end has data.
6d. Push one draft SO to "sent" so the quotation pipeline has
data in every column (was draft → confirmed previously).
Verified on entech: 21 of 21 workflow stages now ✅, including
Diego's 2/3 masking row that shows the auto-promotion mechanic
in flight.
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>
git mv preserves history. fusion_accounting/ retains only __manifest__.py,
__init__.py, CLAUDE.md, and docs/ — the meta-module shell. All Python,
data, views, security, services, static, tests, wizards, report move to
fusion_accounting_ai/. Manifest data list updated; security.xml move to
_core deferred to Task 12.
Made-with: Cursor
Operators run the Tablet Station on iPads (mostly landscape, sometimes
portrait). The previous design pushed the dashboard panels below the
fold on a 1024×768 viewport — meant a swipe before they could see
their queue. Tightens spacing across the page without changing the
visual language.
What changed (all behind @media (max-width: 1180px)):
- Page padding 24/32 → 16/20, gap between sections 24 → 16
- Hero title 32 → 24px, subtitle margin-top halved
- KPI strip switches from auto-fit to fixed 6-column grid so all six
KPIs stay on a single row instead of wrapping at iPad widths;
per-tile padding 20 → 12/16, value font 44 → 24px, label 14 → 12px
- Active WO banner padding 20 → 12/16
- Dashboard breakpoint to single-column lowered 1100 → 760px so
iPad portrait still gets two columns of panels
- Panel padding 20 → 16, panel-head padding-bottom 12 → 8
- Empty state padding 32/16 → 16/12 (the "All caught up" tile no
longer eats 140px per panel)
- Queue rows min-height 64 → 52, bake/gate rows 64 → 48
Station picker dropdown:
- Native chevron suppressed via appearance: none and replaced with
an inlined SVG arrow positioned with explicit right-edge inset.
Stroke uses currentColor so it follows light/dark mode.
- Right padding bumped from $fp-space-4 → $fp-space-7 to give the
arrow breathing room — previously hugged the rounded corner.
Station dropdown labels:
- Append "(CODE)" after the name. The shop's five stations
(Bake Oven Tablet / Inspection Kiosk / Plating Room Tablet 1 /
Receiving Mobile / Shipping Desktop) all live in the same facility
with no work_center, so without the code suffix the dropdown
options looked similar at a glance.
Bumped fusion_plating_shopfloor → 19.0.10.0.0. Asset bundle
regenerated as bc28f73.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Detailed task-by-task plan for executing Phase 0 of the Enterprise
Takeover Roadmap. 22 tasks covering:
- Sub-module skeletons (_core, _ai, _migration) and meta-module conversion
- Move all current AI module code into fusion_accounting_ai with git mv
- ir_model_data ownership reassignment via post-migration script
- Data adapter pattern (base + bank_rec + reports + followup + assets adapters)
- Refactor of every AI tool to route through adapters (pilot in bank_rec, then survey + per-file)
- Strip all hard Enterprise dependencies from manifests
- Enterprise-detection helper and shared-field-ownership models in _core
- Multi-company record rule on fusion.accounting.session (was a Known Issue)
- Migration safety guard that blocks Enterprise uninstall until wizard runs
- Migration wizard skeleton (per-feature migrations added by future phases)
- tools/check_odoo_diff.sh for the annual upgrade ritual
- Per-sub-module CLAUDE.md, UPGRADE_NOTES.md, README.md
- CI pipeline (or deferral note if not yet viable)
- Empirical Enterprise-uninstall verification test on a throwaway instance
- End-to-end smoke test + completion tag
Each task uses TDD where applicable (test fails, implement, test passes,
commit) and concrete validation commands where TDD doesn't fit (file moves,
config changes, manual smoke tests).
Made-with: Cursor
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>