Adds Aged Receivable, Aged Payable, and Partner Ledger as fusion.report
records using the new compute_partner_grouped engine method.
REPORT_TYPES is extended with aged_receivable / aged_payable /
partner_ledger so each report has a unique report_type. The HTTP
controller dispatches these to engine.compute_partner_grouped with
the appropriate account_type via PARTNER_GROUPED_ACCOUNT_TYPE.
Output includes per-partner aging buckets: current, 1-30, 31-60,
61-90, 90+ days.
Westin total: 4 + 4 + 3 = 11 of Enterprise's 22 standard reports.
Made-with: Cursor
Adds engine.compute_partner_grouped(period, account_type=...) that
returns per-partner aggregations with aging buckets (current/1-30/
31-60/61-90/90+). SQL-direct for performance — single GROUP BY query
with conditional sum per bucket.
Foundation for the 3 partner-grouped reports landing in commit 3:
Aged Receivable, Aged Payable, Partner Ledger.
Made-with: Cursor
Adds Cash Flow Statement, Executive Summary, Tax Summary, and Annual
Statements as fusion.report records with line_specs. All work with the
existing engine's bucket-sum pattern — no engine changes needed.
Westin total: 4 + 4 = 8 of Enterprise's 22 standard reports now in
fusion_accounting_reports. Partner-grouped reports (Aged AR/AP,
Partner Ledger) need an engine extension — in commit 2.
Made-with: Cursor
Phase D8 compute was returning x_fc_margin_percent already-multiplied
by 100, but the 'percentage' widget in the SO form multiplies again
for display. Result was 10000% instead of 100%.
Store as 0.0-1.0 fraction; widget handles the multiplier. Caught
during UAT on S00066.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrates Enterprise account_followup per-partner state to Fusion fields:
- res.partner.followup_status -> fusion_followup_status (action_due/no_action)
- res.partner.payment_next_action_date -> fusion_followup_paused_until
(when future-dated; sets status to 'paused')
- res.partner.followup_line_id -> fusion_followup_last_level_id
(resolved by name match against migrated levels)
Wired into fusion.migration.wizard.action_run_migration after the
existing _followup_bootstrap_step. Idempotent: skips partners whose
Fusion state is already non-default. Defensive against missing
Enterprise fields (each field probed individually before use).
Closes the per-partner state migration gap that was blocking
Enterprise account_followup uninstall.
Made-with: Cursor
Two critical, one important, four polish fixes found by the
pr-review-toolkit code-reviewer.
C1 (CRITICAL) Start-at-node filter dropped later siblings
fusion_plating_bridge_mrp/models/mrp_production.py:448
The allowed_ids set was {descendants} ∪ {ancestors}, which wrongly
excluded nodes that should run AFTER the start node — including
later siblings of the start node and all operations in subsequent
sub-processes. Rewrote the upward walk to ALSO include each
ancestor's later-sequence siblings and their descendants. Smoke on
ENP-ALUM-BASIC: full=9 WOs, partial from mid-tree 'De-Masking'=5
WOs (previously was 1).
C2 (CRITICAL) Duplicate MO on re-confirm of pre-PR SOs
fusion_plating_bridge_mrp/models/sale_order.py:96
Legacy untagged MOs (created before this PR had line-linkage m2m)
were not recognized by the untagged idempotency check, so
re-confirming an already-processed SO would create one additional
MO per untagged plating line. Fix: pre-scan for a single legacy
untagged MO and adopt it by linking ALL untagged plating lines
onto it. Those lines are then treated as covered and no per-line
MOs are created on top. Smoke: S00066 before=1 MO, after
re-run=1 MO.
I5 (IMPORTANT) push_to_defaults wrote to pre-bump revision
fusion_plating_configurator/wizard/fp_direct_order_wizard.py:236
When create_new_revision=True, _get_or_bump_revision() returned a
new part record that got written to the SO line, but the
post-confirm push_to_defaults loop re-read line.part_catalog_id
(still the OLD rev) and wrote defaults there, defeating the whole
point of "save as default". Fix: cache resolved parts in a dict
keyed by wizard-line ID during the build loop, and use that cache
in the push_to_defaults pass.
I3/I4/I6 (PERF) Computes lacked @api.depends and did per-record
search_count / search queries
fusion_plating_configurator/models/sale_order.py
_compute_nav_counts, _compute_workorder_count, _compute_wo_completion
now:
- declare @api.depends
- batch via read_group across the whole self recordset
- rebuild {origin: counts} dicts and assign per record
M7 (MEDIUM) No savepoint around per-group MO creation
fusion_plating_bridge_mrp/models/sale_order.py:_fp_auto_create_mo
A mid-loop exception left group 1's MO persisted and aborted
groups 2..N. Wrapped each group's create in SAVEPOINT/RELEASE/
ROLLBACK TO SAVEPOINT so one bad group no longer corrupts state.
M8 (MEDIUM) Email 'opened' status false-positived on internal CC
fusion_plating_configurator/models/sale_order.py:_compute_email_status
Switched from 'any notification is_read' to 'customer partner has
a read email notification on this SO'.
M9 (LOW) start_at_node_id domain silently empty when coating unset
fusion_plating_configurator/wizard/fp_direct_order_line.py:94
Changed `('parent_id', 'child_of', ...)` to
`('id', 'child_of', ..., or 0)` and clarified the help text.
Regression smoke passed all checks on odoo-entech.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Line 77 was `_ = super().action_run_migration()`, using `_` as a
throwaway variable name. That rebinds the module-level `_` (Odoo's
translation function imported at the top) to whatever super() returns
\u2014 in our case the parent's notification dict.
Lines 84/85 then call `_('Bank-Rec Migration Complete')` which is
now `some_dict('Bank-Rec Migration Complete')` \u2192
TypeError: 'dict' object is not callable.
User hit this when running the migration wizard from the menu.
Fix: drop the assignment; we don't actually use super()'s return value.
Made-with: Cursor
V19 removed the 'rpc' service from the registry. All 4 fusion services
(bank_reconciliation, reports, assets, followup) declared dependencies:
['rpc', ...] and accessed services.rpc in their constructor. At runtime
this caused:
Error: Some services could not be started: fusion_bank_reconciliation,
fusion_reports, fusion_assets, fusion_followup. Missing dependencies: rpc
\u2014 which prevented the entire OWL backend from booting (blank screen).
Fix per V19 docs:
- Add 'import { rpc } from "@web/core/network/rpc";'
- Set 'this.rpc = rpc;' in constructor (instead of services.rpc)
- Remove 'rpc' from dependencies list
This is the workspace CLAUDE.md guidance Phase 4's subagent flagged
but didn't act on for backward consistency. V19 actually removed the
service entirely, so the consistency choice was wrong \u2014 fixing now.
All call sites still use this.rpc(...) so no per-method changes needed.
Bundle rebuilt clean; backend boots correctly.
Made-with: Cursor
Phases 1-3's SCSS files used '@import "variables";' to pull in tokens
from _variables.scss. V19's odoo.addons.base.models.assetsbundle
forbids cross-file SCSS imports for security ('Local import forbidden')
and the asset bundle warning was firing on every web request.
Phase 4 caught + fixed this for fusion_accounting_followup; Phases 1-3
were never updated. Today's deployment surfaced the CSS error reported
by the user.
Resolution:
- Removed @import lines from 7 SCSS files across bank_rec, reports, assets
- Variables come from _variables.scss via manifest concatenation order
(bundle order is _variables.scss first, then dependent files)
- Replaced documentation comments to NOT contain the literal string
'@import "variables"' \u2014 Odoo's check is regex-based and was
matching even SCSS comments
Verified clean: bundle rebuilds with zero 'Local import forbidden'
warnings; all 534 fusion-module tests still pass.
Made-with: Cursor
Phase 3 (fusion_accounting_assets) changed list_assets() to return
{count, total, assets} dict instead of a flat list — consistent with
bank_rec.list_unreconciled, reports.run_report, followup.list_overdue.
The pre-existing test in fusion_accounting_ai still asserted isinstance(rows, list)
and was failing on every run since Phase 3 merge. Updated to assert dict shape.
Made-with: Cursor
Ships the remaining items from the Sales UX Uplift plan:
D2 BOM Items kanban
New view_sale_order_line_bom_kanban grouped by x_fc_part_catalog_id.
Smart button 'BOM Items' on SO form opens it.
D5 Archive line
x_fc_archived Boolean on sale.order.line plus action_archive_line /
action_unarchive_line. Acknowledgement report filters out archived
lines.
D6 Add Quoted Lines sub-wizard
New fp.add.from.quote.wizard parallel to fp.add.from.so.wizard. Pick
quotes for this customer and clone them into direct-order lines
carrying part, coating, qty, unit price (from calculated or
override), and notes. Button '+ Add From Quotes' on wizard Lines tab.
D7 SO Acknowledgement PDF
New ir.actions.report + QWeb template in configurator/report/.
Header shows customer / contact / PO / Customer Job #, Bill-To,
Ship-To, planned start + customer deadline + ship-via. Line table
skips archived lines. Includes external notes, blanket-order
callout, and customer-signature + vendor-signature blocks.
Binding added to sale.order so it shows up under Print menu.
D9 Quick-nav chip bar
New smart buttons on SO form: Invoices / Pickings / NCRs / Files
with counts and icons. Each opens a filtered list. NCR button
appears only when fusion_plating_quality is installed.
D10 SO/WO perspective toggle
view_sale_order_line_wo_kanban grouped by x_fc_wo_group_tag. Smart
button 'By WO' on SO form.
D11 Assemblies minimal model
fp.sale.assembly + fp.sale.assembly.line with name, ship_to, count,
procured_count, completed_at. UX (forms / kanbans / integration
into receiving) deferred — model only for now.
D14 Uploaded Files
Files smart button on SO form opens ir.attachment kanban filtered
to this SO. Count appears in the chip bar.
F4 Signed tracking
x_fc_signed_at / x_fc_signed_by / x_fc_is_signed on sale.order +
action_mark_signed helper. Signed column on quotes list view.
F10 New Quote
Kept on existing action_fp_quotations (already surfaces the
default New button).
E5/F9 Action icons per row
Deferred — requires a custom widget; the native PDF action via the
Print menu covers 80% of the use case.
Bumped to 19.0.8.0.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two features from Phases B/C that were previously only data now do work:
1. WO GROUPING (x_fc_wo_group_tag)
_fp_auto_create_mo rewritten to iterate order_lines and group by
x_fc_wo_group_tag. Lines sharing a tag collapse into ONE MO with
product = first line's part.product_id, qty = Σ line qty,
recipe = first line's coating_config.recipe_id. Untagged lines
each get their own MO. Legacy path preserved for service-line SOs
with no plating data.
Idempotency is per (origin, tag): re-confirming an SO doesn't
create duplicate MOs for already-grouped lines.
New on mrp.production:
- x_fc_wo_group_tag (Char, tracking)
- x_fc_sale_order_line_ids (M2M back to sale.order.line)
- x_fc_start_at_node_id (Many2one fusion.plating.process.node)
2. START-AT-NODE (x_fc_start_at_node_id)
_generate_workorders_from_recipe pre-computes allowed_ids as the
set of {descendants of start_node} ∪ {ancestors of start_node}.
_is_node_included rejects any node outside that set. This skips
sibling branches earlier in the recipe while keeping the
container hierarchy so WO sequence numbers still make sense.
Smoke-tested S00070 (4 lines, 2 tagged groups + 1 untagged) -> 3 MOs:
WO#A qty=15 (2 lines batched), WO#B qty=50 (1 line), untagged qty=7
(1 line). Each got the ENP-ALUM-BASIC recipe.
Start-at-node smoke on the same recipe: full generation = 9 WOs,
partial with start_at='Ready for processing' = 1 WO.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- test_create_minimal/negative_delay used sequence=1, which now collides
with the seeded Friendly Reminder level. Use sequences 901/902.
- migration backfill: search by name (not raw seq) for idempotency,
allocate sequence as max(existing)+1 to avoid both seed clashes and
within-batch collisions when Enterprise has duplicate sequence values.
Made-with: Cursor
F1 follow-up: x_fc_follow_up_date + x_fc_follow_up_user_id fields on
sale.order, surfaced in the quotations list + a 'Needs Follow-Up'
preset filter.
F2 expires: native validity_date exposed as togglable column on the
quotes list + an 'Expired' preset filter.
F3 email status pills: x_fc_email_status computed (draft / sent /
opened / won). 'Opened' detects via mail.notification.is_read on any
email-type mail.message attached to this SO.
F5 part numbers summary: x_fc_part_numbers_summary ("PN1, PN2 (+3
more)") across order_line parts, togglable column.
F7 from-RFQ filter reuses existing x_fc_rfq_attachment_id.
Views:
- view_sale_order_list_fp_quotes (new list dedicated to quotes).
- view_sale_order_search_fp_quotes with filters Draft / Sent / Won /
From RFQ / Needs Follow-Up / Expired + group-bys.
- action_fp_quotations rewired to both of the above.
Bumped to 19.0.7.2.0. Closes all six phases originally planned.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
E1/E2/E3/E4: list view gets new togglable columns for
- x_fc_wo_completion (e.g. '3/5'): count of completed vs total WOs
- x_fc_invoiced_amount (Monetary): sum of posted customer invoices
minus credit notes
- x_fc_margin_amount + x_fc_margin_percent: reuses Phase D8 computes
- x_fc_is_blanket_order toggle
New sale.order.search view (sale.order.search.fp) with preset
filters: My Orders / Open / Confirmed / Done / Blanket / Has Rush /
Overdue, plus group-bys for Customer / Status / Customer Deadline.
Bumped to 19.0.7.1.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
D3 first half: x_fc_workorder_count computes live count of active MRP
work orders linked to this SO (via mo.origin = so.name). Adds a
'Active WOs' smart button next to the existing PO / RFQ buttons on
the sale.order form. Clicking opens a filtered mrp.workorder list
grouped by MO.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase D first landing covers the quick-win Steelhead-parity fields on
the SO form / list:
- D1: x_fc_deadline_countdown ("in 2d 3h", "overdue 1d 4h") computed
from commitment_date. Surfaced in SO form scheduling group and as
togglable column on the SO list.
- D4: x_fc_internal_note + x_fc_external_note split (html). Existing
'note' field is left untouched for back-compat. External note is
intended for the SO acknowledgement + portal; internal note is
shop-floor only.
- D8: x_fc_margin_amount + x_fc_margin_percent, currently computed
against fp.coating.config.unit_cost if defined (else 0 -> 100%
margin). When cost rollup lands on fp.coating.config, margin will
reflect reality automatically.
- D12: x_fc_contact_phone related to partner.phone (readonly) on SO
header.
- D13: x_fc_ship_via Char on SO header (carrier name).
Smoke: S00066 shows 'in 9d 22h' countdown + \$3025 margin; S00069
shows 'in 24d 22h' + \$750. Contact phone pulls from partner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase C complete on odoo-entech. Smoke-tested S00069:
- C1 x_fc_start_at_node_id = Ready for De-Masking (resume-rework)
- C2 x_fc_part_wo_description = internal rework note
- C5 x_fc_is_one_off = False
- C3 x_fc_quote_id slot wired (no quote picked in this smoke)
- C4 push-to-defaults wrote EN High-Phos back onto part catalog
Phase D (SO detail view), Phase E (SO list view), and Phase F
(Quotes list) are independent tracks — outlined in the plan doc.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds quote_id (Many2one fp.quote.configurator) on the wizard line
with a domain scoped to the wizard's customer + quote states (sent /
accepted / won). Onchange auto-fills part, coating, and unit price
(final = estimator_override_price or calculated_price, per-part).
Mirrors x_fc_quote_id on sale.order.line for the audit trail. Surfaced
as a togglable column on the SO line tree and under "Qty & Price" on
the wizard line drill-in form.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- fusion.followup.cron AbstractModel with two handlers
- cron_fusion_followup_daily_scan: walks every overdue partner and
delegates to engine.send_followup_email
- cron_fusion_followup_risk_refresh: weekly refresh of
fusion_followup_risk_score / risk_band on res.partner
- V19 ir.cron records (no numbercall field)
- 2 smoke tests added (80 total)
Made-with: Cursor
Adds x_fc_default_coating_config_id and x_fc_default_treatment_ids
fields on fp.part.catalog. Wizard line gets a push_to_defaults
toggle. After action_create_order confirms the SO, any line with
push_to_defaults=True writes its coating + treatments back onto the
part catalog entry as the new defaults.
Reverse direction too: onchange on part_catalog_id in the wizard
line seeds coating + treatments from the part's defaults (if set and
the line doesn't already have them).
Part catalog form gets a new "Defaults" tab showing the stored
defaults. Smoke-tested: pushing default on order 1 populates the
catalog entry; new wizard line for that part auto-seeds the coating.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Switch FUSION_MODEL to fusion.followup.engine so adapter mode
selection matches the new module
- Add list_overdue() with fusion/enterprise/community variants
- Re-route send_followup_via_fusion to engine.send_followup_email
- 4 new TransactionCase tests (73 total)
Existing aging / overdue_invoices adapter methods continue to fall
back to the community implementation.
Made-with: Cursor
C1: start_at_node_id per wizard line, mirrors to x_fc_start_at_node_id
on sale.order.line. Domain filters to nodes descending from the
coating_config's recipe so the estimator only picks valid resume
points. bridge_mrp will use this in a follow-up to skip ancestor
steps in the generated work order.
C2: part_wo_description (separate from customer-facing line_description)
lets the planner add internal-only notes that appear on the travelling
sheet only. Mirrors to x_fc_part_wo_description on sale.order.line.
C5: is_one_off flag for prototype / non-catalog parts. Mirrors to
x_fc_is_one_off. Actual skip-catalog behaviour will be wired in a
later pass.
All three fields appear in the wizard line drill-in form (under a new
"Work Order (internal)" group) and as togglable columns on the
sale.order.line tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end flows over a real posted receivable line: aging discovery,
level resolution, send-with-cache reuse, pause+force override, and
audit history growth. Adds ignore_pause kwarg to compute_followup_level
so force=True in send_followup_email reaches level resolution.
Made-with: Cursor
The orchestrator AbstractModel for follow-up lifecycle.
get_overdue_for_partner, compute_followup_level, send_followup_email,
escalate_to_next_level, pause_followup, reset_followup, snapshot_followup_history.
All controllers, AI tools, wizards, cron must route through these
methods; no direct ORM writes to fusion.followup.run from anywhere else.
Made-with: Cursor
Phase B complete on odoo-entech:
- B1/B2: Blanket + Block Partial flags on wizard header + sale.order
- B3: x_fc_wo_group_tag per SO line (bridge_mrp will use this to
batch MOs in a follow-up)
- B4: 'Add From Prior SO' sub-wizard for repeat orders
- B5: Per-line is_missing_info compute + amber row decoration
- B6: Rush already on line (added in Phase A)
Smoke-tested: wizard accepts 4 lines (1 with missing price, 3 WO-tagged
across 2 groups), banner shows correctly, missing row highlighted in
amber, after fix SO creates cleanly with all flags + tags persisted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task B4. New fp.add.from.so.wizard transient model: given the current
direct-order wizard + customer, lists the customer's prior confirmed
sale orders, lets the estimator tick source lines, and clones them
into fp.direct.order.line rows (part, coating, treatments, qty,
price, deadline, rush, WO group, description).
Button "+ Add From Prior SO" lives on the Lines tab of the main
wizard, visible once the customer is picked. Sub-wizard rejects
source lines that predate the new plating fields (no x_fc_part_catalog_id).
Smoke-tested on odoo-entech: copying all 3 lines of S00066 onto a
fresh wizard reproduces part/coating/qty/price correctly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>