batch_action_bar exposes bulk Suggest-for-selected and Auto-reconcile-selected
toolbar driven by selectedIds prop and the bank_reconciliation service.
reconcile_model_picker is a quick-pick dropdown over account.reconcile.model
records (rule_type=writeoff_button) including the Fusion AI confidence
threshold; apply path is a state-only stub pending Task 38's dedicated endpoint.
Made-with: Cursor
ai_suggestion_strip (inline confidence badge + accept), ai_alternatives_panel
(expandable other-options), ai_reasoning_tooltip (score breakdown). These
go beyond Enterprise's bank_rec_widget which has no AI suggestions.
Made-with: Cursor
After the release-ready refactor in 11837ed the unassigned/active
split runs in Python on `all_active_wos`, so the old SQL domains
(`domain_unassigned`, `domain_active`) no longer exist — but the KPI
block still referenced them via `MrpWO.search_count(domain_unassigned)`.
Manager page crashed with `name 'domain_unassigned' is not defined`.
Fix: derive the KPIs from the in-memory recordsets we just split, no
re-query. Also documents why we can't SQL-count: x_fc_is_release_ready
is a non-stored compute, so search_count would silently miss the
release-ready predicate.
Mirrors 3 OWL components from account_accountant for Phase 1
structural parity:
- quick_create/ (BankRecQuickCreate + BankRecQuickCreateController
for inline missing-record creation)
- chatter/ (BankRecChatter — extends @mail Chatter with a
reloadParentView hook for the bound statement line)
- file_uploader/ (BankRecFileUploader — extends @account
DocumentFileUploader to inject statement_line_id into the
upload context, targeting account.bank.statement.line)
Renames applied per spec; CSS class
`o_bank_reconciliation_quick_create` ->
`o_fusion_bank_reconciliation_quick_create`.
Manifest version bumped to 19.0.1.0.15.
Module upgrade succeeds, 134 logical tests still pass — completing
the Phase 1 OWL component mirror (Tasks 30-33). All 14 components
across 4 batches are now bundled.
Made-with: Cursor
**1. Manager Desk: WO no longer jumps to "In Progress" on partial setup**
User-reported bug: when the manager picked a worker, the WO immediately
left the "Unassigned" column even though the bath/tank (or oven, rack,
masking material) wasn't set yet. Worker would see a half-set job in
their queue and couldn't start it.
Fix:
- New compute `mrp.workorder.x_fc_is_release_ready` — True only when
every field button_start would block on is filled in.
- Companion `x_fc_missing_for_release` — comma-list of what's still
missing (used by the UI as a hint chip).
- Manager controller swaps the column filter from
`assigned_user_id == False` to `is_release_ready == False`.
- A WO stays in "Setup Pending" (formerly Unassigned) until BOTH
worker + per-kind equipment are set; only then does it move to
"In Progress".
**Manager Desk template + SCSS**
The user also said "the manager doesn't know what task they're
assigning". WO row now shows:
• Colour-coded WO-kind badge (wet=blue, bake=red, mask=yellow,
rack=grey, inspect=green)
• Required-role icon + name
• Bath / oven / rack / masking-material chips (whatever's set)
• Yellow "Needs: ..." chip listing what's still missing
• Tank picker only shows for wet WOs (no point on a mask WO)
• Open-WO button to drill into the form for advanced edits
**2. Six enforcement gates patched (without breaking the workflow)**
Each gate fires AFTER the manager sets up the WO and the operator
hits Start/Finish — never on create — so the manager → worker → run
flow stays intact.
| # | Gate | Where |
|---|---|---|
| a | SO confirm requires `client_order_ref` (or x_fc_po_number) | sale_order.action_confirm |
| b | Cert issue requires thickness readings (when partner.x_fc_strict_thickness_required) | fp_certificate.action_issue |
| c | Delivery start_route requires assigned_driver_id | fp_delivery.action_start_route |
| d | Bath log create/save requires line_ids (no empty logs) | fp_bath_log create + @api.constrains |
| e | Quality hold: hold_reason + description now `required=True` | fp_quality_hold field schema |
| f | Receiving accept blocks qty mismatch (manager override allowed + logged) | fp_receiving.action_accept |
New partner flag `x_fc_strict_thickness_required` so commercial
customers don't get blocked but aerospace customers do.
**Verified** via `scripts/fp_enforcement_audit.py`: 18/22 ENFORCED
(2 "GAPS" + 2 "ERRs" are all test artifacts — admin bypass + NOT NULL
fires before my custom check; real gates are correct).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors 5 OWL components from account_accountant for Phase 1
structural parity:
- button/ (single action button)
- button_list/ (toolbar of buttons + dropdown + hotkeys)
- line_to_reconcile/ (editable matched-line editor)
- list_view/ (list view + many2one multi-edit field)
- apply_amount/ (amount application html field)
Renames applied per spec (template names, module IDs, CSS classes).
Notes / deferred to fusion-only Tasks 34-36:
- list_view extends @web ListController instead of Enterprise's
AttachmentPreviewListController; setSelectedRecord is a no-op
pending the previewer pane mirror.
- View/field registry IDs prefixed with `fusion_` to coexist with
Enterprise's account_accountant when both modules are installed
(`fusion_bank_rec_list`, `fusion_bank_rec_dialog_list`,
`fusion_apply_amount_html`, `fusion_bank_rec_list_many2one_multi_id`,
`fusion_bankrec_edit_line`).
- button_list still references Enterprise view_refs in dialog
contexts (`account_accountant.view_account_list_bank_rec_widget`
etc.) for parity; the `set_*` ORM methods on
account.bank.statement.line are Enterprise-only too. These call
sites only fire when the mirrored components are actually
rendered, which Phase 1 does not exercise.
Manifest version bumped to 19.0.1.0.13.
Module upgrade succeeds, 134 logical tests still pass.
Made-with: Cursor
Top-level OWL component (BankRecKanbanController) hosts the bank
reconciliation widget. Reads journal_id + company_id from action context,
initializes the fusion_bank_reconciliation service, and renders the
layout: header (stats), left column (line cards via BankRecLineCard
renderer), right column (detail panel with AI suggestions).
Custom view type 'fusion_bank_rec_kanban' registered so window actions
can use <field name="view_mode">fusion_bank_rec_kanban</field>.
Made-with: Cursor
Optimistic remove was decrementing unreconciledCount before assigning
the authoritative server count, leading to off-by-one. Order swapped:
remove first, then overwrite with server count.
Caught by Task 28 subagent self-review.
Made-with: Cursor
Central data layer + reactive state for the OWL widget. Wraps the 10
JSON-RPC endpoints from the bank_rec_controller (get_state,
list_unreconciled, get_line_detail, suggest_matches, accept_suggestion,
reconcile_manual, unreconcile, write_off, bulk_reconcile,
get_partner_history). Components inject via useService("fusion_bank_reconciliation").
State held in OWL's reactive() so components auto-rerender on
selection / pagination / reconcile-success changes.
Verified: web.assets_backend bundle includes
/fusion_accounting_bank_rec/static/src/services/bank_reconciliation_service.js;
134/134 module tests pass.
Made-with: Cursor
Provides design tokens (variables.scss), main bank-rec stylesheet,
AI suggestion strip + alternatives panel styling, and dark mode
overrides. CSS classes (.o_fusion_*) will be consumed by OWL components
in Tasks 28-36.
Verified: all 4 SCSS files compile via libsass; web.assets_backend
bundle picks up all 4 entries; 134/134 module tests pass.
Made-with: Cursor
All endpoints route through fusion.reconcile.engine via BankRecAdapter
(or directly for engine methods adapter doesn't expose). Uses V19's
type='jsonrpc' (replacement for deprecated type='json'). Auth=user.
Endpoints:
- get_state, list_unreconciled, get_line_detail (read)
- suggest_matches, accept_suggestion (AI surface)
- reconcile_manual, unreconcile, write_off, bulk_reconcile (write)
- get_partner_history (precedent + pattern read)
Tests use HttpCase to exercise the real Werkzeug stack as a Fusion
Accounting administrator. Includes a smoke test for the deferred
write-off path (Task 12) and a negative test confirming auth='user'
rejects anonymous requests. Helper _make_pair shares one bank journal
across pairs to avoid the (code, company) unique-constraint collision
that the default factory would hit on repeat calls.
Verified: 11/11 controller tests pass, 134/134 module tests pass.
Made-with: Cursor
Follow-up to the company-level UoM defaults commit. Wires four more
unit-bearing fields to inherit from res.company defaults at create-time.
**1. fp.bake.oven**
• New `target_temp_uom` (°F / °C) — defaults from
company.x_fc_default_temp_uom.
• View: target_temp_min / max now render with a unit picker on the
same row instead of unitless floats. Rule of thumb: "350–380 °F".
**2. fp.bake.window**
• New `bake_temp_uom` — defaults from company.x_fc_default_temp_uom.
• View: replaced hardcoded `°F` span with a live unit picker so the
label matches whatever unit was actually recorded.
**3. fp.coating.config**
• New `bake_temperature_uom` — defaults from company.
• Removed hardcoded "Bake Temperature (°F)" label; the field is
now unit-agnostic and the unit travels with the value.
**4. fp.tank.volume_uom**
• Default now derives from company.x_fc_default_volume_uom via a
small mapping (gal → gal_us, L → l, imp_gal → gal_imp). The
selection itself stays the same — tanks already supported all
common volume units; we just pre-pick the right one per company.
**Verified end-to-end** (scripts/fp_uom_smoke2.py):
• Switching company default to °C + Litres
• New oven gets C ✓
• New bake window gets C ✓
• New coating config gets C ✓
• New tank gets `l` ✓ (mapped from company `L`)
• Restored defaults afterwards
Existing records keep their stored uom — no surprise mutation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Different facilities use different measurement systems. North-American
aerospace shops live in °F + mils + gallons + lb; ROW + most metric
shops use °C + microns + litres + kg. Add company-level defaults so
each shop picks its units once; new records inherit them automatically.
**Settings on res.company** (7 Selection fields):
• x_fc_default_temp_uom — °F / °C
• x_fc_default_thickness_uom — mils / microns / inches / mm
• x_fc_default_volume_uom — US gal / litres / Imp gal
• x_fc_default_mass_uom — lb / kg / oz / g
• x_fc_default_pressure_uom — psi / bar / kPa
• x_fc_default_current_density_uom — A/ft² (ASF) / A/dm² (ASD)
• x_fc_default_area_uom — sq in / sq ft / cm² / m²
All default to North-American aerospace conventions (F, mils, gal, lb,
psi, asf, sq_in) — admins flip them once during onboarding via
Settings → Fusion Plating → Units of Measure.
**Per-record use** (this round)
• mrp.workorder.x_fc_bake_temp_uom (°F / °C) — defaults from company,
operator can override per WO if a specific bake needs a different
unit (rare but allowed).
• Bake-finish gate error message now reports the actual unit:
"Bake Temp (°F)" or "Bake Temp (°C)" instead of hard-coded F.
• Form: Bake Temp + Temp Unit picker side-by-side in the bake group.
**Settings UI** — new "Units of Measure" block on Settings → Fusion
Plating page with help text per unit explaining where each is used.
**Verified end-to-end** (scripts/fp_uom_smoke.py):
• All 7 defaults populate with NA-aerospace defaults
• Switching company default to °C makes a NEW WO inherit °C
• Existing WOs keep their stored °F (no surprise mutation)
**Roadmap (deferred to next round)** — wire the same default-from-company
inheritance to:
• fp.bake.oven.target_temp (currently no UoM)
• fp.bake.window.bake_temp (currently no UoM)
• fp.coating.config.bake_temperature (currently no UoM)
• fp.tank.volume already has volume_uom; default from company
• fp.bath.log chemistry readings already use parameter.uom; align
with company default for new params
The settings + framework are now in place — adding more per-record uom
fields is mechanical from here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- cron_suggest (every 30min): warm AI suggestions for unreconciled lines
that don't have a recent pending one
- cron_pattern_refresh (daily 02:00): recompute fusion.reconcile.pattern
for each (company, partner) pair with precedents
- cron_mv_refresh (every 5min): REFRESH MATERIALIZED VIEW CONCURRENTLY
using a dedicated autocommit cursor (REFRESH CONCURRENTLY can't run
inside a regular Odoo transaction)
V19 note: ir.cron dropped the numbercall field, so the data XML omits
it (cron now repeats indefinitely as long as active=True).
Tests: 5 new TestFusionBankRecCron tests pass; full module suite is
0 failed / 0 errors of 123 logical tests on westin-v19.
Made-with: Cursor
Bug: in Odoo 19, `required="1"` on a field inside an `invisible="..."`
group still triggers the missing-required-field flag — paints the
whole tab red on EVERY WO regardless of whether the field is shown.
Symptom: Process Details tab was red on masking, racking, oven, etc.
because the rack and mask groups' required fields were always
flagged as missing even when their parent group was hidden.
Fix: switch `required="1"` to `required="x_fc_wo_kind == 'rack'"` and
`required="x_fc_wo_kind == 'mask'"` so the required flag only fires
when the field is actually relevant. Matches the existing pattern
on bath/tank/oven (`required="x_fc_requires_bath"` etc.).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three issues surfaced when running the MV smoke tests against westin-v19:
1. account_bank_statement_line has no `date` column in V19 — `date` is a
related field flowing through move_id -> account_move.date. The MV
now JOINs account_move and selects am.date.
2. is_reconciled is nullable; replace `= FALSE` with `IS NOT TRUE` so
nulls (genuinely unreconciled lines that haven't had the compute run
yet) are still included.
3. _refresh() now flushes the ORM cache (env.flush_all()) before the
REFRESH so computed-stored fields like is_reconciled are written to
the DB before the materialization snapshot reads them. Previously the
reconcile-then-refresh path saw the pre-reconcile column value.
4. _trigger_mv_refresh() (suggestion create/write hook) now uses
concurrently=False because Postgres forbids
REFRESH MATERIALIZED VIEW CONCURRENTLY inside a transaction block,
and Odoo's per-request cursor is always inside one. The cron path
(Task 25) will open an autocommit cursor for CONCURRENTLY refreshes.
5. Tests dropped the env.cr.commit() pattern: Postgres always shows a
transaction its own writes, so a non-CONCURRENTLY refresh in the
same txn picks up freshly-inserted rows. Cleaner + works inside
TransactionCase, which forbids cr.commit().
Verified: 4 new MV tests pass, 0 failures across 118 logical tests
(178 with parametrized property-based runs) of fusion_accounting_bank_rec
on westin-v19.
Made-with: Cursor
CREATE MATERIALIZED VIEW fusion_unreconciled_bank_line_mv pre-computes
the data the kanban widget needs (top suggestion, confidence band,
attachment count, partner reconcile hint) so that listing 50-100 lines
is one indexed query instead of N+1.
Refresh strategy:
- Triggered on fusion.reconcile.suggestion create/write (best-effort,
never poisons the originating transaction)
- Cron (every 5 min) — added in Task 25
The MV is created in the model's init() (Odoo calls this on
install/upgrade). The SQL DDL is idempotent
(CREATE MATERIALIZED VIEW IF NOT EXISTS / CREATE INDEX IF NOT EXISTS)
and includes a UNIQUE(id) index so REFRESH MATERIALIZED VIEW
CONCURRENTLY is supported. _refresh() falls back to a blocking refresh
on the first call after creation.
Made-with: Cursor
Per-step audit caught real enforcement bugs across all 9 WO kinds.
Five gates added/fixed; backfill applied; verification audit shows
0 CRITICAL gaps remaining.
**1. Bake-WO finish gate** (`_fp_check_required_fields_before_finish`)
button_finish on a bake WO blocks unless:
• x_fc_bake_temp set (Nadcap req — actual setpoint)
• x_fc_bake_duration_hours set (actual run time)
• x_fc_oven_id.chart_recorder_ref set on the oven
(so the chart for THIS run can be retrieved by an auditor)
**2. Rack-WO start gate** added to button_start.
**3. Classifier priority fix** (`_fp_classify_kind`)
Reordered so specific keywords win over the broad wet-keyword fallback:
inspect → mask → bake → rack, then workcenter family, then wet.
"Post-plate Inspection" now → inspect (was wrongly → wet).
"Oven bake (Post de-rack)" now → bake (was wrongly → rack).
**4. Auto-populate** target_thickness + dwell_time at WO generation.
Plating WOs inherit thickness/uom from coating_config and dwell from
recipe node estimated_duration.
**5. Mask-WO start gate + masking_material field**
New x_fc_masking_material Selection (tape/plug/paint/silicone/wax/...).
Required to start mask/de-mask WO. Each material requires a different
removal process when stripping later.
**View** — Process Details tab branches by kind:
wet → Bath/Tank/Rack/Thickness/Dwell
bake → Oven/Temp/Duration
rack → Rack/Fixture
mask → Masking Material
inspect/other → informational alerts
**Backfill** (`scripts/fp_backfill.py`) — idempotent catch-up:
• chart_recorder_ref on every oven (1)
• rack_id on existing rack/de-rack WOs (91)
• bake_temp + bake_duration on existing bake WOs (33)
• masking_material on existing mask WOs (62)
• thickness/dwell on existing plating WOs (38)
• Cleared 7 legacy bath/tank from inspection WOs that the OLD
wet-keyword classifier had wrongly tagged.
**Per-step audit** (`scripts/fp_per_step_audit.py`)
Walks every WO of the most recent done MO; reports per-kind which
compliance fields are filled vs missing. Re-runnable for regressions.
**Final verification** on freshly-run MO:
• 0 CRITICAL gaps across all 9 WO steps
• 2 IMPORTANT (dwell_time + rack_id on E-Nickel Plating — both
inherited from recipe node data, not enforcement bugs)
• Classifier correct for all 9 step types
12 negative tests still passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-step audit caught real enforcement bugs across all 9 WO kinds in
the recipe (Masking, Racking, Plating, De-Masking, Oven baking, etc.).
Five gates added or fixed; 0 CRITICAL gaps remain after a verification
run on a fresh MO.
**1. Bake-WO finish gate** (`_fp_check_required_fields_before_finish`)
button_finish on a bake WO now blocks unless:
• x_fc_bake_temp set (Nadcap req — actual setpoint, not just oven)
• x_fc_bake_duration_hours set (actual run time at temp)
• x_fc_oven_id.chart_recorder_ref set (so the chart for THIS run
can be retrieved by an auditor — required for AS9100/Nadcap)
Run-time data lives at FINISH, not START — operators don't know
temp/duration until the bake is done.
**2. Rack-WO start gate** added to the existing button_start gate.
Per-rack life tracking + which physical fixture handled the parts.
**3. Classifier priority fix** (`_fp_classify_kind`)
"Post-plate Inspection" was matching the `plat` wet keyword and
getting kind=wet (then required to have bath/tank). Reordered:
1. Explicit equipment links (bath_id/oven_id)
2. Specific keywords (inspect → mask → bake → rack)
— bake before rack so "Oven bake (Post de-rack)" → bake
3. Workcenter wet families
4. Wet name keywords as last fallback
**4. Auto-populate target_thickness + dwell_time** at recipe→WO
generation. Plating WOs inherit:
• thickness_target from coating_config.thickness_max
• thickness_uom from coating_config.thickness_uom
• dwell_time_minutes from recipe node's estimated_duration
So aerospace QC has the spec target on every WO without paper.
**5. Mask-WO start gate + masking_material field**
New x_fc_masking_material Selection (tape/plug/paint/silicone/wax/
mixed/other). Required to start a mask WO. Needed later when
stripping or replating because each material requires a different
removal process.
**View** (`mrp_workorder_views.xml`)
Process Details tab now branches by kind:
wet → Bath/Tank/Rack/Thickness/Dwell
bake → Oven/Temp/Duration
rack → Rack/Fixture
mask → Masking Material
inspect/other → informational alerts only
WO Kind shows as colour-coded badge in header.
**Backfill** (`scripts/fp_backfill.py`)
Idempotent script that catches up existing data:
• chart_recorder_ref on every oven
• rack_id on existing rack/de-rack WOs (91 backfilled)
• bake_temp + bake_duration_hours on existing bake WOs (33)
• masking_material on existing mask WOs (62)
• thickness/dwell on existing plating WOs (38)
• Cleared 7 legacy bath/tank from inspection WOs that had been
misclassified by the OLD wet-keyword classifier.
**Per-step audit** (`scripts/fp_per_step_audit.py`)
Walks every WO of the most recent done MO and reports per-kind
which compliance fields are filled vs missing. Re-runnable to
catch regressions.
**Final state on freshly-run MO 00049:**
• 0 CRITICAL gaps
• 2 IMPORTANT gaps (dwell_time + rack_id on E-Nickel Plating —
both inherited from recipe node data, not enforcement bugs)
Negative tests still passing (12 total).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When fusion_accounting_bank_rec is installed, match_bank_line_to_payments
and auto_reconcile_bank_lines now use fusion.reconcile.engine via the
BankRecAdapter, gaining precedent recording, AI suggestion superseding,
and shared validation. Legacy paths preserved for Enterprise/Community-
only installs (engine model absent -> fall back to set_line_bank_statement_line
and _try_auto_reconcile_statement_lines).
Also wraps engine.reconcile_batch's per-line loop in a savepoint so a
single bad line's DB error (e.g. check-constraint violation) no longer
poisons the whole batch transaction; the existing per-line try/except
now isolates failures as originally intended.
Made-with: Cursor
Adds fusion_suggest_matches, fusion_accept_suggestion,
fusion_reconcile_bank_line, fusion_unreconcile, and
fusion_get_pending_suggestions. All route through the BankRecAdapter
(or direct engine for ones the adapter doesn't expose), giving the AI
chat the same reconciliation surface a human operator gets in the OWL UI.
Made-with: Cursor
Enhances list_unreconciled_via_fusion to include fusion fields
(top_suggestion_id, confidence_band, attachment_count). Adds 3 new
adapter methods that proxy the engine: suggest_matches, accept_suggestion,
unreconcile. AI tools (Task 22+) and OWL controller (Task 26) will call
these adapter methods instead of touching the engine directly.
Made-with: Cursor
Provider-agnostic system + user prompt builder for the confidence
scoring pipeline's Pass 3 (AI re-rank). Output contract is JSON with
"ranked" array; works with OpenAI, Claude, and local OpenAI-compatible
servers (LM Studio, Ollama).
Made-with: Cursor
Two engine bugs caught by Task 19's integration tests:
1. Partial reconcile (bank_amount < invoice_residual) was creating an
unbalanced bank move. Counterpart balance now clamped to
min(remaining_bank_amount, abs(invoice_residual)) so the move stays
balanced; Odoo's reconcile() handles the resulting partial. The
counterpart's amount_currency is scaled proportionally so multi-
currency lines stay consistent.
2. Unreconcile only removed account.partial.reconcile rows but didn't
restore the suspense line on the bank move, leaving is_reconciled=True
after unreconcile. Now delegates to V19's standard
account.bank.statement.line.action_undo_reconciliation for any
affected bank line, which both deletes partials and restores the
suspense state in one shot.
Made-with: Cursor
Tests engine behavior using factories (Task 18) instead of SQL fixtures.
Covers simple match, partial chain, multi-invoice batch, suggest-then-
accept flow, unreconcile reversal, and edge cases.
Two tests are intentionally failing — they expose real engine bugs
that should be fixed in a follow-up:
- TestReconcilePartialChain.test_partial_reconcile_leaves_residual:
reconcile_one() builds counterpart vals using the full invoice
residual, which leaves the bank move unbalanced when bank amount
is smaller than the invoice (UserError: entry not balanced).
- TestUnreconcile.test_unreconcile_removes_partial: unreconcile()
unlinks partial.reconcile rows but does not restore the suspense
line on the bank move, so account.bank.statement.line.is_reconciled
remains True after reversal.
Made-with: Cursor
Provides make_bank_journal, make_bank_statement, make_bank_line,
make_invoice, make_vendor_bill, make_suggestion, make_pattern,
make_precedent, make_reconcileable_pair helpers used across the
bank-rec test suite. Replaces the original plan's SQL-fixture capture
with programmatic factories — same testing intent, simpler maintenance,
no real Westin data baked into the repo.
Note: the original plan called for 5 SQL fixtures captured from the local
DB (westin_simple_match.sql, westin_partial_chain.sql, etc.). Those are
replaced by factory-driven test creation in Task 19 — eliminates fragile
hand-curated SQL while testing the same code paths.
Made-with: Cursor
Replaces the upstream Odoo icons with the purple-pink-orange V mark
so all three modules show consistent Fusion branding in the Apps list
and settings UI.
Same icon file across all three so they read as a family. Upstream
had its own icon.png on the `iot` module which this overwrites.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds fusion.reconcile.engine — the AbstractModel orchestrator for all
bank-line reconciliations. Six public methods (reconcile_one,
reconcile_batch, suggest_matches, accept_suggestion, write_off,
unreconcile) form the only sanctioned write path to
account.partial.reconcile from the rest of the module (controllers, AI
tools, wizards).
Implementation follows V19's bank_rec_widget pattern: rewrite the bank
move's suspense line into one counterpart per matched invoice (or a
write-off line) on the appropriate receivable / payable / write-off
account, then call account.move.line.reconcile() on each pair. Records
a precedent row per reconcile for downstream pattern learning.
16 new unit tests cover all six methods across happy paths, the
precedent side effect, suggestion lifecycle, batch auto-strategy, and
write-off line clearance. 67 total tests, 0 failed.
Made-with: Cursor
User caught two related issues from screenshots of the WO form:
1. The "Plating Details" tab was meaningless for non-wet WOs —
bath/tank/dwell/thickness all show as empty for masking, oven
bake, racking, and inspection steps. A shop with multiple ovens
had no way to record which oven a bake WO ran in.
2. When there's only ONE option (single oven, single bath), forcing
the manager to pick it on every WO is busywork — pin it
automatically.
**1. WO classification + per-kind equipment**
New `x_fc_wo_kind` (compute, non-stored) Selection field that buckets
each WO into one of: wet / bake / mask / rack / inspect / other.
Classification by priority:
• bath linked → wet
• oven linked → bake
• workcenter's process families wet → wet
• WO name keyword match (bake/oven/cure → bake;
mask/de-mask → mask; rack/de-rack → rack;
inspect/qa/qc/fai → inspect; default → other)
New equipment fields per kind:
• `x_fc_oven_id` (m2o fp.bake.oven) for bake WOs
• `x_fc_bake_temp`, `x_fc_bake_duration_hours` — bake parameters
• Existing bath/tank/rack/thickness reused for wet
• Existing rack reused for rack WOs
**2. Required-fields gate extended**
button_start now also requires `x_fc_oven_id` for bake WOs (alongside
the existing operator + bath/tank rules). Without an oven the
chart-recorder trail can't be tied back to the WO for compliance.
**3. View reorganized**
Process Details tab now shows only the equipment groups that apply
to this WO's kind (using `invisible="x_fc_wo_kind != 'bake'"` etc.).
Mask + Inspection + Other show informational alerts instead of
empty form fields. WO header shows a colour-coded kind badge.
**4. Smart auto-fill defaults**
New `_fp_autofill_default_equipment()` method on mrp.workorder. When
the facility has exactly ONE active option, it pre-pins:
• Bath → if facility has 1 active bath
• Tank → if the chosen bath has 1 active tank
• Oven → if facility has 1 active oven
Hooked from:
• `@api.onchange('workcenter_id', 'x_fc_facility_id', 'x_fc_bath_id')`
→ fills as user edits in the form
• Recipe → WO generation `_generate_workorders_from_recipe()`
→ fills at creation time so single-line shops never see an
empty bath/oven field
None of this overwrites an already-set value. Multi-line shops still
get a blank field to choose from.
**Simulator updates** (scripts/fp_e2e_workforce.py)
• Creates an oven if none exists
• Pins per-kind equipment in Hannah's planning step
• New PASS check: bake-WO auto-pinned to default oven
• New negative test 2b: bake WO with oven stripped → blocked
**Final E2E**: 54 PASS / 2 WARN / 0 FAIL out of 56 checks.
12 negative tests passing — all gates fire when triggered:
Tests 1-2 + 2b: WO start (operator + bath/tank + oven)
Tests 3-7: MO facility, cert spec, delivery POD, invoice
payment terms, thickness cal std
Tests 8-11: NCR close, CAPA close, discharge close, invoice ref
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase A of the IoT initiative — gets the server-side infrastructure
in place before the Raspberry Pi hardware arrives, so the iot admin
UI + /fp/iot/ingest endpoint are ready to accept the first real
temperature reading as soon as the Pi is wired up.
New top-level folder: fusion_iot/
1. **iot_base/** — Odoo S.A. iot_base module, copied from
RePackaged-Odoo verbatim. LGPL-3 upstream, no changes needed.
2. **iot/** — Odoo S.A. iot module, repackaged:
- `models/update.py` neutralised (removed the publisher_warranty
IoT-Box-counting report that phones home to odoo.com for
enterprise licence enforcement)
- `iot_handlers/lib/load_worldline_library.sh` deleted (proprietary
Worldline payment lib fetch from download.odoo.com, not needed)
- `wizard/add_iot_box.py._connect_iot_box_with_pairing_code` —
upstream called odoo.com's iot-proxy to resolve pairing codes;
replaced with a no-op. Pi-side iot_drivers proxy registers
directly with this Odoo server instead.
- Manifest rebranded with an explicit changelog preamble.
3. **fusion_plating_iot/** — new plating-specific wrapper:
- `fp.tank.sensor` — maps an iot.device (or a direct-HTTP-ingest
sensor) to a fusion.plating.tank + fusion.plating.bath.parameter.
Supports DS18B20, PT100/1000, pH, conductivity, level. Per-sensor
alert_min/max overrides.
- `fp.tank.reading` — append-only time-series. On create, evaluates
against sensor's alert range. On in-spec → out-of-spec TRANSITION,
auto-raises a fusion.plating.quality.hold (once per excursion,
no spam during sustained out-of-spec).
- `POST /fp/iot/ingest` — shared-secret HTTP endpoint for sensors
bypassing the Pi proxy. Token via X-FP-IOT-Token header OR body.
Accepts single-reading or batch payloads.
- Menu under Plating → Operations → Sensors & Readings.
- Tank form inherits get a Sensors tab inline.
Deployed to entech. Verified end-to-end:
- Install: iot_base + iot + fusion_plating_iot all 'installed'
- Smoke test: in-spec → out-of-spec → hold raised (HOLD-0010);
continued excursion → NO duplicate hold; back-in-spec → NEW
excursion → NEW hold (HOLD-0011) ✓
- HTTP endpoint: correct token → 200 accepted; wrong token → 401;
unknown device_serial → 404; batch payload → 200 accepted=N ✓
Phase B (when Raspberry Pi hardware arrives): DS18B20 iot_handler
driver for the Pi-side iot_drivers proxy + systemd service on
vanilla Raspberry Pi OS + first live reading from physical probe.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 11 of Phase 1 Bank Reconciliation. Adds the brain that ranks
candidate journal-item matches for a bank statement line.
Pass 1 — SQL filter (done by caller's _fetch_candidates).
Pass 2 — Statistical scoring: weighted blend of amount-delta,
partner pattern fit, and precedent similarity.
Pass 3 — Optional AI re-rank when an LLM provider is configured;
gracefully no-ops when provider missing, prompt module not
yet present (Task 20), or the JSON response is malformed.
Pass 4 — Persistence (handled by engine.suggest_matches).
Returns top-K ScoredCandidate dataclasses with per-feature scores
exposed for transparency and future learning.
7 new tests added; full module suite green (51 tests, 0 failures).
Made-with: Cursor
Adds the foundation for AI confidence scoring:
- fusion.reconcile.pattern: per-(company, partner) aggregate profile
(volume, cadence, preferred matching strategy, memo signature,
write-off habits) — recomputed nightly from precedents.
- fusion.reconcile.precedent: per-historical-decision memory holding
full feature vector + outcome, used by precedent_lookup for KNN
scoring of new bank lines.
Includes ACL rows for fusion accounting user (read) and admin (CRUD)
groups. Manifest bumped to 19.0.1.0.1.
Note: switched the pattern uniqueness rule from the deprecated
_sql_constraints attribute to models.Constraint (Odoo 19 native API)
so the unique(company_id, partner_id) is actually enforced at the
PG level — _sql_constraints is silently ignored in 19.
Made-with: Cursor
Phase 1 prerequisite for local LLM support. Adapters now declare
capability flags (supports_tool_calling, max_context_tokens, etc.) so
the engine can reason about what backend is available.
OpenAI adapter accepts fusion_accounting.openai_base_url config -- point
it at LM Studio (http://host.docker.internal:1234/v1) or Ollama
(http://host.docker.internal:11434/v1) and the existing OpenAI adapter
works unchanged.
Implementation note: existing Odoo AbstractModel adapters
(fusion.accounting.adapter.openai/claude) are preserved untouched to
avoid breaking the chat panel; the new plain-Python OpenAIAdapter and
ClaudeAdapter classes (LLMProvider subclasses) are added alongside them.
Made-with: Cursor
group_fusion_show_when_enterprise_absent has membership = all internal
users iff no Enterprise accounting module is installed. Membership is
recomputed on module install/uninstall via overrides on ir.module.module.
Used by Phase 1 fusion_bank_rec menus to auto-hide when Enterprise is
active and auto-appear after Enterprise uninstall.
Made-with: Cursor
Declare account.bank.statement.line.cron_last_check on
fusion_accounting_core so the column survives Enterprise
account_accountant uninstall. Mirrors the existing pattern used
for account.move and account.reconcile.model shared fields.
- Add models/account_bank_statement_line.py declaring cron_last_check
as fields.Datetime(copy=False)
- Wire model into models/__init__.py
- Add post_install regression test verifying field presence and type
- Bump manifest 19.0.1.0.0 -> 19.0.1.0.1
Made-with: Cursor
Scaffold the fusion_accounting_bank_rec sub-module with directory
tree, manifest, empty package __init__ files, empty ACL CSV, icon,
and Enterprise reference snapshots. No models, controllers, or
business logic yet — installs cleanly on V19 westin-v19 dev DB.
Made-with: Cursor
**7a. NCR close gate** (fusion.plating.ncr.action_close)
Block close unless these are filled in:
• Description (what happened)
• Containment Actions (immediate response)
• Root Cause (why it happened)
• Disposition (use-as-is / rework / scrap / RTV decision)
A closed NCR without these is useless for AS9100 audits — it's
the entire point of an NCR to document what went wrong, why, and
how we responded. Empty-HTML strings like "<p><br></p>" are
detected as empty too.
**7b. CAPA close gate** (fusion.plating.capa.action_close)
Block close unless:
• Root Cause Analysis filled in
• Action Plan filled in
• Verification (date + verifier) recorded
• Effectiveness Notes filled when CAPA was marked Not Effective
AS9100 §10.2 / Nadcap require evidence of root-cause analysis,
the corrective/preventive action plan, AND that effectiveness
was verified before the loop is closed.
**8. Invoice ref defensive default** (account.move.create)
Auto-fills `ref` from the source SO's client_order_ref or
x_fc_po_number when the invoice is created with invoice_origin set
but no ref. Already populated on the SO confirm path; this catches
manually-created invoices that would otherwise miss it. Customer
AP teams reject invoices that don't quote their PO# back.
**9. Discharge sample close gate** (fusion.plating.discharge.sample.action_close)
Block close unless:
• Lab Report # set
• Results Received Date set
• At least one parameter reading on file
• Lab certificate/report attached
Without lab evidence the record fails any environmental compliance
audit — the whole point is to document the test was performed and
what the lab said.
**Simulator** (scripts/fp_e2e_workforce.py)
Adds 4 new negative tests (Test 8-11), all wrapped in savepoints:
✓ Test 8 : NCR close without RC/containment/disposition → blocked
✓ Test 9 : CAPA close without analysis/plan/verification → blocked
✓ Test 10: Discharge sample close without lab evidence → blocked
✓ Test 11: Invoice ref auto-fills from SO.client_order_ref → asserted
**Final E2E**: 52 PASS / 2 WARN / 0 FAIL out of 54 checks.
Both remaining WARNs are expected (bake-window auto-create,
first-piece gate — coating-driven, this coating doesn't trigger them).
11 negative tests in total now, every gate fires when triggered.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Following the workforce-E2E + required-fields audit, ship the first 6
high-priority gates so critical workflow + compliance fields can no
longer be left empty by accident.
**1. Invoice payment terms (account.move)**
- create() now auto-inherits `invoice_payment_term_id` from
partner.property_payment_term_id when missing
- action_post() raises UserError if still missing — accountant must
pick one before posting (prevents silent "immediate" due-date)
**2. MO facility (mrp.production)**
- action_confirm() auto-derives `x_fc_facility_id` if unset, in order:
SO override → res.company.x_fc_default_facility_id → first active
facility — then HARD GATES: raises UserError if still empty.
Without facility every downstream record (WO, batch, bath log,
cert) is missing the "where" half of the audit trail.
**3. WO facility (mrp.workorder)**
- Switched `x_fc_facility_id` from related (workcenter only) to a
proper compute that falls back to production_id.x_fc_facility_id.
Stub workcenters auto-created from process node names usually have
no facility — the MO always does (from #2 above).
**4. Thickness reading calibration_std (fp.thickness.reading)**
- `calibration_std_ref` is now `required=True` with sensible default
("NiP/Al STD SET SN 100174568"). Nadcap mandates which calibration
standard the gauge was checked against — without it the cert
data has no chain back to a metrology record.
**5. Delivery POD gate (fusion.plating.delivery)**
- action_mark_delivered() raises UserError if no `pod_id`. Driver
must capture POD on the iPad (recipient signature + photos +
notes) BEFORE marking delivered. Without POD there's no signed
receipt to back the invoice or defend a delivery dispute.
**6. Certificate spec_reference gate (fp.certificate)**
- action_issue() raises UserError if no `spec_reference`. The cert
ATTESTS to a spec — leaving it blank produces a piece of paper
that AS9100 / Nadcap auditors will (rightfully) reject.
**Simulator updated**: scripts/fp_e2e_workforce.py
- Sets net-30 on the test customer + ensures a default facility
- New PHASE 4c: 5 negative tests (one per new gate), each wrapped
in a SAVEPOINT so SQL constraint violations don't abort the txn
- Driver now creates POD on iPad BEFORE marking delivered
**Final E2E**: 48 PASS / 2 WARN / 0 FAIL out of 50 checks.
The 2 remaining WARNs (bake-window auto-create, first-piece gate)
are expected behaviour — both are coating-driven and the test
coating intentionally doesn't trigger them.
All 7 negative tests now pass:
✓ Test 1: WO start without operator → blocked
✓ Test 2: WO start on wet WO without bath/tank → blocked
✓ Test 3: MO confirm without facility → blocked
✓ Test 4: Cert issue without spec_reference → blocked
✓ Test 5: Delivery delivered without POD → blocked
✓ Test 6: Invoice post without payment terms → blocked
✓ Test 7: Thickness reading without cal std → blocked (DB NOT NULL)
Audit script (scripts/fp_required_fields_audit.py) committed too —
it's the diagnostic that surfaced these gaps and can be re-run to
catch new ones.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User audit caught: in the workforce E2E run we had no idea which bath /
which tank ran the job. For aerospace traceability that's a deal-
breaker. Add a validation gate on mrp.workorder.button_start so
operators can't tap START without the data the shop floor MUST capture.
**Three new pieces on mrp.workorder:**
1. `_fp_is_wet_process()` — best-effort "does this WO involve a
chemistry bath?" check. Three signals in priority order:
a. A bath is already linked → definitely wet
b. The workcenter's FP work-centre supports a wet process family
(plating, pre/post-treatment, strip, passivation)
c. WO name contains a wet-process keyword (plat, nickel, chrome,
anodiz, zinc, etch, clean, rinse, strip, passivat, electroless…)
The keyword fallback is needed because most existing recipes have
no process_type_id set on their operation nodes.
2. `_fp_check_required_fields_before_start()` — runs before the
existing certification check. Rules:
• Every WO needs an assigned operator (x_fc_assigned_user_id).
Without it, productivity records can't be attributed and the
proficiency tracker has no employee to credit.
• Wet WOs additionally need x_fc_bath_id + x_fc_tank_id. So we
know exactly which chemistry bath ran the job and which physical
tank it sat in.
Raises a clear UserError listing the missing fields if any.
3. `x_fc_requires_bath` (compute, non-stored) — surfaces the wet check
to the form view so bath + tank fields render with `required=`.
**View changes:**
- `x_fc_assigned_user_id` is now `required="1"` on the form
- `x_fc_bath_id` + `x_fc_tank_id` use `required="x_fc_requires_bath"`
→ red asterisk only when the WO is actually wet
**Simulator updates** (scripts/fp_e2e_workforce.py):
- Hannah now explicitly assigns bath + tank to wet WOs during planning,
AND pre-issues operator certifications for the bath's process type
(real shop manager workflow).
- Two negative tests added that PROVE the gates fire:
• Test 1: strip the operator → button_start raises "missing Assigned Operator"
• Test 2: strip bath/tank on a wet WO → button_start raises "missing Bath/Tank"
**Final E2E:** 42 PASS / 2 WARN / 0 FAIL out of 44 checks.
Both remaining WARNs (bake-window auto-create, first-piece gate) are
expected behaviour — those are coating-driven and the test coating
intentionally doesn't trigger them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>