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
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
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
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).
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>
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
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 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
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>
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
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>
Adds the brainstormed roadmap design that turns fusion_accounting from an
AI-only extension into a full replacement for Odoo 19 Enterprise accounting
(account_accountant, account_reports, accountant, account_followup, plus
selected satellites) for Nexa client deployments.
Covers:
- Sub-module topology (9 modules + meta-module): _core, _bank_rec, _reports,
_dashboard, _followup, _assets, _budget, _ai, _migration
- Data preservation strategy: bank reconciliations verified preserved
automatically (live in Community account.partial.reconcile);
shared-field-ownership pattern for Enterprise extension fields on
account.move; pre-uninstall migration wizard for Enterprise-only tables
- Phased roadmap: Phase 0 foundation through Phase 7+ optional satellites,
with Bank Rec as Phase 1 priority and Reports as the largest phase
- Architecture rules: hybrid mirror/abstract zones, fusion.* naming,
runtime coexistence detection, zero hard Enterprise deps
- Cross-version upgrade workflow: pinned Odoo source snapshots per version,
annual diff ritual, UPGRADE_NOTES.md per sub-module
- AI integration via adapter pattern (current AI tools route through
adapters that prefer fusion native, fall back to Enterprise, then to
pure Community)
- Testing strategy, security, performance, multi-company/currency,
localization, hosting
Implementation of each phase happens in subsequent sessions, each with
its own writing-plans pass starting with Phase 0 Foundation.
Made-with: Cursor
The coloured priority stripe (4px vertical bar at the card's left
edge, set via ::before pseudo) extended past the top and bottom
rounded corners of the card — visible as sharp corners on cards with
Urgent or HOT priority (yellow/red stripe).
Cause:
.o_fp_po_card::before was positioned at left/top/bottom: -1px and
given its own border-radius, but the stripe's own radii didn't
match the card's 14px radius precisely, and the -1px offsets
pushed the stripe outside the card's curves.
Fix:
1. .o_fp_po_card gets overflow: hidden. Shadows are painted outside
the content box in CSS so box-shadow still renders fine, but any
child element (including ::before) now clips to the parent's
border-radius automatically.
2. Stripe ::before simplified to left/top/bottom: 0 — no more
negative offsets, no more independent border-radius rules.
The parent's overflow does the corner-matching.
Verified in /web/assets/5e85f15/web.assets_backend.min.css:
.o_fp_po_card { ...; overflow: hidden; ... }
.o_fp_po_card::before { content: ""; position: absolute;
left: 0; top: 0; bottom: 0; width: 4px; ... }
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two problems after the previous round:
1) Mobile scroll still not working, even on a real phone.
Dug into /usr/lib/python3/dist-packages/odoo/addons/web/static/src/
webclient/webclient_layout.scss and found Odoo's mobile layout
switches scroll ownership at @media-breakpoint-down(md) (<768px):
Desktop: .o_content has overflow:auto — your content scrolls there
Mobile: .o_action gets overflow:auto, .o_content is overflow:initial
Our client action roots had `min-height: 100%` and relied on an
ancestor for scroll. That ancestor changes between breakpoints, and
somewhere in the transition scroll gets lost — the page fills but
can't scroll.
Fix: make each page OWN its scroll, like .o_content on desktop
kanban/list views. Three roots now have:
.o_fp_tablet / .o_fp_manager / .o_fp_plant_overview {
height: 100%;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
Scroll works regardless of which ancestor Odoo decides owns it at
any given breakpoint.
2) Sharp corner on column header at mobile widths.
The previous commit set `overflow: visible` on .o_fp_po_column at
<=900px trying to help scroll. But the column has border-radius: 20px
and contains .o_fp_po_col_header (which has its own background). When
overflow is visible, the header bg extends to the column's corners
without being clipped — you see squared corners on the mobile card.
Fix: keep `overflow: hidden` on .o_fp_po_column at every breakpoint
(that's what clips the rounded corners). Only lift `max-height` on
mobile so columns size to content naturally. Since the PAGE now owns
the scroll (see fix#1), the column doesn't need internal scroll —
no `overflow: auto` on the body is needed either.
Verified in compiled CSS at /web/assets/7ff5b28/web.assets_backend.min.css:
.o_fp_tablet { height: 100%; overflow-y: auto; ... }
.o_fp_manager { height: 100%; overflow-y: auto; ... }
.o_fp_plant_overview { height: 100%; overflow-y: auto; ... }
.o_fp_po_column { border-radius: 20px; overflow: hidden }
@media (max-width: 900px) .o_fp_po_column {
flex: 1 1 auto; min-width: 100%; max-width: 100%;
max-height: none; // no overflow override — hidden stays
}
Version bumped 19.0.6.0.0 -> 19.0.7.0.0 to force bundle hash change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User: "scrolling is not working" in Chrome DevTools mobile simulation.
Three actual problems:
1. Plant Overview columns had max-height: calc(100vh - 180px) +
overflow: hidden, with a nested overflow-y: auto on the column
body. Classic Trello kanban pattern — works on desktop, breaks
on mobile. You get two scroll containers fighting each other and
the PAGE itself can't scroll past the viewport height.
2. .o_fp_po_columns had overflow-x: auto on all widths. On the
phone-stack breakpoint (<600px) this was also still on, creating
another nested scroll container.
3. Draggable cards can swallow touch events on mobile because
touch-action defaults to "auto" and Chrome's mobile simulator
treats touch on draggable elements as potential drag-start.
Fixes — all at the <=900px breakpoint (tablets + phones):
.o_fp_po_column max-height: none; overflow: visible
.o_fp_po_col_body overflow-y: visible
.o_fp_po_columns flex-direction: column; overflow: visible
Plus .o_fp_po_card carries `touch-action: pan-y` unconditionally —
touch-scroll gestures never get hijacked by the draggable="true"
attribute. Desktop mousedown drag still works (HTML5 drag-drop
isn't touch-based by default).
Also added -webkit-overflow-scrolling: touch to all three page
roots (.o_fp_tablet, .o_fp_manager, .o_fp_plant_overview) and to
the internal scroll containers that remain on desktop — gives iOS
Safari proper momentum scroll (11 occurrences in the compiled
bundle).
Drag-drop JS preventDefault calls audited — they only fire on
dragover/drop (HTML5 drag events), which don't exist on touch by
default, so no touch interference there.
Verified via compiled CSS:
.o_fp_po_card { touch-action: pan-y; ... }
@media (max-width: 900px) .o_fp_po_column { overflow-x: visible;
overflow-y: visible; min-height: auto }
@media (max-width: 900px) .o_fp_po_col_body { overflow-y: visible }
Version bumped 19.0.5.0.0 -> 19.0.6.0.0 to force the bundle hash
to change. New URL: /web/assets/4a1b69e/web.assets_backend.min.css
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dug deeper after the user reported shop-floor pages staying white in
dark mode. Traced through Odoo 19 source:
_dependencies/web_enterprise/static/src/
webclient/color_scheme/color_scheme_service.js <- reads cookie
scss/primary_variables.scss \$o-webclient-color-scheme: bright
scss/primary_variables.dark.scss \$o-webclient-color-scheme: dark
Odoo compiles TWO separate CSS bundles:
web.assets_backend -> compiled with \$...scheme: bright
web.assets_web_dark -> compiled with \$...scheme: dark
(the .dark.scss files are layered in front of the light ones)
Our shop-floor SCSS is in web.assets_backend, which means it gets
compiled into BOTH bundles. But the previous CSS-variable fallback
chain (var(--fp-page-bg, var(--bs-tertiary-bg, #hex))) baked the
SAME hex fallback into both bundles, so cards stayed white in dark.
Odoo's own code doesn't redefine --bs-* CSS custom properties at
runtime either — it just bakes the dark palette straight into the
dark bundle via SCSS \$-variables during compile.
Fix: _fp_shopfloor_tokens.scss now branches at compile time:
\$o-webclient-color-scheme: bright !default;
\$_fp-page-hex: #f3f4f6; // light defaults
\$_fp-card-hex: #ffffff;
...
@if \$o-webclient-color-scheme == dark {
\$_fp-page-hex: #1a1d21 !global;
\$_fp-card-hex: #22262d !global;
...
}
\$fp-page: var(--fp-page-bg, \$_fp-page-hex);
\$fp-card: var(--fp-card-bg, \$_fp-card-hex);
The CSS-custom-property fallback stays so deployments can still skin
via --fp-* without touching SCSS; the underlying hex changes between
bundles.
Verified via odoo-shell:
LIGHT bundle: .o_fp_plant_overview { background-color: var(...#f3f4f6) }
.o_fp_po_card { background-color: var(...#ffffff);
border: ... #d8dadd }
DARK bundle: .o_fp_plant_overview { background-color: var(...#1a1d21) }
.o_fp_po_card { background-color: var(...#22262d);
border: ... #343942 }
Two separate bundle URLs generated:
/web/assets/a593157/web.assets_backend.min.css
/web/assets/a9dba7d/web.assets_web_dark.min.css
=== CLAUDE.md ===
Replaced the previous (incorrect) .o_dark_mode override advice with
a proper "Branch on \$o-webclient-color-scheme at SCSS compile time"
section, including the bundle names and the verify-via-odoo-shell
snippet. Future redesigns now have a single, correct pattern to
follow.
Version bumped 19.0.4.0.0 -> 19.0.5.0.0 to force asset hash change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two fixes + a memory entry in CLAUDE.md.
=== Dark mode ===
User: "when I change the theme the whole background does not turn
dark like the other pages does". Digging through Odoo 19 source:
/_dependencies/web_enterprise/static/src/scss/
bootstrap_overridden.dark.scss
primary_variables.dark.scss
secondary_variables.dark.scss
Odoo doesn't flip dark mode via a runtime .o_dark_mode class on the
DOM — it compiles a SEPARATE asset bundle where $o-webclient-color-
scheme: dark is set, which redefines every --bs-* token with dark
values. When the user toggles dark mode, Odoo swaps the whole CSS
bundle.
So my previous :root[data-bs-theme="dark"] { --fp-page-bg: #13161a; }
block was DEAD CODE — nothing ever sets data-bs-theme on the root.
Fixed: tokens now fall through to Bootstrap's --bs-* semantic tokens
before hitting a hex default, so they auto-invert when Odoo swaps
bundles. Three-level fallback chain:
$fp-page : var(--fp-page-bg,
var(--bs-tertiary-bg, #f3f4f6));
$fp-card : var(--fp-card-bg,
var(--bs-card-bg,
var(--o-view-background-color, #ffffff)));
$fp-border : var(--fp-border-color,
var(--bs-border-color, #d8dadd));
$fp-ink : var(--fp-ink, var(--bs-body-color, #1f2937));
Dead .o_dark_mode block removed. No runtime selector needed.
=== Quick View button ===
User: "Quick View button color is white with white button in light
mode." Cause: Bootstrap's .btn-primary loads AFTER our custom CSS
in the bundle and resets color: #fff, background: var(--bs-btn-bg)
— which clobbered our $fp-accent / $fp-ink assignment because a
later rule at the same specificity wins.
Fix: split the primary button into its own rule with higher
specificity (.o_fp_manager .o_fp_manager_head_actions .btn.btn-primary)
and !important on the three key properties — so Bootstrap can't
shout us down. Hover uses brightness(1.08) for a subtle darken
without needing another color assignment.
=== CLAUDE.md additions ===
Added two new rules documenting the lessons so this isn't relearned:
Rule 8 — Odoo 19 forbids @import in custom SCSS (silent warning,
falls back to cached bundle). Register partials in the assets list
in load order; SCSS variables cascade through the bundle.
"Card Styling — Copy Odoo's Kanban Pattern" section explaining:
- Don't rely on --bs-border-color directly for card surfaces
- Chain through $fp-* → --fp-* → --bs-* → hex
- 3-layer contrast rule (page → container → card)
- Reference _fp_shopfloor_tokens.scss as canonical
"Asset Bundle Cache Busting" section with 4-step escalation path
for when CSS changes don't show up in browser.
Verified: bundle regenerated to /web/assets/b48ab17/web.assets_backend.min.css
(id 1945). Card rule compiled with full fallback chain visible.
Primary button carries !important modifier for bg/border/color.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Why the borders weren't showing: the previous approach used
color-mix(var(--bs-body-color) 4%, var(--o-view-background-color)) for
card/column backgrounds. Under Odoo 19 the resolved values for those
variables were nearly identical to var(--bs-body-bg), so the card
surfaces visually merged into the page. Same problem for borders:
var(--bs-border-color) can render extremely faint depending on theme.
Checked what Odoo's native kanban does — dug through the compiled
CSS and found:
.o_kanban_record { background-color: white;
border: 1px solid #d8dadd; }
.o_kanban_group { background: var(--KanbanGroup-background); }
Odoo uses EXPLICIT hex values and card-specific tokens, not the
generic body/border variables. Adopted the same approach.
New tokens in _fp_shopfloor_tokens.scss — all explicit, plus a
dark-mode override block keyed off [data-bs-theme="dark"] and
.o_dark_mode (Odoo 19 uses both):
light dark
------------------------ ------------------
--fp-page-bg: #f3f4f6 #13161a
--fp-column-bg: #e9ebef #1a1e24
--fp-card-bg: #ffffff #22262d
--fp-card-soft-bg: #f8fafc #1c2027
--fp-border-color: #d8dadd #343942
--fp-ink: #1f2937 #e5e7eb
--fp-ink-mute: #6b7280 #8a909a
shadow scale switched from color-mix to explicit rgba(0,0,0,...)
so it renders identically across browsers.
All three SCSS files updated via sed to swap
var(--bs-border-color) -> #{$fp-border}
...then $fp-border resolves to var(--fp-border-color, #d8dadd) — a
proper card-level border that is VISIBLE (28 refs to --fp-card-bg
and 35 refs to --fp-border-color confirmed in the compiled bundle).
Plant Overview specifically now has:
* Column: #f8fafc bg + #d8dadd border + shadow
(column is brighter than the page it sits on)
* Column HEADER: #ffffff inside the column, with bottom border
(clear separator between stages)
* Card: solid #ffffff bg + #d8dadd border + shadow
(brightest surface, pops off the column)
* Gap between columns: 16px so the column borders don't touch
Module version bumped to 19.0.3.0.0. Bundle regenerated at
/web/assets/0cd8bc1/web.assets_backend.min.css (1.45 MB, id 1939).
Verified by parsing compiled CSS:
.o_fp_po_card: background-color: var(--fp-card-bg, #ffffff);
border: 1px solid var(--fp-border-color, #d8dadd);
.o_fp_po_column: background-color: var(--fp-card-soft-bg, #f8fafc);
border: 1px solid var(--fp-border-color, #d8dadd);
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three direct fixes responding to user feedback:
1. Drag-drop "simulation" — now works like Trello/Linear. As the
cursor moves over a column, a live DOM placeholder node is
INJECTED into the card list at the exact position the dragged
card will drop. The placeholder is a 4px pulsing accent-coloured
bar with a soft glow ring. Slides smoothly between cards as the
cursor moves. Column body also gets a tinted background + inset
accent outline for the "whole column is receptive" cue.
Previous version only tinted the column — no indicator of WHERE
the card would land. The new approach actually mimics the physical
gesture: cards visually make room for the incoming card.
2. Customer logo restored at 32×32px.
Removing it was the wrong call. It's back now as a small
thumbnail avatar (rounded 10px corners, soft border, object-fit
contain so wide logos don't squish). Sits to the left of the
customer name in the card top row. Fallback icon for customers
without a logo. Takes the same space as the step badge on the
right — compact and organised.
3. Module version bumped 19.0.1.0.0 → 19.0.2.0.0 so the asset
bundle content hash changes. The new compiled CSS is served at
/web/assets/022171c/web.assets_backend.min.css (previously
/web/assets/278b43c/...). Fresh URL forces browser to refetch —
this is what was causing the "still no border" complaint.
Verified in compiled CSS: o_fp_po_card_avatar, o_fp_po_drop_placeholder,
o_fp_placeholder_pulse keyframes, o_fp_drop_target — all present.
Zero SCSS warnings. Module upgrade clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three concrete fixes based on user feedback:
1. Card borders restored — every card / panel / KPI tile / queue row /
bake row / team card now has a thin 1px border (var(--bs-border-color))
ON TOP of the soft shadow. That's the classic SaaS card treatment
and solves the "jobs have no borders, they blend together" problem.
Hover lifts the border to the accent colour (~45% mix) so cards
feel responsive.
2. Plant Overview drop-zone indicator restored.
- Column body gets inset outline + tinted background on dragover
(.o_fp_drop_target class already added by onColDragOver in JS)
- A 56px dashed placeholder bar appears at the bottom of the column
via ::after on the drop target. That's the "here's where the card
will land" visual the user remembered.
- Dragged card gets scale(0.97) + slight rotation + opacity 0.4 for
a clearer "I'm picking this up" feedback.
3. Customer logo removed from Plant Overview cards.
The big company logo at the top of each kanban card was wasting
space. Customer NAME still shows (in bold, full-width, with text-
ellipsis), step badge pill stays on the right. No more wasted
real estate on visuals nobody looks at twice.
Extra polish while in there:
- Section headers (Tablet + Manager) now have a coloured icon badge
— a rounded square 36×36 with tinted background + accent-coloured
icon next to the H3 title. Adds visual weight without noise.
- Panel head gets a 1px bottom divider.
- Manager panels tint the icon badge per panel tone (amber for
Unassigned, green for In Progress, blue for Team).
- Header action buttons (Tablet scan/picker, Manager refresh/mode)
get proper borders + hover state.
- State dividers on bake/gate/hold rows preserved as inset shadows.
Verified: bundle rebuilt at /web/assets/278b43c/web.assets_backend.min.css
(1.45MB, id 1930). All key classes present: o_fp_drop_target,
o_fp_dragging, o_fp_po_parts_bar, o_fp_po_parts_fill, section-header
icon badges. Zero SCSS warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: the previous gradient-heavy look felt cluttered, job
cards had confusing heavy borders, the hierarchy was noisy. Wiped all
three SCSS files and both OWL templates and rebuilt from scratch with
a clean minimalist design language.
Design philosophy — the single source of truth:
* NO borders on cards — depth comes from elevation (shadow) + a
tiny surface-tint difference between page and card
* ONE accent colour (var(--o-action)); semantic red/amber/green only
for status pills and state bars
* Shadow-only cards: $fp-elev-1, $fp-elev-2, $fp-elev-3 built on
color-mix of foreground so they adapt to dark mode automatically
* Generous whitespace, 8pt spacing scale ($fp-space-1 through
$fp-space-10)
* Type-first hierarchy: 32px page titles, 44px KPI numbers, tabular
numerics so refreshing counts don't jitter
* Priority/state cues via narrow 4-6px coloured bars and small dots
— never via loud backgrounds or gradient washes
* All interactive elements at 48px touch minimum (shop-floor gloves)
New token file (_fp_shopfloor_tokens.scss) exports:
- $fp-space-1..10, $fp-radius-sm..xl, $fp-radius-pill
- $fp-page / $fp-card / $fp-card-soft surface tints
- $fp-ink / $fp-ink-soft / $fp-ink-mute / $fp-ink-faint text tiers
- $fp-elev-1..3 layered shadows
- $fp-text-xs..3xl type scale
- @mixin fp-pill, fp-focus-ring, fp-card, fp-hover-only
- fp-wash() function for state-coloured soft backgrounds
Tablet Station (fusion_plating_shopfloor.scss + shopfloor_tablet.xml):
- Clean hero: just the title, station chip, picker + scan button
- KPI cards: no gradient overlay, just a 10px coloured dot and big
44px number. Hover lifts with shadow
- Active WO: soft green wash background, no border, pulsing dot
- Panels contain queue/baths/bakes/gates/holds — all on the same
card surface with big rounded corners, no internal borders
- Queue rows: flat on a soft page-tinted background, hover slides
right 2px (no lift, cleaner)
- Bake/Gate/Hold rows: state-coloured inset shadow as a 4px stripe,
no border
- Empty states: centred with a 44px muted icon and friendly copy
Manager Desk (manager_dashboard.scss + manager_dashboard.xml):
- Matching hero with live dot that calmly pulses green during a fetch
- 4 KPI cards in the same language as the tablet
- Three panels (Unassigned / In Progress / Team) with coloured dots
next to their titles instead of top accent bars
- MO cards NO borders, subtle page-tint background, 4px left stripe
only for priority (red HOT, amber Urgent)
- Team cards: avatar + name + live load pill, hover slides right
- WO expanded rows use card-soft buttons/dropdowns for low contrast
Plant Overview (plant_overview.scss):
- Columns are now shadow-lifted cards on the tinted page background
- Kanban cards: no border, small shadow, lift on hover
- Priority stripe is an inset box-shadow (not a border) so hover
transform doesn't wobble
Backend contract preserved — OWL class names, prop signatures, RPC
endpoints, and stateBadge mapping all unchanged. Only visuals.
Verified:
* Bundle compiled to /web/assets/.../web.assets_backend.min.css
(1.45MB, id 1926)
* All 6 new classes present in compiled CSS
* Zero SCSS "forbidden import" warnings
* Zero Odoo module upgrade errors
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Odoo 19 forbids local SCSS @import statements for security reasons and
silently falls back to the OLD cached CSS bundle when it sees them. My
redesign commit used:
@import "./fp_shopfloor_tokens";
in three SCSS files. Odoo logged
WARNING Local import './fp_shopfloor_tokens' is forbidden for
security reasons. Please remove all @import {your_file} imports
in your custom files.
...and the compiled bundle kept rendering the old look. That's what
the user saw.
Fix:
1. Add _fp_shopfloor_tokens.scss as the FIRST entry in
web.assets_backend in the manifest. Odoo concatenates the bundle
in order, so variables/mixins in the first file are visible to
every later file — native @import is not needed.
2. Strip the @import "./fp_shopfloor_tokens"; line from all three
consumer files (tablet, manager, plant overview).
Verified: asset bundle regenerated to /web/assets/.../web.assets_backend.min.css
(1.45 MB). Grepped the compiled CSS and all five new classes are present:
o_fp_tablet_header, o_fp_kpi_strip, o_fp_mgr_card, o_fp_live_dot,
o_fp_panel_unassigned. 8 radial-gradients baked in. Zero warnings in
the Odoo server log post-rebuild.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Shop-floor operators and managers live on these screens all day — the
old look worked but felt like a spec sheet. Pass through all 5 pages
with one design language: gradient KPI cards, hero banners, soft
shadows, rounded corners, large touch targets, friendly empty states.
Dark mode and light mode both look deliberate, not inverted.
New shared file: _fp_shopfloor_tokens.scss
A single source of truth for radii, elevation (shadows that respect
dark mode via color-mix on foreground), typography scale (tabular
numerics for KPIs, 18px base for shop-floor readability), animation
easings, semantic gradients (@mixin fp-grad), tone helpers
(@mixin fp-tone), focus ring, and the 44px touch-min token.
Every other SCSS file imports this — no duplicated colour math.
Gradients are built on color-mix(in srgb, var(--bs-foo) X%, transparent)
so they layer naturally on either the light or dark page background.
No @media-prefers-color-scheme forks needed.
Tablet Station (fusion_plating_shopfloor.scss):
* Hero banner with dual radial-gradient wash (brand + success), live
station chip, gradient focus ring on the picker.
* KPI cards (6-up on desktop, 2x3 on phone) get a subtle top accent
line, coloured gradient overlay, 40px headline number, faded icon
at corner. Tone variants (info/success/warning/danger/muted) drive
colour without extra CSS.
* Active WO banner is a green gradient pill with a breathing-dot
pulse — unmissable when something is running.
* Panels get top accents, queue rows get priority pills (HI/M/·),
bake/gate/hold rows get colour-coded left accent bars.
* Tiles have a 4px left stripe keyed to state + hover lift.
* Status chips are uppercase, pill-shaped, tone-tinted with
color-mix so they respect theme.
* Empty states now have a large 36px icon + friendly copy instead
of a one-liner.
* Focus rings use the shared @mixin fp-focus-ring.
Manager Desk (manager_dashboard.scss):
* Same hero treatment with radial gradient + live-dot pulse.
* 3 panels carry a coloured top accent bar — amber (Unassigned),
green (In Progress), blue (Team). Instant visual routing.
* KPI strip matches tablet.
* MO cards get a left priority stripe (red for HOT, amber for
Urgent), lift on hover, expand cleanly.
* Team avatars get a border + subtle tint background for depth.
* Worker/tank pickers have custom focus rings.
Plant Overview (plant_overview.scss):
* Header is now a gradient wash tied to the brand colour.
* Work-centre columns get a thin gradient top-stripe and pill-style
count badges.
* Cards have real depth (layered shadow), lift harder on hover,
change border colour on hover.
All three files share the same design tokens, so colours/shadows/
radii are identical across pages. Edit one place, everything updates.
Verified: backend asset bundle compiles clean (no SCSS errors), zero
warnings on module upgrade, asset cache cleared for fresh delivery.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Shop-floor workers and managers use phones and iPads on the line.
The existing layouts only stacked at 1100px / 1280px, which left
everything cramped on a 375px iPhone or 390px Android. Pass through
all 5 shop-floor screens with disciplined breakpoints and touch-first
sizing.
Breakpoint ladder (consistent across files):
1400px : manager WO row: worker/tank pickers drop to their own rows
1280px : manager grid 3 → 2 columns, Team spans both
1100px : tablet dashboard 2 → 1 column
900px : manager grid → 1 column; tablet + manager padding shrinks
768px : plant overview columns stack; first-piece & bake kanbans
already handled natively by Odoo
600px : PHONE — all columns stack, everything full-width, every
button min-height 44px (Apple HIG touch target), font
shrinks for denser phone screens
Manager Desk (manager_dashboard.scss):
- Header stacks into two full-width rows on phone, action buttons
flex-grow to share the row
- 3 column grid stacks earlier (900px instead of 800px) so iPad
portrait gets a clean single-column view
- WO rows: assign/tank pickers go full-width on their own rows at
1400px, then the whole row stacks to 1 column at 600px
- Cards min 56px tap zone
- Team avatars keep their layout but cap gap on phone
Tablet Station (fusion_plating_shopfloor.scss):
- Header: picker/scan button stack full-width on phone
- KPI strip auto-fit by default, forced 2×3 grid on phone so 6
tiles stay visible without scrolling past a wall of tall cards
- Queue rows: Start/Finish buttons drop to their own row on phone,
each flexing to 50% width → easy one-thumb tap
- Bake/Gate/Hold rows: full stack on phone, action buttons flex-grow
- Bath tile grid: 2-up on phone (not auto-fit)
- Active WO banner stacks, Open-WO button full-width
- Station picker and scan input go full-width
Plant Overview (plant_overview.scss):
- Columns stack at 768px (already there) + 600px padding shrink,
search input full-width, header wraps sensibly
- Cards get min-height 64px for touch
Touch-device hover suppression:
@media (hover: none) — hover highlights were sticking after tap on
phones/iPads. Block them for .o_fp_queue_row, .o_fp_tile,
.o_fp_tablet_card, .o_fp_mgr_card, .o_fp_team_card, .o_fp_po_card.
Asset cache cleared so phones pick up the new SCSS on next load.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause of the stuck "Loading manager data..." spinner: the overview
endpoint included a search_count on sale.order.x_fc_workflow_stage,
which is a non-stored computed field. Odoo 19 raised:
ValueError: Cannot convert sale.order.x_fc_workflow_stage to SQL
because it is not stored
The controller silently logged the error; the JS caught and swallowed
the RPC failure, leaving state.overview=null forever. So the UI just
kept spinning while production changed around the manager.
Fixes:
1. Controller (manager_controller.py)
- "Awaiting assignment SOs" is now computed from STORED fields only:
state='sale' AND x_fc_receiving_status='inspected'
AND x_fc_assigned_manager_id=False
Same stage, legal SQL.
- Whole endpoint wrapped in try/except; failures return
{'ok': False, 'error': '...'} so the UI can surface them instead
of dying silently.
- Response carries a payload_hash (md5 of the JSON body minus
user_name). If the client sends back known_hash and nothing has
moved, the server returns {'unchanged': True, 'payload_hash': ...}
and the client skips the repaint entirely. Keeps the UI quiet
between polls.
2. OWL component (manager_dashboard.js)
- Poll cadence tightened from 30s → 8s (production-pace).
- Unchanged payloads don't mutate state.overview → no re-render,
no flash. Live dot just updates its tooltip.
- Changed payloads do an in-place MERGE of the overview (copying
scalars/arrays onto the existing reactive object) instead of
replacing it wholesale. OWL's diff only re-renders rows that
actually moved.
- isFetching guard so overlapping polls can't stack up.
- state.loadError surfaces backend errors in a red banner with a
Retry button — no more silent spinner.
3. UX
- Live dot next to the title: soft green at rest, bright green
pulsing during a fetch.
- "Updated Xs ago" subtitle uses a getter so the label freshens
between polls.
- Manual Refresh button next to Quick/Detailed toggle.
- Spinner only appears on the genuine first load; gone forever
once the first payload lands.
Verified: the old crashing query now runs clean on demo data; odoo
logs show zero errors for the last 5 minutes of polling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The earlier description templates were global — same 8 generic texts
applied to any part. That's useless when a customer has 3,500 parts
and each part has 3–5 canned variants (standard, masked threads,
masked bore, rework, rush packaging). That's ~17,500 rows total, and
the variants ONLY make sense in the context of a specific part number.
Restructured so descriptions live on each part:
Model changes:
fp.sale.description.template.part_catalog_id (new M2O, indexed,
ondelete cascade) — the primary scoping field
fp.sale.description.template.partner_id — now a related store=True
field pulled from the part, so customer-level search still works
fp.part.catalog.description_template_ids (new O2M inverse) — the
5–10 canned descriptions attached to this specific part
fp.part.catalog.description_template_count (computed)
UI changes:
Part Catalog form: new "Descriptions" notebook page with inline
editable list (sequence + name + tag + description + usage_count).
5 variants take 30 seconds to enter.
Part Catalog form: new smart button "Descriptions" showing the count,
jumps to the full list filtered by this part.
Template list view: part_catalog_id column added, list ordered by
part first. Search view adds Part filter + Part-Specific /
Generic (No Part) filters + Group By Part.
Wizard changes:
description_template_id domain now prioritises part-specific, falls
through to partner, coating, or generic on a single dynamic domain.
_onchange_suggest_template priority: part → customer → coating →
none. No longer auto-picks a random global template when a part
has its own.
Smoke-tested on VS-HSA201-B (Amphenol):
5 canned variants seeded on the part form
Wizard with this part auto-suggested the lowest-sequence one
The part's Descriptions smart button shows "5"
Bulk data entry path for the client's 3,500 parts: either use the
inline list on each part form, or import via CSV with the new
part_catalog_id column (external_id or DB id).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes the worker-access story. Handoffs now route themselves.
New model fp.work.role with 8 seeded defaults (noupdate so shops can
rename/prune):
masking · racking · plating_op · demask · oven · derack ·
inspection · rework
Each one has a code, icon, description, sequence, active flag.
Config menu: Configuration → Shop Roles (manager-only).
Field additions:
hr.employee.x_fc_work_role_ids (Many2many) — tag workers with the
roles they perform. One-person shop: one employee, every role.
Specialised shop: one role per employee. Cross-trained: multiple.
fusion.plating.process.node.x_fc_work_role_id (Many2one) — tag
each recipe operation with the role that performs it.
mrp.workorder.x_fc_work_role_id (Many2one) — copied from the recipe
operation on WO generation.
Auto-assignment on WO generation:
_generate_workorders_from_recipe() now copies the operation's role
onto the WO, then calls _fp_pick_worker_for_role() which picks the
least-loaded employee (active WO count) with that role. WO lands in
their Tablet "My Queue" the moment the MO is confirmed. No manual
routing needed for the common case.
Tablet Station — worker mode:
/fp/shopfloor/tablet_overview now filters to WOs where
x_fc_assigned_user_id == env.user when the field is populated.
KPIs (WOs Ready / In Progress) reflect the logged-in worker's load,
not shop-wide totals. "My Queue" rows carry wo_state + can_start +
can_finish so inline Start/Finish buttons appear.
New JS handlers onStartWo / onFinishWo call /fp/shopfloor/start_wo
and /fp/shopfloor/stop_wo (finish=true). One-tap progression.
Views:
hr.employee form gets a "Shop Roles" notebook page with many2many_tags.
Process node form gets x_fc_work_role_id inline after work_center_id.
Work Order form shows role + assigned worker.
Smoke-tested end-to-end on WH/MO/00010:
Masking → Administrator (masking role)
Racking → Administrator (racking role)
E-Nickel → Andrew (plating_op, least-loaded tiebreaker)
Demask → Administrator (masking)
Oven bake → Andrew (oven)
Derack → Administrator (racking fallback)
Post-plate QA → Administrator (inspection)
80 existing WOs backfilled with role + worker via name-match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New client action "Manager Desk" under Shop Floor menu (manager-only).
Three-column dashboard designed for the shop manager's daily job:
Column 1 — Needs a Worker
MOs with active WOs missing user assignment. Each card expands to
show per-WO rows with:
- Assign Worker dropdown (pulls from group_fusion_plating_operator)
- Tank swap dropdown (all tanks with current bath)
- Take Over (claims for the manager in one click)
- Open (jump to WO form)
Column 2 — In Progress
MOs with workers actively running WOs. Shows who's on each step,
lets manager reassign or take over if someone steps away.
Column 3 — Team
Avatar grid of operators with live queue + in-progress counts.
Click to drill into that operator's full WO list.
KPI strip on top: Unassigned WOs, In Progress, Ready to Ship, Awaiting
Assignment SOs.
Quick / Detailed view toggle — Detailed auto-expands every card body.
New field mrp.workorder.x_fc_assigned_user_id (indexed, tracked) —
the worker currently owning this step. Will be the pivot the Tablet
Station filters on in Chunk 4.
Three new endpoints:
/fp/manager/overview — dashboard snapshot (30s auto-refresh)
/fp/manager/assign_worker — set user on a WO
/fp/manager/assign_tank — swap tank on a WO
/fp/manager/take_over — manager claims the WO (no-show coverage)
Controller is graceful when mrp module isn't installed (empty overview,
no crash) and when the bridge_mrp assignment field isn't present (falls
back to showing all active WOs as "unassigned").
Verified: 4 WOs assigned across 2 users, overview queries return the
expected counts, res.groups.user_ids (Odoo 19 API, not deprecated .users).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>