Adds bank_rec_bootstrap step that backfills fusion.reconcile.precedent
from existing account.partial.reconcile rows during migration. This
gives the AI memory from past Enterprise reconciles. Also triggers
pattern refresh + MV refresh for immediate UI readiness.
- New service services/precedent_backfill.py walks
account.partial.reconcile rows, identifies the bank-statement-line
side, and creates a precedent per qualifying partial. Idempotent via
(statement_line, account, amount, source='backfill') signature.
- New model models/fusion_migration_wizard.py inherits
fusion.migration.wizard, exposes _bank_rec_bootstrap_step() (callable
from tests/audit), and overrides action_run_migration() to call
super() + the bootstrap.
- Adds 'backfill' to fusion.reconcile.precedent.source selection.
- Adds fusion_accounting_migration to depends.
Made-with: Cursor
TransientModel + view + binding action so users can select bank lines
from any list view and bulk-apply either engine.reconcile_batch or
a chosen reconcile model.
Made-with: Cursor
TransientModel that filters unreconciled bank lines by journal +
date range + strategy and runs engine.reconcile_batch. Shows
reconciled_count / skipped_count / error_summary in result view.
Made-with: Cursor
attachment_strip renders inline mimetype-aware chips linking to /web/content
downloads. partner_history_panel calls bank_reconciliation.getPartnerHistory
to surface the learned reconcile pattern (preferred strategy, typical cadence)
plus the most recent reconciles per partner — context Enterprise's bank-rec
widget cannot show because it has no behavioural-learning layer.
Made-with: Cursor
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
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
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
- 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
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 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
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
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>
51 tasks across 17 groups covering the full Phase 1 build:
Group 1 (5 tasks): Foundation — branch, sub-module skeleton, shared
fields on _core, LLMProvider contract for local LLM readiness
Group 2 (8 tasks): Reconcile engine — TDD-layered build of
matching_strategies, exchange_diff, memo_tokenizer, precedent_lookup,
pattern_extractor, confidence_scoring 4-pass pipeline, the AbstractModel
engine with 6-method API, and Hypothesis property-based tests
Group 3 (4 tasks): Models — fusion.reconcile.pattern,
fusion.reconcile.precedent, fusion.reconcile.suggestion, widget transient,
and inherits on Community account.bank.statement.line + account.reconcile.model
Group 4-5 (6 tasks): Integration tests with SQL fixtures from real Westin
reconciles + AI prompts + adapter fill-ins + AI tools refactor
Group 6-7 (3 tasks): Materialized view, cron schedules, and 10-endpoint
JSON-RPC controller with auth guards
Group 8-10 (10 tasks): Frontend — SCSS tokens, service, kanban controllers,
all 18 Enterprise-mirror OWL components, and 5 fusion-only components
(ai_suggestion folder, batch_action_bar, attachment_strip,
partner_history_panel, reconcile_model_picker)
Group 11-13 (5 tasks): Wizards (auto-reconcile + bulk), migration wizard
inheritance with bootstrap of 16,500 historical reconciliations + audit
report PDF + round-trip test, coexistence menu/group + tests
Group 14-16 (3 tasks): 5 OWL tour tests, performance benchmarks against
P95 targets, local LLM compatibility test against LM Studio
Group 17 (4 tasks): Closeout — meta-module manifest update, sub-module
docs, end-to-end smoke test, completion tag
TDD discipline throughout: every code task is red test → impl → green
→ commit. Property-based tests for amount invariants. Migration round-
trip test asserts byte-identical reconciliation state pre/post Enterprise
uninstall. All testing on local OrbStack VM only (environment-safety
rule applies).
Made-with: Cursor
Built a comprehensive simulator (scripts/fp_e2e_workforce.py) that
role-plays 10 employees driving an order quote → invoice using real
operator timers (button_start / button_finish with elapsed time.sleep).
Initial run: 31 PASS / 2 WARN / 0 FAIL exposed two gaps that would
hurt a real shop:
**Gap 1 — Thickness readings never reached the CoC**
The Fischerscope readings inspectors take during post-plate inspection
had no path to the CoC. The cert came out empty, useless for AS9100
or aerospace audits.
Fixes:
- New tablet endpoint `/fp/shopfloor/log_thickness_reading` so the
inspector can record one reading at a time during the inspection WO
(auto-numbers, defaults the operator, supports microscope image).
- mrp_production._fp_mark_done_post_actions now bulk-links any
orphan thickness readings (those with production_id=mo.id but no
certificate_id) to the freshly-created CoC. So inspectors can log
during inspection AND the cert PDF picks them up automatically.
**Gap 2 — Operator queue leaked other people's work + simulator missed it**
fusion.plating.operator.queue.build_for_user pulled EVERY ready /
in-progress WO regardless of assignment. Tom would see John's masking
WO in his "Up Next" list — bad for aerospace traceability where you
want strict per-operator accountability.
Fix: build_for_user now filters MRP WOs by
`(x_fc_assigned_user_id == user_id OR x_fc_assigned_user_id == False)`.
Operators see their own assigned tasks first, plus any unassigned
tasks anyone can grab. Other operators' assigned WOs no longer leak
through.
Also caught: simulator was using wrong field name on the queue model.
Fixed and added a "queue isolation" check that verifies no operator
sees another operator's assigned WOs.
After fixes: **39 PASS / 2 WARN / 0 FAIL** (out of 41 checks).
Remaining WARNs are both expected behaviour:
- bake-window auto-create: this coating doesn't require_bake_relief
(the recipe has an inline Oven step instead)
- first-piece gate: same — coating-driven, only fires when needed
Areas validated end-to-end:
- quote → SO with PO# carried into client_order_ref
- SO confirm → MO + portal job auto-created
- receiving qty prefill + accept
- 9 WOs generated from recipe + assigned to specific operators
- All 9 WOs ran with real elapsed timers + 17 productivity records
across 4 distinct operators
- MO done triggers CoC auto-issue with 5 thickness readings linked,
319 KB rich PDF, customer-slug filename
- Delivery auto-created with prefilled date + driver + CoC link
- Delivery delivered, 2 chain-of-custody entries
- Invoice posted (NOT auto-paid)
- All 5 customer notifications fired (so_confirmed +
parts_received + mo_complete + shipped + invoice_posted) with
correct attachments
- Portal job → complete, SO workflow_stage → invoicing
- Chemistry log persisted, operator proficiency tracked
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drafts the design for fusion_accounting_bank_rec — a native bank
reconciliation widget that replaces Odoo Enterprise account_accountant
in V19 OWL architecture, with a clean-room reconcile engine reading and
writing Community account.partial.reconcile rows.
Key design decisions captured:
- CORE scope (~5.5-6 weeks): manual + auto reconcile, write-offs,
partial, multi-currency, chatter, model picker
- Strict mirror of all 18 Enterprise OWL units (zero functional loss)
plus 5 fusion-only additions for AI/history visibility
- Hybrid AI badge layout: inline strip with one-click Accept plus
expandable ranked-alternatives panel
- Behavioural learning via fusion.reconcile.pattern (per-partner) and
fusion.reconcile.precedent (per-decision memory) with bootstrap from
the 16,500 historical reconciliations
- Local LLM ready via OpenAI-compatible adapter base_url config and
per-feature provider routing — works against LM Studio, Ollama, vLLM
- Statistical-mode-without-API-key as a first-class path
- Coexistence with Enterprise: Enterprise wins by default, fusion
menu hides until uninstall, then auto-appears
- Migration wizard step bootstraps pattern memory and produces an
audit report PDF proving every reconciliation preserved
- TDD on engine algorithms with Hypothesis property-based tests for
amount invariants; migration round-trip integration test
Builds on Phase 0 (commit c450bb2, range pre-phase-0..phase-0-complete).
Made-with: Cursor
Users were seeing both Odoo's stock PDFs and FP's branded equivalents
in the Print dropdown side-by-side, and accidentally sending the wrong
(unbranded, missing PO# / job ref / plating fields) PDF to customers.
Add fp_hide_default_reports.xml that drops the Print-menu binding on:
| Model | Hidden | FP replacement |
|-----------------|-------------------------------------------------------------|---------------------------------|
| sale.order | sale.action_report_saleorder | action_report_fp_sale_* |
| sale.order | sale_pdf_quote_builder.action_report_saleorder_raw | action_report_fp_sale_* |
| account.move | account.account_invoices | action_report_fp_invoice_* |
| account.move | account.account_invoices_without_payment | action_report_fp_invoice_* |
| stock.picking | stock.action_report_delivery | action_report_fp_packing_slip_* |
| mrp.production | mrp.action_report_production_order | action_report_fp_job_traveller_*|
| account.payment | account.action_report_payment_receipt | action_report_fp_receipt_* |
Mechanism: set binding_model_id=False + binding_type=action — removes
from the Print dropdown but leaves the report record + template intact.
Fully reversible from Settings → Technical → Reports if anyone needs
the stock PDF back.
Intentionally NOT touched:
- sale.action_report_pro_forma_invoice (no FP pro-forma yet)
- account.action_account_original_vendor_bill (vendor bills, internal)
- stock.action_report_picking / picking_packages / return_label_report
(internal warehouse ops, not customer-facing)
- mrp.action_report_finished_product / mrp.label_manufacture_template
(production labels — ZPL, not customer-facing)
- sale_timesheet.* (timesheet integration)
Added sale_pdf_quote_builder to depends so the data file always finds
that record when applied (it ships in entech's repackaged enterprise
bundle and was already installed there).
Verified on entech: re-running the print-menu audit shows zero stock
Odoo customer-facing PDFs left where FP has an equivalent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>