246 KiB
fusion_claims — Claude Code Instructions
Read together with the repo-root
../CLAUDE.md(Odoo 19 rules, naming, dark-mode SCSS pattern, PDF preview helper, Supabase KB credentials). This file documents only what is specific tofusion_claims.
1. What this module is
- Name: Fusion Claims (declared as
application: True— it owns its own top-level menu "ADP Claims"). - Category: Sales.
- License: OPL-1 (Nexa Systems Inc.).
- Version:
19.0.8.0.7(bump on every CSS/asset change — see asset cache-busting in repo CLAUDE.md). - Purpose: end-to-end management of Ontario funder claims for assistive devices. Single sale-order record drives the entire lifecycle across eight distinct funder workflows, each with its own state machine, wizards, emails, and reports. The largest module in the repo by far.
- Customers: Westin Healthcare (primary) and NEXA Systems internal use.
2. Dependencies
Odoo addons (__manifest__.py:75-88)
base, sale, sale_management, sale_margin, purchase,
account, sales_team, stock, calendar, ai,
fusion_ringcentral, fusion_tasks
ai(Odoo 19 native): used for theai.agentintegration. Theai_agent_fusion_claimsrecord + 3 server-action tools (_fc_tool_search_clients,_fc_tool_client_details,_fc_tool_claims_stats) live indata/ai_agent_data.xmlandmodels/ai_agent_ext.py. Channels of typeai_chatare spawned fromfusion.client.profile.action_open_ai_chat.fusion_tasks(sibling Nexa module): providesfusion.email.builder.mixin— the_email_build(title, summary, sections, note, email_type, button_url, ...)helper used by ~50 email methods inmodels/sale_order.py. Without this dependency, every email send breaks.fusion.technician.task— the base model thatmodels/technician_task.pyinherits from to addsale_order_id/purchase_order_idlinks, delivery/pickup hooks, and rental inspection fields.
fusion_ringcentral(sibling Nexa module): click-to-dial + softphone for any phone field. No code in this module calls into it directly — it's a runtime UX dependency.calendar:schedule_assessment_wizardcreatescalendar.eventrecords with an email alarm 1 day before.sale_margin: used by the landscape report (margin column).
⚠ Soft (undeclared) dependency: fusion_faxes
wizard/odsp_submit_to_odsp_wizard.py calls into fusion_faxes.send.fax.wizard (the fax composer) and reads partner.x_ff_fax_number — but fusion_faxes is NOT in __manifest__.py.depends. The fax actions are guarded by hasattr checks so the wizard still loads if fusion_faxes is missing, but the "Send Fax" / "Send Email + Fax" buttons will fail at click-time. If you're moving this module to a new database, install fusion_faxes alongside it.
⚠ Reverse-dependency: fusion_authorizer_portal always installed alongside
The dependency direction is fusion_authorizer_portal → fusion_claims (hard, declared in fusion_authorizer_portal's manifest), but fusion_claims uses APIs that only exist when fusion_authorizer_portal is installed:
sale.order._apply_pod_signature_to_approval_formimportsPDFTemplateFillerfromodoo.addons.fusion_authorizer_portal.utils.pdf_filler—ImportErrorif missing.fusion.page11.sign.requestrenders PDFs usingfusion.pdf.templaterecords — that model lives in fusion_authorizer_portal, not here.- The
/page11/sign/<token>URL that the Page 11 wizard generates is handled byfusion_authorizer_portal.controllers.portal_page11_sign— without it the public signing flow is dead. page11_sign_request._generate_signed_pdfreferencesfusion.assessmentrecords — that model also lives in fusion_authorizer_portal.
In practice both modules are always installed together. See §29 for the full integration map.
External Python (__manifest__.py:89-91)
pdf2image,PIL— required (manifest declares).pdfrw— optional, used bywizard/odsp_sa_mobility_wizard.pyto fill the SA Mobility government PDF form. Module logs a warning and disables that wizard if missing.requests— used implicitly by AI calls (models/client_chat.py) and SMS (_twilio_send_sms). Inherited from base Odoo deps.
Post-init hook (__init__.py:14-54)
_load_adp_device_codes runs on install and every upgrade. Two idempotent steps:
fusion.adp.device.code._load_packaged_device_codes()importsdata/device_codes/adp_mobility_manual.json(~hundreds of records) viaimport_from_json._link_products_to_device_codes()runs two SQLUPDATEstatements: one linksproduct_template.x_fc_adp_device_code_idfor products that already havex_fc_adp_device_codeset and matches a device code, and one togglesx_fc_is_adp_product = TRUEfor products with a code but no flag. Both are guarded byIS NULLchecks — preserve idempotence if you edit them.
3. Funder workflows (the architecture)
x_fc_sale_type on sale.order (models/sale_order.py:320-339) selects one of:
adp, adp_odsp, odsp, wsib, direct_private, insurance,
march_of_dimes, muscular_dystrophy, other, rental, hardship
Once any non-quotation status is set on a funder workflow, x_fc_sale_type_locked becomes True (models/sale_order.py:347-371) and the dropdown is read-only — override via setting fusion_claims.allow_sale_type_override.
Each funder has its own status field, wizards, kanban columns, emails, and (in some cases) submission helpers:
| Funder | Status field | Module is_* flag | show_* flag | Key wizards |
|---|---|---|---|---|
| ADP (Assistive Devices Program) | x_fc_adp_application_status (22 states) |
x_fc_is_adp_sale |
implicit | schedule_assessment, assessment_completed, application_received, ready_for_submission, submission_verification, device_approval, ready_for_delivery, ready_to_bill, case_close_verification, status_change_reason, send_page11 |
| MOD (March of Dimes HVMP) | x_fc_mod_status (14 states) |
x_fc_is_mod_sale |
x_fc_show_mod_fields |
mod_submission_path, send_to_mod (drawing/quotation/POD), mod_awaiting_funding, mod_funding_approved, mod_funding_denied, mod_pca_received, mod_resubmit, mod_submission_confirmed |
| ODSP Standard | x_fc_odsp_std_status |
x_fc_is_odsp_sale |
x_fc_show_odsp_fields |
odsp_submit_to_odsp, odsp_pre_approved, odsp_ready_delivery |
| ODSP SA Mobility | x_fc_sa_status |
(subset of ODSP) | odsp_sa_mobility (fills gov PDF via pdfrw) | |
| ODSP Ontario Works | x_fc_ow_status |
(subset of ODSP) | odsp_discretionary | |
| WSIB | x_fc_wsib_status |
x_fc_is_wsib_sale |
x_fc_show_wsib_fields |
(generic funder transitions) |
| Insurance | x_fc_insurance_status |
x_fc_is_insurance_sale |
x_fc_show_insurance_fields |
(generic funder transitions) |
| MDC (Muscular Dystrophy) | x_fc_mdc_status |
x_fc_is_mdc_sale |
x_fc_show_mdc_fields |
(generic funder transitions) |
| Hardship | x_fc_hardship_status |
x_fc_is_hardship_sale |
x_fc_show_hardship_fields |
(generic funder transitions) |
| Direct/Private, Other, Rental | — | — | — | invoiced directly, no funder lifecycle |
x_fc_odsp_division distinguishes the three ODSP sub-workflows; _get_odsp_status() returns whichever of x_fc_sa_status/x_fc_odsp_std_status/x_fc_ow_status is active.
4. The ADP workflow (the spine)
4.1 Status field
x_fc_adp_application_status (models/sale_order.py:2302-2333) — 22 states. Workflow sequence enforced by _STATUS_ORDER (models/sale_order.py:2361-2384) which drives the kanban column order (_read_group override at line 2393).
quotation → assessment_scheduled → assessment_completed → waiting_for_application →
application_received → ready_submission → submitted →
accepted (within 24h) / rejected → resubmitted →
needs_correction → (corrected then back to submitted/resubmitted) →
approved / approved_deduction → ready_delivery → ready_bill → billed → case_closed
Special branches:
on_hold (any time) ←→ resume_from_hold (back to previous status)
withdrawn → resubmit_from_withdrawn (back to ready_submission)
denied → resubmit_from_denied (back to ready_submission)
cancelled → reopen (only if not reported to ADP)
expired (12 months after approved with no delivery) → reopen / duplicate_for_reassessment
There is also a legacy x_fc_adp_status field (7-state, simpler) — keep it in mind but do NOT use it for new logic.
4.2 Status transitions are NEVER set via dropdown
Every controlled status (every transition that should fire an email, write to chatter, or update related records) lives on a button that opens a dedicated wizard. static/src/js/status_selection_filter.js registers a filtered_status_selection field that hides controlled statuses from the dropdown:
const CONTROLLED_STATUSES = [
'assessment_scheduled', 'assessment_completed', 'application_received',
'ready_submission', 'submitted', 'resubmitted', 'approved',
'approved_deduction', 'ready_bill', 'billed', 'case_closed',
'on_hold', 'withdrawn', 'denied', 'cancelled', 'needs_correction',
];
When you must bypass validation (legitimate framework calls like sync paths), pass with_context(skip_status_validation=True).
4.3 Two-stage verification system
| Stage | Wizard | What it captures | When |
|---|---|---|---|
| 1. Submission | submission_verification_wizard.py (397 lines) |
x_fc_submitted_device_types (JSON dict {device_type: True} of device types submitted), groups lines by fusion.adp.device.code.device_type |
Before submitted |
| 2. Approval | device_approval_wizard.py (724 lines) |
per-line x_fc_adp_approved, plus optional deductions and approval-letter attachments |
After ADP approves |
Stage 1 is dual-purpose — when invoked with context submit_application=True, the SAME wizard also writes status to submitted (or resubmitted if current status is needs_correction) and stores the final application PDF (x_fc_final_submitted_application) + XML file (x_fc_xml_file). It validates the application is PDF + XML is .xml.
When stage 2 is complete (x_fc_device_verification_complete = True) lines with x_fc_adp_approved = False flip to client-100% (see §6). Header-level rollups: x_fc_approved_device_count, x_fc_total_device_count, x_fc_device_approval_done, x_fc_has_unapproved_devices.
The device approval wizard can be re-opened from the client invoice if it was created before approval — account.move.action_open_device_approval_wizard finds the linked SO and routes there.
Stage 2 in mark_as_approved mode (context flag) — the wizard does THREE more things:
- Sets status to
approved(no deductions) orapproved_deduction(any deduction applied). - Saves
claim_number,claim_approval_date,approval_letter(single PDF) — plus a Many2manyapproval_photo_idsof approval screenshots. - Critical attachment persistence fix: many2many_binary attachments uploaded to a TransientModel are garbage-collected when the wizard closes. The wizard copies the attachment data into NEW
ir.attachmentrecords linked tosale.orderso they persist. Both single-file attachments AND screenshots are posted in a SINGLE chatter card (alert-success) with allattachment_idsattached at once. - Calls
_sync_approval_to_invoices(updated_lines)— for each existing invoice, updates lines so unapproved items get 100% to client (price=0 on ADP invoice, full subtotal on client invoice) and approved items get the normal split. Tracksinvoices_updatedcount for the notification.
4.3.1 Assessment completed override mode
assessment_completed_wizard has a scheduling-override branch: if status is currently quotation (the user is skipping the scheduled-assessment step entirely), then:
is_overridecomputes True.override_reasonbecomes mandatory (raisesUserErrorif blank).- The chatter card uses yellow
alert-warningstyling with the override reason shown. - If
notify_authorizer=True, the email to the authorizer includes a yellow note explaining the override. - Normal (non-override) path uses green
alert-successstyling.
Also validates completion_date >= assessment_start_date.
4.4 Three-mode "Application Received" intake (application_received_wizard)
The wizard handles three distinct ways Pages 11 & 12 (client consent) can arrive — this is a key business invariant:
| Mode | Field set | Description |
|---|---|---|
| bundled | x_fc_pages_11_12_in_original = True |
A single PDF that already contains the signed pages 11 & 12 (no separate file needed) |
| separate | x_fc_signed_pages_11_12 + x_fc_signed_pages_filename populated |
Original application + a separate PDF with the signed pages |
| remote | A fusion.page11.sign.request exists in state sent or signed |
Page 11 sent to client/agent for digital signature via /page11/sign/<token> |
x_fc_has_signed_pages_11_12 is a computed boolean that returns True if ANY of the three conditions hold — DO NOT check x_fc_signed_pages_11_12 directly to gate workflow steps, that misses bundled and remote modes. (models/sale_order.py:2921-2942.)
The wizard does two layers of PDF validation:
- Filename constraint — must end in
.pdf(case-insensitive). - Magic-byte check — base64-decoded payload must start with
%PDF-. Test fixtures useb'%PDF-1.4\n%fake pdf for tests'to pass.
default_get picks the initial mode based on existing state: bundled flag set → bundled; separate file present → separate; pending sign request → remote; otherwise bundled.
The wizard has an inline button action_request_page11_signature that opens send_page11_wizard without leaving the parent wizard.
4.5 Document locks (separate from x_fc_case_locked)
sale.order.write enforces document-level locks based on workflow status (models/sale_order.py:7289-7382):
| Document field(s) | Locked when status ≥ |
|---|---|
x_fc_original_application, x_fc_signed_pages_11_12 (+ filenames) |
submitted |
x_fc_final_submitted_application, x_fc_xml_file (+ filenames) |
approved |
x_fc_approval_letter (+ filename) |
billed |
x_fc_proof_of_delivery (+ filename) |
billed |
Bypass requires both: setting fusion_claims.allow_document_lock_override = True AND user in group_document_lock_override. Context flag skip_document_lock_validation=True bypasses for programmatic writes only.
4.6 Case-wide lock (x_fc_case_locked)
Distinct from the per-document status locks above — this is a manual lock toggled via the "Case Locked" switch in the ADP Order Trail tab. When True, the write override blocks all x_fc_* field writes except:
x_fc_case_locked(so you can untoggle it)message_main_attachment_id,message_follower_ids,activity_ids(Odoo plumbing)
Used for archiving completed legacy cases. Bypass with with_context(skip_all_validations=True) (used by crons/email-tracking).
4.7 Document audit trail in chatter
The same write override (models/sale_order.py:7384-7430) preserves the OLD copy of any replaced document (x_fc_original_application, x_fc_signed_pages_11_12, x_fc_final_submitted_application, x_fc_xml_file, x_fc_proof_of_delivery, x_fc_approval_letter) in chatter before it gets overwritten. Set with_context(skip_document_chatter=True) to suppress.
4.8 reason_for_application field (12 values)
x_fc_reason_for_application controls invoicing rules and required fields:
first_access — First Time Access (NO previous ADP)
additions — Additions
mod_non_adp — Modification/Upgrade — original NOT through ADP
mod_adp — Modification/Upgrade — original through ADP
replace_status — Replacement — Change in Status
replace_size — Replacement — Change in Body Size
replace_worn — Replacement — Worn out (past useful life)
replace_lost — Replacement — Lost
replace_stolen — Replacement — Stolen
replace_damaged — Replacement — Damaged beyond repair
replace_no_longer_meets — Replacement — No longer meets needs
growth — Growth/Change in condition
Rules:
previous_funding_dateis required for all reasons exceptfirst_accessandmod_non_adp.<5 yearswarning:x_fc_under_5_years(computed fromprevious_funding_date) — if True and reason ∈ {replace_status,replace_size,replace_worn}, posts a chatter warning when creating client invoice. (Surfaces possible ADP deductions.)- Modification reasons (
mod_non_adp,mod_adp) block client invoice creation until status reachesapprovedorapproved_deduction— surfaced as adanger-styled sticky notification.
4.9 Status-driven side effects in sale.order.write
The 800-line write override (models/sale_order.py:7225-8023) does much more than save fields. When ADP status changes, it:
Auto-populates dates (only if not already in vals):
| Status target | Date field auto-set |
|---|---|
assessment_scheduled |
x_fc_assessment_start_date = today |
assessment_completed |
x_fc_assessment_end_date = today, then auto-advances to waiting_for_application |
submitted / resubmitted |
x_fc_claim_submission_date = today |
accepted |
x_fc_claim_acceptance_date = today |
approved / approved_deduction |
x_fc_claim_approval_date = today |
billed |
x_fc_billing_date = today |
Required-field gates (raise UserError if missing — also enforced by the dedicated wizards, but this is the second safety net):
| Status target | Required fields |
|---|---|
assessment_completed |
assessment_start_date, assessment_end_date |
application_received |
assessment_start_date |
ready_submission |
assessment dates, reason_for_application, client_ref_1, client_ref_2, claim_authorization_date, previous_funding_date (if reason needs it), original_application, x_fc_signed_pages_11_12 — NOTE: this gate uses the raw field, NOT x_fc_has_signed_pages_11_12. The application_received_wizard sidesteps this by populating one of the three computed-sources, but a direct write may fail; see gotcha #21 below. |
submitted / resubmitted |
final_submitted_application, xml_file, claim_submission_date |
approved / approved_deduction |
claim_number, claim_approval_date |
ready_bill |
adp_delivery_date, proof_of_delivery |
billed |
billing_date |
case_closed |
billing_date |
MOD contract_received |
x_fc_case_reference (HVMP Reference Number) |
MOD pod_submitted |
x_fc_mod_proof_of_delivery |
Authorizer required-field gate (only fires when authorizer-related fields are in vals: x_fc_sale_type, x_fc_authorizer_id, x_fc_authorizer_required, x_fc_adp_application_status):
- Always required for:
adp,adp_odsp,wsib,march_of_dimes,muscular_dystrophy - Optional based on
x_fc_authorizer_required='yes'for:odsp,direct_private,insurance,other - Never required for:
rental
Resume from on_hold checks 3-month assessment validity — if x_fc_assessment_expired is True (>90 days since x_fc_assessment_end_date), blocks resume with UserError and a chatter warning showing days past expiry. The OT must redo the assessment.
needs_correction document clearing — when status changes to needs_correction, the override:
- Posts the existing
x_fc_final_submitted_applicationandx_fc_xml_fileto chatter for preservation. - Clears these fields plus
x_fc_final_application_filename,x_fc_xml_filename,x_fc_claim_submission_date. - Posts a yellow warning notice.
Submission history auto-creation — on submitted / resubmitted, creates a fusion.submission.history record (type initial or resubmission) and resets x_fc_acceptance_reminder_sent so the acceptance reminder fires again for the new submission cycle (2026-04 anti-spam fix).
Submission history result update — on accepted or rejected, finds the most recent pending submission record (by date desc, limit 1) and calls update_result() to mark it.
MOD follow-up counter reset — on ANY real MOD status change (detected via pre-write snapshot old_mod_status_by_id), resets x_fc_mod_followup_month_count, _month_start, _escalated, _cap_notified. This is the "new chapter" reset that makes the rolling cap work correctly.
MOD auto-stamp dates — quote_submitted stamps x_fc_case_submitted if blank; funding_approved stamps x_fc_case_approved if blank.
POD signature auto-overlay — when x_fc_pod_signature is set, calls _apply_pod_signature_to_approval_form (overlays the client signature onto the SA Mobility government PDF), and auto-advances SA Mobility from ready_delivery → delivered. Bypass via skip_pod_signature_hook=True context.
Field-mapping aware recompute — reads ICP fusion_claims.field_sale_type / field_so_client_type and triggers _compute_is_adp_sale + line-level _compute_adp_portions if those fields (or their mapped equivalents) changed.
Sync to invoices — when sync fields change (x_fc_claim_number, x_fc_client_ref_1/2, x_fc_adp_delivery_date, x_fc_authorizer_id, x_fc_client_type, x_fc_primary_serial, x_fc_service_start/end_date), calls _sync_fields_to_invoices to push to all linked invoices.
4.10 Special transitions matrix
Each special transition has its own allowed-from set (models/sale_order.py:4426-4762):
| Action | Allowed FROM | Effect |
|---|---|---|
action_adp_put_on_hold |
approved, approved_deduction ONLY (2026-04 rule) |
Status → on_hold, records previous status, resets hold-reminder flags |
action_adp_withdraw |
submitted, resubmitted, accepted, ready_submission, on_hold |
Status → withdrawn, records previous status, sends withdrawal email |
action_adp_mark_rejected |
submitted, resubmitted, accepted |
Status → rejected |
action_adp_mark_denied |
submitted, resubmitted, accepted, approved, approved_deduction |
Status → denied |
action_adp_mark_needs_correction |
submitted, resubmitted, accepted, approved, approved_deduction, rejected |
Status → needs_correction (clears submission docs, see §4.9) |
action_adp_cancel |
Anything except case_closed, cancelled, expired, billed |
Status → cancelled, records previous status |
action_adp_reopen_cancelled |
cancelled AND x_fc_cancel_reported_to_adp=False |
Restores to previous status. If x_fc_cancel_reported_to_adp=True, REJECTS and points to duplicate_for_reassessment. |
action_adp_reopen_expired |
expired |
Back-compat shim — just calls action_adp_duplicate_for_reassessment (per 2026-04 policy: expired cases cannot be self-renewed) |
action_adp_duplicate_for_reassessment |
expired, cancelled |
Creates a new SO with x_fc_previous_sale_order_id pointing here; new order starts at quotation |
action_adp_resubmit_from_denied |
denied |
Status → ready_submission for fresh attempt |
_adp_chatter_transition(title, icon, colour_class, details) is the shared helper for posting Bootstrap-styled cards (alert-warning / alert-danger / alert-secondary / alert-info / etc.).
4.11 Cron-driven transitions
_cron_adp_expire_approved(3 AM daily): approved orders pastfusion_claims.adp_approval_expiry_months(default 12) auto-transition toexpired. Now scansapproved,approved_deduction, andon_hold+ready_delivery(2026-04 update — funding window applies regardless of intermediate state)._cron_auto_close_billed_cases(daily):billed→case_closed1 month after billing._cron_adp_hold_expiry_reminders(9:30 AM daily): monthly reminder to client (authorizer excluded per 2026-04 policy) on on-hold cases; one final pre-expiry warning to client + authorizer ~30 days before funding window closes.
5. The MOD (March of Dimes HVMP) workflow
The Home and Vehicle Modification Program is its own complete lifecycle in models/sale_order.py:438-985, 8590-9805.
5.1 Status (x_fc_mod_status)
quotation → assessment_scheduled → assessment_completed → processing_drawing →
quote_submitted → handed_off (client or authorizer) → awaiting_funding →
funding_approved → contract_received (PCA) → in_production →
project_complete → pod_submitted → case_closed
funding_denied → resubmit / cancel
on_hold / resume
5.2 Submission paths
mod_submission_path_wizard records who submits the application to MOD:
- internal — we submit; auto-triggers
_send_mod_vod_request_emailto the authorizer the first time it's selected (so they fill the Verification of Disability form and send it back). Settings:company.x_fc_mod_vod_formis the latest blank VOD form, auto-attached. - client — client submits themselves.
- authorizer — OT submits.
For non-internal paths, _cron_mod_handoff_followup creates an mail.activity.type.mod_followup activity for the office contact (or sales rep, per company.x_fc_mod_followup_assignee_mode) every 14 days.
5.3 Follow-up rolling cap & cron architecture
The MOD follow-up system has THREE distinct loops sharing one rolling 2-per-30-days cap (fusion_claims.mod_followup_max_per_month × _window_days):
| Cron | Time | What it does |
|---|---|---|
_cron_mod_schedule_followups |
8 AM daily | For orders in quote_submitted / awaiting_funding: if no open follow-up activity AND cap not reached AND x_fc_mod_next_followup_date is in the past, creates a new mail.activity (assignee = sales rep), automated=True to suppress Odoo's default "activity assigned" email. Increments month counter, bumps x_fc_mod_next_followup_date by 14 days. Per-run throttle fusion_claims.mod_followup_schedule_max_per_cron_run = 10 (default). |
_cron_mod_escalate_followups |
10 AM daily | For overdue follow-up activities (date_deadline <= today - 3 days): processes oldest-first. If status moved past follow-up phase → unlinks stale activity. Else calls _send_mod_followup_email; if it returns True (sent), unlinks activity. If cap blocks the send, activity stays put so a human can action it. Per-run throttle fusion_claims.mod_followup_max_per_cron_run = 10. |
_cron_mod_handoff_followup |
9 AM daily | For handoff_to_client orders: creates an activity (assignee from company.x_fc_mod_followup_assignee_mode) with deadline +3 days. Same rolling cap, dedup against existing open activities. Summary includes ({N} days since handoff). |
_mod_followup_cap_state() is a pure read that returns (within_cap, reset_needed, new_start, max_per_month) — call it before creating activities or sending emails, then mutate the order's counters via the returned tuple. The x_fc_mod_followup_cap_notified one-shot flag posts a chatter note the first time the cap blocks a send in a given window, resets when the window expires.
Activity dedup pattern: every MOD cron checks for an existing open mail.activity of type mail_activity_type_mod_followup on the order before creating a new one — prevents daily activity spam.
_send_mod_followup_email enforces the cap internally and returns True only when an email actually goes out. The escalator cron uses that boolean to decide whether to unlink the activity.
5.4 send_to_mod_wizard (multi-mode email composer)
Three modes — drawing, quotation, completion — driven by the mod_wizard_mode context flag. The wizard pre-fills recipients differently per mode, validates per-mode required files, advances MOD status, and attaches the right documents.
| Mode | Required uploads | Status target | TO | CC | Attachments |
|---|---|---|---|---|---|
drawing |
drawing_file |
quote_submitted, stamps x_fc_mod_drawing_submitted_date |
Client | MOD partner (auto-created by email), Authorizer, Sales Rep | Quotation PDF (rendered from fusion_claims.action_report_mod_quotation) + Drawing + Initial Photos |
quotation |
none — just re-send | (no change) | Client | MOD partner, Authorizer, Sales Rep | Quotation PDF + (toggle-controlled) Drawing + Initial Photos |
completion |
completion_photos_file AND pod_file |
pod_submitted, stamps x_fc_mod_pod_submitted_date |
Case worker (or MOD partner fallback) | Authorizer, Sales Rep | Completion Photos + POD |
Subject line includes HVMP reference if set: "{prefix} - {ref} - {client_name}" when x_fc_case_reference is populated; otherwise "{prefix} - {client_name} - {order.name}".
_DOC_NAMES dict (wizard/send_to_mod_wizard.py module-level) maps internal field name → user-facing label:
'x_fc_mod_drawing' → 'Drawing'
'x_fc_mod_initial_photos' → 'Assessment Photos'
'x_fc_mod_pca_document' → 'Payment Commitment Agreement'
'x_fc_mod_proof_of_delivery' → 'Proof of Delivery'
'x_fc_mod_completion_photos' → 'Completion Photos'
_pro_name(field_name, order, orig_filename) builds professional attachment names: "{display_name} - {client_name_underscored} - {order.name}.{ext}". Example: "Drawing - John_Doe - S29958.pdf". Spaces and commas are stripped from the client name.
_get_field_att(order, field_name) — finds the existing ir.attachment for a binary field (Odoo auto-creates one per attachment=True field), renames it in-place to the pro format, and returns the record. Don't create a new attachment for binary fields — the helper reuses the existing one.
The MOD partner is auto-created on first use via _get_mod_partner(email) — name='March of Dimes Canada (HVMP)', is_company=True, company_type='company'.
5.5 Documents tracked on the SO
Binary fields prefixed x_fc_mod_*: drawing, initial_photos, pca_document (Payment Commitment Agreement), proof_of_delivery, completion_photos, application_form_doc, vod_letter, notice_of_assessment, property_tax, proposal_doc. Plus the audit-trail booleans x_fc_mod_trail_* (computed by _compute_mod_trail / _compute_mod_audit_trail).
5.6 MOD invoicing
_create_mod_invoice(partner_id, invoice_lines, portion_type, label) and action_mod_send_invoice on account.move. Invoices use x_fc_invoice_type = 'march_of_dimes'; the MOD invoice template + send action attach the PDF and email the case worker. Distinct from ADP split invoicing.
6. ADP/Client portion calculation rules
The heart of the billing logic lives in models/sale_order_line.py:148-267 (_compute_adp_portions) and is mirrored in models/account_move_line.py:96-164 for invoice lines.
For each line, the algorithm:
- Skip if not ADP sale —
order._is_adp_sale()returns False unlessx_fc_sale_typecontainsadp. ADP portion = 0, client portion = 0 (the line is "regular" billing). - NON-ADP funded products → client 100%.
product.product.is_non_adp_funded()returns True when the device code (case-insensitive, prefix match) starts with:NON-ADP,NON-FUNDED,UNFUNDED,NOT-FUNDED,ACS,ODS,OWP. These are explicitly out-of-ADP-scope codes used to bill ancillary items on a single SO. - Product without a valid ADP device code → client 100%. The line must have a code that resolves against
fusion.adp.device.code(active=True). Code lookup order on the product template (seesale_order_line._get_adp_device_code):x_fc_adp_device_code(this module's field)x_adp_code(Studio/legacy field)default_code(internal reference)- Code in parentheses in the product name, e.g.
[MXA-1618] GEOMATRIX SILVERBACK MAX BACKREST - ACTIVE (SE0001109)→SE0001109. The strict regex onaccount.move.lineisr'\(([A-Z]{2}\d{7})\)'.
- Verification complete AND line not approved → client 100%. (
x_fc_device_verification_complete=Trueandx_fc_adp_approved=False.) - Otherwise — split by client type:
- REG → 75% ADP / 25% client.
- ODS, OWP, ACS, LTC, SEN, CCA → 100% ADP / 0% client.
- Deductions (applied after the base split):
- PCT —
effective_adp_pct = base_adp_pct × (deduction_value / 100); client gets the rest. - AMT — subtract
deduction_value(per unit) from the base ADP portion (floored at 0); client gets the rest.
- PCT —
6.1 Price-source priority for the calculation
When computing the ADP base (NOT price_unit × qty):
product.product_tmpl_id.x_fc_adp_price(this module's stored price)- line
x_fc_adp_max_price(override at the line level) - line
price_unit(last resort)
Invoice lines (account_move_line._compute_adp_portions) prefer fusion.adp.device.code.adp_price directly via a search — slightly different priority chain, but the deduction maths is identical.
Always use product.product.get_adp_price() / .get_adp_device_code() rather than reading the fields directly: those helpers honour the legacy field-mapping ICP (see §15).
6.2 Recomputation triggers
_compute_adp_portions @api.depends on: price_subtotal, product_uom_qty, price_unit, product_id, order_id.x_fc_sale_type, order_id.x_fc_client_type, order_id.x_fc_device_verification_complete, x_fc_deduction_type, x_fc_deduction_value, x_fc_adp_max_price, x_fc_adp_approved. Header rollups x_fc_adp_portion_total / x_fc_client_portion_total recompute on any line change.
7. Split invoicing (the model under §3.1)
ADP REG sales typically yield two invoices on the same SO:
- Client invoice (
x_fc_adp_invoice_portion = 'client') — 25% in REG.action_create_client_invoice(models/sale_order.py:5321-5417) does NOT require device verification (clients can pay before ADP approval) but blocks modification-reason cases (mod_non_adp,mod_adp) until approval is in. A<5yreplacement triggers a chatter warning. - ADP invoice (
x_fc_adp_invoice_portion = 'adp') — 75% in REG, 100% for ODS/OWP/ACS/LTC/SEN/CCA.action_create_adp_invoicerequires verification + POD (x_fc_proof_of_delivery).
Both link back to the SO via x_fc_source_sale_order_id (indexed). Per-order quick-access fields x_fc_adp_invoice_id and x_fc_client_invoice_id can be set manually for invoices that pre-date this module's tracking.
7.1 Two-way sync
account.move.action_sync_to_sale_order (models/account_move.py:698-789) treats the invoice as source of truth: copies x_fc_claim_number, x_fc_client_ref_1/2, x_fc_adp_delivery_date, x_fc_authorizer_id, x_fc_client_type, x_fc_service_start/end_date, x_fc_primary_serial back to the SO, then calls sale.order._sync_fields_to_invoices to push the values out to all sibling invoices. Serial numbers sync line-by-line (_sync_line_fields_to_sale_order) via sale_line_ids → invoice_line_ids mapping. Loop prevention: pass with_context(skip_sync=True) on the writes.
Mark is_manually_modified = True on an invoice to opt it out of the SO → invoice sync direction (the model honours this flag in _sync_fields_to_invoices).
_sync_fields_to_invoices body (models/sale_order.py:8067-8145):
- For each non-cancelled invoice on the order:
- Build a
valsdict ofx_fc_*fields from the SO, but ONLY include each key if the field exists onaccount.move(in invoice._fieldscheck). This is defensive — the module doesn't assume Studio fields are present. - Write with
skip_sync=Trueto prevent recursion. - After all invoices are updated, call
_sync_serial_numbers_to_invoices.
_sync_serial_numbers_to_invoices body (models/sale_order.py:8147-8197):
- Uses dynamic field mappings from settings (
mappings['sol_serial']andmappings['aml_serial']). - Each SO line syncs its OWN serial to its linked invoice lines — no header-fallback. If the SOL has no serial, the AML's serial is left alone.
- Searches via
sale_line_idslink to find matching invoice lines. - Bypass:
skip_sync=Truecontext returns early.
7.2 Sibling totals
x_fc_sibling_adp_total / x_fc_sibling_client_total (computed) read the other portion's total off the source SO so the PDF report can always show both halves even before the sibling invoice exists.
7.3 sale.advance.payment.inv extension
wizard/sale_advance_payment_inv.py adds two new options to the Create Invoice wizard's advance_payment_method:
adp_client— "ADP Client Invoice (25%)" — only valid when client_type =REG; raises if not ADP sale.adp_portion— "ADP Invoice (75%/100%)" — for any ADP client type.
Both route through sale.order._create_adp_split_invoice(invoice_type='client'|'adp'), the same method the per-order action buttons use. This means the standard "Create Invoice" UI also produces split invoices when used on ADP orders.
7.4 Payment registration extension
wizard/account_payment_register.py extends the payment register wizard with:
x_fc_card_last_four(size 4) — required when paying via a card methodx_fc_payment_note— free textx_fc_is_card_payment(computed) — readspayment_method_line_id.x_fc_requires_card_digits(set on the journal form viaviews/account_journal_views.xml). Fallback: keyword match on method name (credit,visa,mastercard,amex)
action_create_payments override validates the last-4 input is exactly 4 numeric digits before delegating. Values persist onto the created account.payment via the x_fc_card_last_four / x_fc_payment_note fields (models/account_payment.py).
The journal form view adds the "Req. Card #" column to both inbound and outbound payment method lists.
7.5 The _create_adp_split_invoice body (models/sale_order.py:5553-5960)
Things to know when reading or modifying this 400-line core method:
- Customer switch on ADP invoice — when
invoice_type='adp', the invoice'spartner_idis set to the ADP partner record (searched by name:'ADP (Assistive Device Program)','Assistive Device Program','ADP', or'ADP -'). The original client becomespartner_shipping_id. So an ADP invoice is billed to ADP, shipped to the client. Client invoices keep the original customer. x_fc_invoice_typeis sale-type aware — client invoices always get'adp_client'. ADP-portion invoices use the sale type directly (adp,adp_odsp,odsp,wsib, ...), preserving funder context for downstream reports.x_fc_adp_billing_status='waiting'is auto-set on creation for ADP invoices (kicks off the billing-deadline cron).invoice_origincarries the portion suffix —S29958 (Client 25%)orS29958 (ADP 75%). This is what surfaces in the user-facing breadcrumbs.- Price-mismatch detection AND auto-correction — when the device-code DB price differs from the product's
x_fc_adp_priceby > $0.01:- Posts a chatter warning listing each mismatched product.
- Auto-updates the product's
x_fc_adp_priceto the DB price (only for products WITHOUT ax_fc_adp_device_code_idlink — products with the Many2one are kept managed via the link instead).
[NOT APPROVED - 100% Client]suffix added to the line name on client invoices when an ADP device exists in DB but wasn't approved. Useful audit trail.- Unapproved + non-ADP-funded items are SKIPPED from the ADP invoice entirely (not even a $0 line). They appear only on the client invoice.
Markupchatter cards at the end of the method are styled with Bootstrap alerts —alert-primary(blue) for client invoice,alert-success(green) for ADP invoice, both with a "View Invoice" link.
7.6 _get_invoiceable_lines override
sale.order._get_invoiceable_lines is overridden (models/sale_order.py:5965) to include ALL line_section, line_subsection, and line_note lines regardless of position. Standard Odoo only includes display lines that have an invoiceable product line AFTER them — which drops warranty notes, refund policy sections, etc. placed at the bottom of the order. This override keeps them on every invoice.
7.7 _prepare_invoice override + invoice type normalization
sale.order._prepare_invoice is overridden (models/sale_order.py:5995-6021) to copy ADP fields to the invoice on creation. It normalizes x_fc_sale_type to lowercase and validates against the selection. If the normalized value isn't in the valid list:
- Contains
'adp'→ falls back to'adp' - Otherwise → falls back to
'other'
Note: when called by _create_adp_split_invoice, this base x_fc_invoice_type gets immediately overwritten — client invoices → 'adp_client', ADP invoices → the raw sale_type. The override in _prepare_invoice matters mainly for invoices created OUTSIDE the split flow (e.g., via the standard "Create Invoice" button without the ADP method selection).
7.8 Document chatter helper (_post_document_to_chatter)
The shared helper for document audit trail (models/sale_order.py:6026-6087):
- Default mode — references the existing
ir.attachment(Odoo creates one for eachattachment=Truebinary field). preserve_copy=True— creates a SEPARATE<name>_archived.<ext>copy. Used when the original is about to be deleted/replaced (e.g., needs_correction clearing) and we need to snapshot before Odoo's attachment is removed.- Posts a
<strong>{label}</strong> uploaded by <b>{user}</b>chatter message with the attachment attached.
Companion utilities: _build_attachment_name(field_name) builds the user-facing filename, _get_document_attachment(field_name) resolves the existing attachment, _prepare_attachment_for_email(attachment, field_name) renames it for outbound mail.
7.9 MOD action method index
All on sale.order, all in models/sale_order.py:8594-8901:
| Method | What it does | Status target |
|---|---|---|
action_mod_schedule_assessment |
bare write | assessment_scheduled, stamps x_fc_mod_assessment_scheduled_date |
action_mod_complete_assessment |
bare write | assessment_completed, stamps x_fc_mod_assessment_completed_date |
action_mod_processing_drawing |
writes processing_drawings, then opens send_to_mod_wizard in mod_wizard_mode='drawing' |
progresses to quote_submitted via the wizard |
action_mod_awaiting_funding |
opens mod_awaiting_funding_wizard |
awaiting_funding |
action_mod_funding_approved |
opens mod_funding_approved_wizard (records case worker + HVMP ref) |
funding_approved |
action_mod_funding_denied |
opens mod_funding_denied_wizard (category + reason) — 2026-04 was bare write, now captures denial reason |
funding_denied |
action_mod_contract_received |
opens mod_pca_received_wizard (PCA upload + full/partial invoice split) |
contract_received |
action_mod_in_production |
bare write | in_production, stamps x_fc_mod_production_started_date |
action_mod_project_complete |
bare write | project_complete, stamps x_fc_mod_project_completed_date |
action_mod_pod_submitted |
opens send_to_mod_wizard in mod_wizard_mode='completion' |
pod_submitted via the wizard |
action_mod_close_case |
bare write | case_closed, stamps x_fc_mod_case_closed_date |
action_mod_on_hold |
saves previous status into x_fc_mod_previous_status_before_hold (2026-04 fix — was being lost) |
on_hold |
action_mod_resume |
restores from x_fc_mod_previous_status_before_hold (default in_production) — 2026-04 fix — was hardcoded to in_production |
previous status |
action_mod_set_submission_path |
opens mod_submission_path_wizard (internal/client/authorizer) |
n/a — sets x_fc_mod_submitted_by |
action_mod_request_vod |
emails authorizer the blank VOD form (also auto-fired when internal path first selected) | n/a |
action_mod_handoff_to_client |
only when submitted_by ∈ (client, authorizer); requires proposal_doc + drawing |
handoff_to_client, stamps x_fc_mod_handoff_date |
action_mod_confirmed_submission |
opens mod_submission_confirmed_wizard |
confirms client/authorizer submitted |
action_mod_resubmit_from_denied |
opens mod_resubmit_wizard; only from funding_denied |
back to earlier status via wizard |
action_mod_cancel_from_denied |
only from funding_denied |
cancelled |
action_mod_reopen_cancelled |
only from cancelled |
need_to_schedule, clears x_fc_mod_funding_denial_reason |
action_cancel (override on sale.order) |
when the SO is cancelled (built-in), also force-sets x_fc_mod_status='cancelled' for MOD orders |
cancelled |
_get_mod_partner() — finds or creates the MOD partner by email fusion_claims.mod_default_email (default hvmp@marchofdimes.ca). New record is created with name 'March of Dimes Canada (HVMP)', is_company=True.
_create_mod_invoice(partner_id, invoice_lines, portion_type, label) — the shared MOD invoice creator. Sets x_fc_invoice_type='march_of_dimes', x_fc_adp_invoice_portion=portion_type, populates narration with HVMP reference + client + case worker + SO + vendor code as an HTML block.
7.10 Stage 2 invoice sync (_sync_approval_to_invoices) — re-posts posted invoices
When the device approval wizard's mark_as_approved mode runs _sync_approval_to_invoices(updated_lines), the method walks every non-cancelled invoice linked to the order and rewrites line price_unit + suffixes the line name based on the new approval state:
| State | Client invoice line | ADP invoice line |
|---|---|---|
| Non-ADP funded item (code in NON-ADP/NON-FUNDED/etc.) | price_unit = full subtotal, name unchanged |
price_unit = 0, name suffixed [NON-ADP - Excluded] |
| Unapproved ADP device | price_unit = full subtotal, name suffixed [NOT APPROVED - 100% Client] |
price_unit = 0, name suffixed [NOT APPROVED - Excluded] |
| Approved ADP device | price_unit = x_fc_client_portion / qty, name unchanged |
price_unit = x_fc_adp_portion / qty, name unchanged |
For POSTED invoices, the method calls invoice.button_draft() → write → action_post() — i.e. it resets to draft, rewrites the lines, then re-posts. If the posted invoice has already been exported to ADP or emailed to the client, the reset-then-repost cycle can break sequence numbers, re-fire post-actions, or invalidate exports. Set is_manually_modified=True on the invoice to opt it out of this sync direction if you need to lock it.
The wizard reports the count of invoices_updated in the success notification.
7.11 Activity scheduling pattern (_schedule_or_renew_adp_activity)
On account.move, the helper _schedule_or_renew_adp_activity(activity_type_xmlid, user_id, date_deadline, summary, note) is the shared pattern for ADP billing/correction activities:
- Finds existing activity of the same type AND same user_id on the record.
- If found: UPDATES
date_deadline,summary,note(preserves existing note if new is blank). - If not found: creates new via
activity_schedule.
Companion _complete_adp_activities(activity_type_xmlid) calls activity.action_feedback(feedback='Completed automatically') on every activity of the given type — used to auto-close ADP Billing activities when the invoice flips to submitted/payment_issued, and ADP Correction activities when flipped to resubmitted/payment_issued.
This dedup pattern shows up in three places: ADP billing reminders, ADP correction reminders, and MOD follow-ups — different fields, same logic.
7.12 MOD PCA dual-invoice split (wizard/mod_pca_received_wizard.py)
When contract_received is set via this wizard, the user picks approval_type:
full— creates ONE invoice (to the MOD partner) for the full order amount; client owes nothing.partial— creates TWO invoices simultaneously:- MOD invoice: each line ×
(approved_amount / order.amount_untaxed)= MOD's share. - Client invoice: each line ×
(1 - ratio)= client's share. If the client's per-line amount is ≤ 0 (fully covered), the line name is suffixed\n[Covered by March of Dimes]andprice_unit=0.
- MOD invoice: each line ×
Live preview (preview_line_ids — fusion_claims.mod.funding.approved.wizard.line transient lines) updates via _onchange_compute_preview as the user types approved_amount. Both invoices flow through sale.order._create_mod_invoice(partner_id, invoice_lines, portion_type, label).
portion_type='adp' for the MOD partial invoice: the wizard reuses the x_fc_adp_invoice_portion field (set to 'adp') for the MOD invoice as well as the ADP one — so x_fc_adp_invoice_portion == 'adp' does NOT mean ADP. Always check x_fc_invoice_type ('march_of_dimes' vs 'adp') when disambiguating.
8. ADP billing lifecycle & posting schedule
8.1 Posting schedule mixin
fusion_claims.adp.posting.schedule.mixin (models/adp_posting_schedule.py) is inherited by sale.order, account.move, and fusion_claims.adp.export.record.
posting cycle = every `fusion_claims.adp_posting_frequency_days` days (default 14)
starting from `fusion_claims.adp_posting_base_date` (default 2026-01-23)
submission deadline = Wednesday 6 PM of the posting week
delivery reminder = Tuesday of the posting week
billing reminder = Monday of the posting week
payment processed = posting day + 7
payment received = posting day + 10
Methods: _get_next_posting_date, _get_current_posting_date, _get_posting_week_monday/tuesday/wednesday, _get_expected_payment_date, _get_payment_processed_date, _is_past_submission_deadline (checks past 6 PM Wed), _get_adp_billing_reminder_user, _get_adp_correction_reminder_users.
8.2 Billing status (post-export)
x_fc_adp_billing_status on account.move:
not_applicable → waiting → submitted → resubmitted / need_correction → payment_issued / cancelled
Cron _cron_renew_billing_reminders (account.move) reschedules overdue mail_activity_type_adp_billing activities on waiting invoices to the next posting week's Monday. _cron_renew_correction_reminders does the same for need_correction against mail_activity_type_adp_correction activities (scheduled for all users in fusion_claims.adp_correction_reminder_user_ids).
Auto-write hook: when payment_state transitions to paid / in_payment on an ADP invoice currently submitted / resubmitted / waiting, the billing status flips to payment_issued automatically (account.move.write override at models/account_move.py:884-892).
9. ADP claim export
wizard/adp_export_wizard.py (fusion_claims.export.wizard):
- Pulls invoices via
active_ids, filters toout_invoice/out_refund. - For each line, regenerates the comma-separated ADP claim line using the device-code database price, then verifies the stored
x_fc_adp_portion/x_fc_client_portionagainst the recomputed values (tolerance$0.01 × qty). CRITICAL: Verification mismatches RAISE aUserErrorthat BLOCKS the export — the previous draft of this doc was wrong; mismatches don't just warn, they hard-stop and the user must fix invoices before re-trying. - Skips lines whose device code resolves to one of
FUNDING,NON-FUNDED,N/A,NA,NON-ADP,LABOUR,DELIVERY, or empty. - Per-unit-quantity expansion: ADP expects
qty=1per line. A line withqty=3generates 3 export rows, each with the per-unit portion (stored_portion / qty). - Filename format is fixed:
{vendor_code}_{YYYY-MM-DD}.txt— ADP rejects renamed files. If the same filename already exists infusion_claims.adp.export.record, the wizard adds a yellow warning but still proceeds; the user must manually rename for the resubmission case. - CSV format (no header) — 16 fields per row, comma-separated, with 3 reserved/empty fields at positions 7, 8, 11 (1-indexed):
vendor_code, claim_number, client_ref_2, invoice_number, invoice_date, delivery_date, [empty], [empty], device_code, serial_number, [empty], qty(=1), device_price, adp_portion, client_portion, client_type - Creates a
fusion_claims.adp.export.record(models/adp_export_record.py) — the file lives there, the model auto-extracts invoice numbers from the file content and back-links them viainvoice_ids. Records are grouped byyear/month/posting_period_labelin the menu.
9.1 ADP export record helpers (models/adp_export_record.py)
| Method | Purpose |
|---|---|
_parse_export_filename(filename) |
Regex ^(.+?)_(\d{4}-\d{2}-\d{2})\.\w+$ extracts (vendor_code, file_date) from a filename. Returns (None, None) if unparseable. |
_get_posting_period_for_file(file_date) |
Maps a file date to the posting period it belongs to. If file_date <= current_posting, returns current_posting. Otherwise returns current_posting + frequency (next posting). Handles pre-base-date floor division correctly. |
_collect_subfolder_ids(Document, parent_ids) |
Recursively finds all documents.document subfolder IDs under given parents (used by migration helper). |
migrate_from_documents() |
One-shot migration — searches documents.document records under "ADP Billing Files" hierarchy, creates export records, archives the originals (active=False). Idempotent (skips by filename). Called via "Migrate ADP Export Files" button in Settings. Only runs if the documents app is installed. |
action_download |
Single-file download via /web/content URL. |
action_download_zip |
Multi-record action — zips all selected export records into a single ADP_Export_Files_{YYYY-MM-DD}.zip via zipfile.ZIP_DEFLATED and serves it as a transient ir.attachment. |
action_view_invoices |
Opens the list of invoice_ids linked to the record. |
- Per-invoice flags updated on each invoice in the export:
adp_exported=True,adp_export_date=now,adp_export_count += 1. - Settings:
fusion_claims.vendor_code.
The _validate_dates helper warns on future invoice/delivery dates and delivery-after-invoice mismatches — these are soft warnings, only the calculation mismatches are hard blocks.
10. ADP Mobility Manual / device codes
fusion.adp.device.code (models/fusion_adp_device_code.py, 428 lines):
Fields: device_code (unique, indexed, required), device_type (indexed), manufacturer, build_type (modular / custom_fabricated), device_description, adp_price, max_quantity, sn_required, active, last_updated. Display name auto-formats as CODE - DESCRIPTION ($PRICE).
Loaded on every install/upgrade from data/device_codes/adp_mobility_manual.json. Importable via JSON or CSV (device_import_wizard, manager-only). The model defines _clean_text, _parse_price utilities; CSV expects columns Device Type, Manufacturer, Device Description, Device Code, Qty, Approved Price, Serial.
Write override (fusion_adp_device_code.write): when adp_price or device_code changes, propagates to all product.template.x_fc_adp_device_code_id linked products (x_fc_adp_price, x_fc_adp_device_code denormalized fields).
10.1 Linking products
product.template (models/product_template.py):
x_fc_is_adp_product(bool) — toggle to mark ADP product.@api.constrainsrequires ax_fc_adp_device_code_idwhen True.x_fc_adp_device_code_id(Many2one) — the canonical link.x_fc_adp_device_code(Char) — denormalized for query/legacy use.x_fc_adp_price(Float) — denormalized for query/legacy use.x_fc_adp_device_type,x_fc_adp_build_type,x_fc_adp_max_quantity— allrelatedfields stored.- Also:
x_fc_security_deposit_type/amount/percentfor rentals (sibling module fusion_rental). action_sync_adp_price_from_database— admin button to re-sync the link from the JSON for a given product.
11. Client profile, applications, XML parser
11.1 fusion.client.profile (models/client_profile.py)
One record per client (linked to res.partner). Stores personal info, address, contact, benefit eligibility (ODSP/OWP/ACSD/WSIB/VAC), and latest medical condition + mobility status. _compute_claim_stats aggregates claim_count, total_adp_funded, total_client_portion, total_amount across the partner's sale_order_ids filtered to ADP sale types. _compute_ai_analysis writes a human-readable summary into ai_summary and risk flags (ai_risk_flags) — frequency analysis (avg days between applications), multiple-replacement detection.
11.2 fusion.adp.application.data (models/adp_application_data.py, 670 lines)
One record per submitted ADP application (parsed from XML). Captures all ~300 XML fields for round-trip fidelity:
- Section 1: applicant biographical + benefits confirmation.
- Section 2 (devices/eligibility): medical condition, mobility status, previously-funded checkboxes (12 device types), currently-required device checkboxes (16 device types).
- Section 2a: Ambulation Aids / walkers + paediatric.
- Section 2b: Manual Wheelchairs.
- Section 2c: Power Bases / Scooters.
- Section 2d: Positioning / Seating.
Each section captures every confirmation checkbox, every prescription field (seat width/depth/height, handle height, brakes, wheels, back support, custom modifications, etc.). raw_xml stores the original; xml_data_json stores a dot-notation JSON dict for export.
11.3 fusion.xml.parser (models/xml_parser.py, 772 lines)
AbstractModel. Public API:
parse_from_binary(binary_data, sale_order=None)— base64 → XML → records.parse_and_create(xml_content, sale_order=None)— string → records.reparse_existing(app_data_record)— re-parse existingraw_xmlin place.
Flow: XML → 1:1 dot-notation JSON dict (_xml_to_json) → model values (_json_to_model_vals) → find-or-create fusion.client.profile keyed by health card → create fusion.adp.application.data → auto-link the application's authorizer to the SO by ADP registration number (x_fc_authorizer_number on res.partner → matched against authorizer_adp_number from the XML).
Auto-parse: setting fusion_claims.auto_parse_xml = True runs the parser when x_fc_xml_file is uploaded to a SO.
Bulk import: xml_import_wizard (manager-only) processes multiple XML files at once.
Profile matching priority (_find_or_create_profile):
- Health Card Number (exact).
- First + Last name + DOB (case-insensitive, exact DOB).
If neither matches, a new profile is created. Either way, profile_vals is always written — existing profiles get updated with the latest XML data, so the most recent application's personal info wins.
Authorizer auto-linking (_link_authorizer_by_adp_number):
- Match
res.partnerbyx_fc_authorizer_number(exact). - Fallback: fuzzy name match (
name ilike "{first} {last}"OR"{last}, {first}"). - Learn-as-we-go enrichment: when a partner matches by NAME AND has no
x_fc_authorizer_number, the parser writes the ADP number to the partner. Future imports for the same OT then match by number directly. This is how the authorizer database gets populated over time without manual data entry. - Skips if SO already has
x_fc_authorizer_idset; skips ADP numbers that are'NA','N/A', or empty.
Date parsing (_pd): tries %Y/%m/%d, %Y-%m-%d, %Y%m%d in that order. Returns False on failure. Used for every date field in the XML.
Section 4 fields also captured: Vendor 1 + Vendor 2 (business name, ADP number, representative, position, location, phone+ext, sign date); Equipment Spec table (Table2.Row1.Cell1–5); POD (received_by, date); Note to ADP section markers (boolean per section: section1, section2a–d, section3and4) plus vendor replacement / custom / funding chart / letter free-text notes.
12. AI integration
12.1 Native AI agent (Odoo 19 ai module)
data/ai_agent_data.xml:
- 3
ir.actions.serverrecords withuse_in_ai=True— call methods onai.agent(extended viamodels/ai_agent_ext.py):_fc_tool_search_clients(search_term, city_filter, condition_filter)_fc_tool_client_details(profile_id)_fc_tool_claims_stats()
- One
ai.topic("Fusion Claims Client Intelligence") bundles them. - One
ai.agent("Fusion Claims Intelligence") with the topic + system prompt + modelgpt-4.1+analyticalresponse style.
fusion.client.profile.action_open_ai_chat opens a chat (channel of type ai_chat) seeded with the client's context.
12.2 Legacy fusion.client.chat.session (models/client_chat.py)
Hand-rolled OpenAI chat layer. Calls https://api.openai.com/v1/chat/completions directly. Settings: fusion_claims.ai_api_key (manager-only), fusion_claims.ai_model (gpt-4o-mini / gpt-4o / gpt-4.1-mini / gpt-4.1). Falls back to local DB-only responses (_generate_local_response) when no key is configured. Kept for back-compat; the native AI agent above is the preferred path going forward.
13. Technician tasks integration
models/technician_task.py (674 lines) extends fusion.technician.task (from fusion_tasks):
- Adds
sale_order_id+purchase_order_id(validates exactly one is set unless task is from cross-instance sync or is anltc_visit). - Onchange auto-fills
partner_id+ address from the SO/PO shipping/destination address. - Hook overrides:
_create_vals_fill— copy partner + address into create vals._on_create_post_actions— chatter notice; ifmark_ready_for_deliverycontext flag is set, advance SO toready_delivery; ifmark_odsp_ready_for_delivery, advance ODSP order._check_completion_requirements— rental pickup tasks block completion until inspection done._on_complete_extra— ODSPready_delivery→delivered; rental pickup → write inspection back to SO and refund security deposit (if passed) or schedule activity (if flagged)._on_cancel_extra— delivery cancellation reverts SO tox_fc_status_before_delivery(if no other delivery tasks remain), sends cancellation email.
- Rental inspection fields:
rental_inspection_condition(excellent/good/fair/damaged),rental_inspection_notes,rental_inspection_photo_ids(max 6, opens in FileViewer via gallery hook). - Email overrides: routes through
sale_order._email_buildso technician emails match the rest of the project's email style; CCs the SO sales rep + office notification recipients.
14. Page 11/12 signing workflow
ADP form 13027E pages 11 & 12 require client and authorizer signatures.
14.1 Page 11 (client consent)
fusion.page11.sign.request (models/page11_sign_request.py):
- Standalone signing request: random
access_token(UUID4), public URL/page11/sign/<token>,stateofdraft / sent / signed / expired / cancelled. - Signer can be client, spouse, parent, legal_guardian, power_of_attorney, public_trustee. Agent details (full address) captured when not the client.
consent_signed_by:applicant(client signs themselves) oragent(anyone else).send_page11_wizardauto-sets this based onsigner_type.- Public security ACL:
base.group_publichas read-only access tofusion.page11.sign.requestso the public sign page can resolve the token. _generate_signed_pdfusesfusion.pdf.template(from another module; the active template is namedadp_page_11orpage 11) to render a filled PDF, then writes the result tox_fc_signed_pages_11_12on the SO + creates anir.attachment.send_page11_wizardopens the composer. Default expiry 7 days. Pre-fills signer frompartner_id.name+partner_id.email. Signer relationship auto-fills from the signer_type label (spouse → "Spouse", etc.).- Form view header buttons: Resend Email (sent/expired), Request New Signature (signed/cancelled, with confirm dialog), Cancel (draft/sent). Statusbar: draft → sent → signed.
- Cron
_cron_expire_requests(2 AM daily) marks expired unsigned requests.
14.2 Page 12 (authorizer + vendor)
Tracked directly on the SO via two booleans + signer fields:
x_fc_page12_authorizer_signed— OT signs after page 11 is received.x_fc_page12_vendor_signed+x_fc_page12_vendor_signer_id— designated vendor signer (fusion_claims.designated_vendor_signersetting) signs on the company's behalf.
14.3 SA Mobility + OW Discretionary signing
Two government-issued PDFs are filled by Python (using pdfrw):
| Form | Template | Wizard |
|---|---|---|
| SA Mobility form (ODSP SA division) | static/src/pdf/sa_mobility_form_template.pdf (482 KB) |
odsp_sa_mobility_wizard (560 lines) |
| OW Discretionary Benefits form | static/src/pdf/discretionary_benefits_form_template.pdf (1.1 MB) |
odsp_discretionary_wizard (395 lines) |
The SA Mobility wizard's _build_field_mapping() is the field-name reference for the gov form 13007E:
- Vendor section:
Text 1(with space — gov form quirk),Text2,Text3, ...,Text7. Salesperson:Text8,Text9. Client:Text10,Text11. - Member ID is 9 separate text boxes (
Text12-Text20) — one per digit, left-justified via.ljust(9). - Relationship checkboxes:
Check Box16(self),17(spouse),18(dependent). - Device type checkboxes:
Check Box19(manual_wheelchair) ...Check Box24(other). - Parts table (up to 6 rows):
Text30-Text59in groups of 5 (qty, description, unit_price, taxes, amount). Total atText60. - Labour table (up to 5 rows):
Text61-Text80in groups of 4 (hours, rate, taxes, amount). Total atText81. - Additional Fees (up to 4 rows):
Text82-Text97in groups of 4 (description, rate, taxes, amount). Total atText98. - Estimated totals summary:
Text99(parts),Text100(labour),Text101(fees),Text102(grand total). - Page 2 Notes/Comments area:
Text1(collides with vendorText 1— different field, gov-form naming quirk).
The wizard pre-populates from the SO automatically: products with default_code == 'LABOR' → labour tab; everything else → parts tab.
_get_template_path() uses raw os.path operations to resolve the template file, NOT Odoo's tools.misc.file_path(). Brittle if the module is loaded as a zip — consider migrating to file_path('fusion_claims/static/src/pdf/sa_mobility_form_template.pdf') if that issue arises.
14.3.1 OW Discretionary Benefits wizard quirks
This wizard fills a different gov form (discretionary_benefits_form_template.pdf, 1.1 MB) with its own set of footguns:
-
Uses
PyPDF2, notpdfrw— because the gov PDF is AES-encrypted (no password, just "protected mode"). pdfrw cannot decrypt; PyPDF2 handles it viareader.decrypt(''). If pdfrw is the only PDF library available, the wizard will fail. Both are optional Python deps. -
Preserves
/AcroFormfrom the original document and sets/NeedAppearances= True so the filled form renders correctly in Acrobat (without this, the values are stored but not visible). -
Splits text and checkbox fields into separate dicts. Text fields use PyPDF2's bulk
update_page_form_field_values; checkboxes are updated directly by mutating each annotation's/Vand/ASto/1(checked) or/Off(unchecked) viaNameObject. PyPDF2 doesn't have a clean API for this. -
The gov form has misleading field names — they don't match physical layout. Hard-earned mapping:
Field name (PDF) Actually populates txt_First[0]Client Name txt_CITY[1]Member ID (not city — note the [1]index)txt_add[0]Address txt_CITY[0]City (the [0]index — different from[1])txt_email[0]Phone (not email) txt_bphone[0]Alternate Phone txt_emp_phone[0]Email (not employer phone) txt_clientnumber[0]Date (not client number) CheckBox15[0]Medical Equipment CheckBox11[0]Dentures CheckBox11[1]Vision Care CheckBox13[0]Other TextField1[0]Description/details Don't normalize or rename when building the mapping — write to the names exactly as ODSP shipped them.
-
4 item types:
medical_equipment,vision_care,dentures,other.
ODSP signing positions (signature/initial fields) are managed via Configuration > PDF Templates with a drag-and-drop visual editor — managed in fusion.pdf.template records (category=ODSP), not as static templates in data/pdf_template_data.xml (which is now an empty placeholder with a "templates retired" note).
static/src/pdf/sa_mobility_page2_sample.pdf (241 KB) is a reference sample showing what page 2 should look like when filled.
14.4 ODSP submission paths (odsp_submit_to_odsp_wizard)
Three actions on the wizard, all of which (a) render the standard sale.action_report_saleorder PDF, (b) attach x_fc_odsp_authorizer_letter, and (c) advance status quotation/documents_ready → submitted_to_odsp:
| Action | What it does |
|---|---|
action_send_email |
Emails the ODSP office (x_fc_odsp_office_id) with both attachments. Uses _send_odsp_submission_email with email_body_notes. |
action_send_fax |
Opens fusion_faxes.send.fax.wizard (soft-depended on) with attachments pre-loaded and the office's x_ff_fax_number if available. |
action_send_fax_and_email |
Sends email first, then chains into the fax wizard via display_notification.next. |
The wizard can override x_fc_odsp_office_id per-submission and syncs the choice back to the SO.
14.4.1 ODSP submission document bundling
Two methods on sale.order build the email payload sent to the funder:
| Method | Used by | Attaches |
|---|---|---|
_sa_mobility_submit_documents |
SA Mobility flow | (1) signed SA form (x_fc_sa_signed_form OR x_fc_sa_physical_signed_copy), (2) internal POD PDF via _get_sa_pod_pdf, (3) latest posted invoice PDF (Odoo's account.account_invoices report) |
_odsp_std_submit_documents |
ODSP Standard flow | (1) x_fc_odsp_approval_document, (2) internal POD PDF, (3) auto-confirms SO and creates invoice if no posted one exists, then attaches invoice PDF |
Both call the corresponding _send_*_email method (SA Mobility → _send_sa_mobility_completion_email; Standard → _send_odsp_submission_email). Both post a green chatter card listing the attachment names.
14.4.2 Ontario Works invoice flow
_ow_payment_create_invoice (action_odsp_payment_received for OW orders):
- Auto-confirms the SO if
state != 'sale'. - Calls standard
self._create_invoices(). - Writes
x_fc_source_sale_order_idon the invoice. - Advances ODSP status to
payment_receivedwith chatter note "Ontario Works payment confirmed. Invoice {name} created." - Returns an action that opens the new invoice form.
This is the ONE case where payment comes BEFORE delivery — the OW office pays the vendor, the vendor delivers later (and the auto-close cron uses delivered not payment_received as the timer trigger — see §18).
14.4.3 ODSP on_hold / resume (2026-04 fix)
Mirrors the MOD on_hold pattern. x_fc_odsp_previous_status_before_hold saves the current status; action_odsp_resume restores it (falls back to quotation only for legacy records held before the fix shipped). Blocks hold from on_hold/case_closed/cancelled. Calls _odsp_advance_status which routes to whichever of x_fc_sa_status/x_fc_odsp_std_status/x_fc_ow_status is active.
14.4.4 SA Mobility signature overlay — TWO mechanisms
| Method | Used when | Mechanism |
|---|---|---|
action_sign_sa_mobility_form |
Client signs the SA Mobility form directly (Page 2 client consent) | Hard-coded coordinates: writes printed name at (180, h-180) and (72, h-560), date at (350, h-560), signature image at (72, h-540, 200×50px). Uses reportlab.pdfgen.canvas + odoo.tools.pdf.PdfFileReader/Writer. Brittle — if the gov PDF layout changes, the coordinates must be re-measured. |
_apply_pod_signature_to_approval_form |
POD signature collected (auto-fired by write override when x_fc_pod_signature is set) |
PDFTemplateFiller from fusion_authorizer_portal — reads field positions from the active fusion.pdf.template (category=odsp), uses per-case x_fc_sa_signature_page. Configurable via drag-and-drop visual editor, not code. Bypass via skip_pod_signature_hook=True context. |
The PDFTemplateFiller approach is the preferred path going forward — it survives gov form revisions because positions live in the database, not in Python code.
14.5 ODSP signature-page setup (odsp_ready_delivery_wizard)
When transitioning SA Mobility / OW orders to ready_delivery, this wizard:
- Loads field positions from the active
fusion.pdf.template(category=odsp). - Renders a preview image of the chosen
signature_pageusingpdf2image.convert_from_bytes+ PILImageDraw, with colored markers overlaid at each field position:- blue for text fields (sample text: "John Smith")
- purple for date fields (sample: "2026-02-17")
- red rectangles for signature fields
- Writes
x_fc_sa_signature_pageto the SO. - Returns an action that opens the technician task form pre-filled with
default_task_type='delivery',default_pod_required=True, andmark_odsp_ready_for_delivery=Truecontext — the task model's_on_create_post_actionshook then advances ODSP status toready_delivery.
action_preview_full opens the full PDF via the custom fusion_claims.preview_document client action tag.
15. Field-mapping system (legacy Studio support)
This module ships with a field mapping layer that lets it run against existing Studio-created fields in production databases. The mapping uses ir.config_parameter keys (fusion_claims.field_*) → field name. Defaults live in data/ir_config_parameter_data.xml.
Getters that respect the mapping:
sale.order._get_sale_type / _get_client_type / _get_authorizer / _get_claim_number / _get_client_ref_1 / _get_client_ref_2 / _get_adp_delivery_dateaccount.move._get_invoice_type / _get_client_type / _get_authorizer / _get_claim_number / _get_client_ref_1 / _get_client_ref_2 / _get_adp_delivery_datesale.order.line._get_serial_number / _get_device_placementaccount.move.line._get_serial_number / _get_device_placementproduct.product.get_adp_device_code / get_adp_price
Always go through the getters when reading these fields. Direct attribute access breaks legacy databases where the canonical field name is x_studio_* instead of x_fc_*.
Auto-detection:
fusion_claims.config.action_detect_existing_fields(models/fusion_central_config.py) scans for customx_*fields on the canonical models, fuzzy-matches against keyword lists, and writes the discovered names into the matchingir.config_parameterkeys. Surfaces unmapped fields for review.field_mapping_config_wizardis the form-based config UI.
The full list of mapping keys is in models/res_config_settings.py (fc_field_*) — ~25 ICP keys covering SO, SOL, invoice, invoice line, and product fields.
16. Email system
Every workflow transition has a corresponding _send_*_email method on sale.order, all routed through fusion.email.builder.mixin._email_build(...):
16.1 ADP emails (~25)
Each method is on sale.order and routed through _email_build. Key ones:
| Method | Trigger | Recipients | Attachments |
|---|---|---|---|
_send_submission_email |
status → submitted/resubmitted |
Client (TO), Authorizer + Sales Rep + Office (CC) | Final Application PDF + XML File |
_send_assessment_scheduled_email |
status → assessment_scheduled |
Client (TO), Authorizer + Sales Rep (CC) | none |
_send_application_received_email |
status → application_received |
Authorizer (TO), Sales Rep (CC) | none |
_send_application_reminder_email |
cron, X=4 days after assessment_end_date if still waiting_for_application/assessment_completed AND x_fc_application_reminder_sent=False |
Authorizer (TO), Sales Rep + Office (CC). Sets x_fc_application_reminder_sent=True after send. |
none |
_send_application_reminder_2_email |
cron, X+Y=8 days after assessment_end_date if first reminder sent AND x_fc_application_reminder_2_sent=False. Email mentions 90-day assessment validity. |
Authorizer (TO), Sales Rep + Office (CC) | none |
_send_accepted_email |
status → accepted |
Client + Authorizer (TO) | none |
_send_approval_email |
status → approved/approved_deduction |
Client (TO), Authorizer + Sales Rep + Office (CC). Differentiates approved (standard message) vs approved_deduction (extra note about deduction). |
Generates action_report_approved_items PDF via _generate_approved_items_pdf. Also embeds _build_approved_items_html table inline. |
_send_denial_email |
status → denied |
Client (TO), Authorizer + Sales Rep + Office (CC). Urgent style. | none |
_send_rejection_email |
status → rejected |
Client (TO), Authorizer + Sales Rep + Office (CC). 2026-04 fix: client previously excluded. Includes rejection_reason label (5 enum values) + free-text x_fc_rejection_reason_other. |
none |
_send_correction_needed_email(reason) |
status → needs_correction (typically with wizard's reason text) |
Client + Authorizer + Sales Rep + Office | none |
_send_billed_summary_email |
status → billed |
Authorizer + Sales Rep (TO). Green success card with totals. | none |
_send_case_closed_email |
status → case_closed |
(uses _get_email_recipients standard) |
none |
_send_ready_for_delivery_email |
called from ready_for_delivery_wizard after task creation |
Client + Authorizer + technicians (CC) | none |
_send_on_hold_email, _send_withdrawal_email, _send_expired_email, _send_cancelled_email |
corresponding status transitions | varies | none |
_build_approved_items_html(for_pdf=False) builds an HTML table with columns S/N | ADP Code | Device Type | Product | Qty | ADP Portion | Client Portion | (Deduction — only when any line has one). Total row at bottom. Different font stack for PDF (Arial,Helvetica,sans-serif) vs email (system font). Truncates product names > 40 chars in email mode.
_generate_approved_items_pdf renders fusion_claims.action_report_approved_items QWeb report → attaches with filename {first}_{last}_Approved_Items.pdf.
16.1.1 Quotation/SO send override
sale.order.action_quotation_send is overridden to auto-select the ADP email template:
- Draft state →
email_template_adp_quotation - Sale/done state →
email_template_adp_sales_order - Layout:
mail.mail_notification_layout - Context flags:
mark_so_as_sent=True,force_email=True
Falls back to the standard behaviour for non-ADP sales. Mirror override exists on account.move.action_invoice_sent for ADP invoices (selects email_template_adp_invoice).
16.2 MOD emails (~14)
_send_mod_assessment_scheduled_email, _send_mod_assessment_completed_email, _send_mod_quote_submitted_email, _send_mod_vod_request_email, _send_mod_handoff_email, _send_mod_funding_approved_email, _send_mod_funding_denied_email, _send_mod_contract_received_email, _send_mod_invoice_submitted_email, _send_mod_initial_payment_email, _send_mod_project_complete_email, _send_mod_pod_submitted_email, _send_mod_final_payment_email, _send_mod_case_closed_email, _send_mod_cancelled_email, _send_mod_followup_email. Build helper: _mod_email_build.
16.3 ODSP emails
_send_sa_mobility_email, _send_sa_mobility_completion_email, _send_odsp_submission_email. Build helper: _odsp_email_build.
16.4 Generic funder emails (WSIB / Insurance / MDC / Hardship)
Five client-facing methods + five authorizer-facing variants, all routed through the unified _send_funder_email(recipient, milestone, email_type, title, summary, attachment_ids, attachments_note):
| Method | Used at |
|---|---|
_send_funder_package_ready_* |
Quotation + application package prepared |
_send_funder_approval_* |
Funding approved (attaches the funder-specific approval letter via _get_funder_approval_attachments) |
_send_funder_delivered_* |
Product delivered |
_send_funder_case_closed_* (client only) |
Case closed |
_send_funder_denial_* |
Funding denied |
Per-funder trigger maps (class constants on sale.order) connect statuses to methods. The write override calls _fire_funder_emails(trigger_map, new_status) for each funder workflow.
| Trigger map | Status keys → methods |
|---|---|
_WSIB_EMAIL_TRIGGERS |
documents_ready → package ready (both), pre_approved → approval (both), delivered → delivery (both), case_closed → client only, denied → denial (both) |
_INSURANCE_EMAIL_TRIGGERS |
documents_ready → package ready (client only), approval_received / pre_auth_approved → approval (both), delivered → delivery (both), case_closed → client only, denied → denial (both) |
_MDC_EMAIL_TRIGGERS |
documents_ready → package ready (both), po_received → approval (both), delivered → delivery (both), case_closed → client only, denied → denial (both) |
_HARDSHIP_EMAIL_TRIGGERS |
application_package_ready → package ready (both), approval_received → approval (both), delivered → delivery (both), case_closed → client only, denied / eligibility_failed → denial (both) |
_FUNDER_LABELS maps the sale type to a human label ('wsib': 'WSIB', 'insurance': 'Insurance', 'muscular_dystrophy': 'Muscular Dystrophy Canada', 'hardship': 'Hardship Funding'). Used for subject lines and email body.
_build_funder_case_rows() builds the email's "Case Details" section dynamically per funder — WSIB adds claim#, adjudicator, approval date; Insurance adds company, policy#, claim#, pre-auth expiry; MDC adds Client ID, PO number/date; Hardship adds funder partner, approval date.
16.5 Recipients & CC
_get_email_recipients(include_client, include_authorizer, include_sales_rep) collects the to/cc list. The CC list always includes contacts in company.x_fc_office_notification_ids. Master toggle: fusion_claims.enable_email_notifications (_is_email_notifications_enabled).
MOD has its own recipient helper: _get_mod_email_recipients(include_client, include_authorizer, include_mod_contact, include_sales_rep) — adds include_mod_contact (uses x_fc_mod_contact_email). Returns a dict with to, cc, office_cc plus the partner records (authorizer, sales_rep, client). Authorizer becomes TO if no client email, else CC.
Per-MOD-method recipient rules (some include CC of the MOD case worker, some don't):
| Method | Client | Authorizer | MOD contact | Notes |
|---|---|---|---|---|
assessment_scheduled |
✓ | ✓ | — | |
assessment_completed |
✓ | ✓ | — | 2026-04 fix: authorizer was previously excluded |
quote_submitted |
✓ | ✓ | ✓ | |
funding_approved |
✓ | ✓ | — | with amounts section |
funding_denied |
✓ | ✓ | — | urgent style |
contract_received |
✓ | — | — | client only |
invoice_submitted |
— | — | ✓ | MOD contact only |
initial_payment |
✓ | — | — | client only |
project_complete |
✓ | ✓ | — | |
pod_submitted |
✓ | ✓ | ✓ | 2026-04 fix: client was previously excluded |
16.4.1 Authorizer email policy (2026-04 audit)
A consistent rule across the module: the authorizer (OT) is only CC'd / notified for states that require their action or are definitive outcomes. They are deliberately excluded from passive intermediate states to reduce noise.
| State | Authorizer in email? | Why |
|---|---|---|
assessment_scheduled |
✓ | They're conducting it |
assessment_completed |
✓ | Triggers next-step actions for them |
application_received |
✓ | Confirms handoff to office |
submitted / resubmitted |
✓ | Material progress |
accepted |
✗ | Passive intermediate state, no OT action needed |
approved / approved_deduction |
✓ | Definitive funding outcome |
denied / rejected / needs_correction |
✓ | OT may need to act |
on_hold |
✓ | OT should know case paused |
ready_for_delivery |
✗ | Operational scheduling, not delivery confirmation |
case_closed |
✓ | Definitive outcome — they get the "delivered" notification at this point |
cancelled / expired / withdrawn |
✓ | Closing the case |
Hold reminders + final warning have a slightly different rule (see §4.11) — monthly client reminder excludes authorizer, but the final warning includes them.
16.4.2 _adp_send_stage_email helper
The shared sender for ADP stage emails (_send_assessment_scheduled_email, _send_assessment_completed_email, _send_application_received_email, _send_accepted_email, _send_cancelled_email, _send_expired_email). Signature:
_adp_send_stage_email(title, subject, summary, note,
note_color='#4f8cff', email_type='info',
include_authorizer=True)
Defaults to client + authorizer + sales rep. include_authorizer=False is the one knob — used by _send_accepted_email and explained in the docstring with reference to the 2026-04 audit rule.
16.4.3 _send_withdrawal_email three-intent dispatch
Takes reason (free text) and intent arguments:
intent |
Title / subject | Note | Color |
|---|---|---|---|
'cancel' |
"Application Withdrawn & Cancelled" | "Permanently withdrawn and cancelled. SO and invoices cancelled." | #dc3545 urgent |
'resubmit' |
"Application Withdrawn for Correction" | "Will be resubmitted. Back to Ready for Submission." | #d69e2e attention |
| (None) | "Application Withdrawn" | "Withdrawn from ADP." | #d69e2e attention |
action_adp_withdraw calls with intent='cancel' by default; the status_change_reason_wizard exposes both intents.
16.5.1 Three footer voices
The module branches the footer line of every transactional email by funder type, via two thin wrappers around _email_build:
| Helper | Footer reads |
|---|---|
_email_build (default) |
"This is an automated notification from the ADP Claims Management System." |
_mod_email_build |
"This is an automated notification from the Accessibility Case Management System." |
_odsp_email_build |
"This is an automated notification from the ODSP Case Management System." |
Both wrappers do a literal string replace on the result of _email_build. If you change the master footer text, update the two replace calls or the branching will silently break.
16.6 mail.template records (data/mail_template_data.xml)
Three templates, all with auto_delete=True and a report attached:
email_template_adp_quotation— sale.order, landscape report.email_template_adp_sales_order— confirmation, landscape report.email_template_adp_invoice— account.move, landscape report.account.move.action_invoice_sentis overridden to pre-select this template for ADP invoices (don't remove this without preserving the routing).
17. SMS via Twilio
Settings: fusion_claims.twilio_enabled, fusion_claims.twilio_account_sid (manager-only), fusion_claims.twilio_auth_token (manager-only), fusion_claims.twilio_phone_number.
Helpers on sale.order:
_twilio_send_sms(to_number, message)— low-level POST tohttps://api.twilio.com/2010-04-01/Accounts/{sid}/Messages.jsonwith basic auth, 10-second timeout. Returns True on HTTP 200/201, False otherwise._send_mod_sms(trigger)— picks a message template from a hard-coded dict keyed by trigger string. Readspartner.mobilethen falls back topartner.phone. Silently bails if Twilio isn't enabled or the partner has no phone.
4 MOD SMS message templates (models/sale_order.py:9823-9840):
| Trigger | Sample message |
|---|---|
assessment_scheduled |
"Hi {name}, your accessibility assessment with Westin Healthcare has been scheduled. We will confirm the exact date and time shortly. For questions, call {company_phone}." |
funding_approved |
"Hi {name}, great news! Your March of Dimes funding has been approved. Our team will be in touch with next steps. Questions? Call {company_phone}." |
initial_payment_received |
"Hi {name}, we have received the initial payment for your project. Work is in progress. We will keep you updated. Call {company_phone} for info." |
project_complete |
"Hi {name}, your accessibility modification project is now complete! If you have any questions or concerns, call us at {company_phone}." |
⚠ Multi-tenant gotcha: the
assessment_scheduledtemplate hard-codes the string"Westin Healthcare"— it's NOT pulled fromself.company_id.name. If this module is deployed at a non-Westin customer, that message reads wrong. Fix or parameterize before going multi-tenant.
ODSP also has _send_sa_mobility_email (request_type='batteries'|'repair', device_description, attachment_ids, email_body_notes) and _send_odsp_submission_email. SA Mobility email defaults to samobility@ontario.ca (fusion_claims.sa_mobility_email).
18. Cron jobs (13 total)
All defined in data/ir_cron_data.xml and data/ir_actions_server_data.xml (currently empty placeholder).
| Cron | Time | What it does |
|---|---|---|
| Renew Delivery Reminders | daily | sale.order._cron_renew_delivery_reminders — finds approved/approved_deduction orders with overdue mail_activity_type_adp_delivery activities, reschedules to next posting Tuesday |
| Renew Billing Reminders | daily | account.move._cron_renew_billing_reminders — for waiting-status ADP invoices with overdue billing activities |
| Renew Correction Reminders | daily | account.move._cron_renew_correction_reminders — for need_correction-status ADP invoices |
| Auto-Close Billed Cases | daily | billed → case_closed 30 days after x_fc_billing_date. Posts chatter with days_since_billed |
| Auto-Close ODSP Paid Cases | daily | SA Mobility / ODSP Standard: 7 days after payment_received → case_closed. Ontario Works: 7 days after delivered (payment comes BEFORE delivery for OW) |
| First Application Reminder | 9 AM daily | _cron_send_application_reminders — assessment_end_date == today - X (default 4), status in waiting_for_application/assessment_completed, x_fc_application_reminder_sent=False. Sends to authorizer + sales rep + office |
| Second Application Reminder | 9 AM daily | _cron_send_application_reminders_2 — assessment_end_date == today - (X+Y), first reminder sent, second not yet. Mentions 90-day assessment validity |
| Acceptance Reminders | 9 AM daily | _cron_send_acceptance_reminders — see §18.1 below |
| MOD Follow-up Scheduler | 8 AM daily | Bi-weekly MOD activity creation (capped). See §5.3 |
| MOD Follow-up Escalation | 10 AM daily | Auto-email after activity 3 days overdue. See §5.3 |
| MOD Handoff Follow-up | 9 AM daily | Office call activity for client/authorizer MOD paths. See §5.3 |
| Expire Page 11 Signing Requests | 2 AM daily | Mark unsigned past-expiry requests expired |
| ADP Expire Approved Applications | 3 AM daily | approved → expired 12 months later |
| ADP Hold Expiry Reminders | 9:30 AM daily | Monthly client reminder + 30-day final warning |
18.1 _cron_send_acceptance_reminders mechanics
Sends reminders for orders still submitted next business day after submission. Three layers of anti-spam protection (2026-04 update):
| Layer | Setting | Effect |
|---|---|---|
| 14-day backlog guard | fusion_claims.acceptance_reminder_max_age_days (default 14) |
Only reminds cases submitted within the last 14 days — first cron run after deploy doesn't blast every old stuck submitted case |
| 2-day lower bound | hard-coded | cutoff_date = today - 2 days — skips weekend-only delays |
| Per-run cap | fusion_claims.acceptance_reminder_max_per_cron_run (default 10) |
Spreads large backlogs across multiple runs |
| One-shot flag | x_fc_acceptance_reminder_sent |
Each order gets at most ONE reminder per submission cycle. Reset to False on resubmission (see §4.9) |
FIRST/SECOND tier escalation:
days_since_submission ≤ 3→ "FIRST" reminder → office onlydays_since_submission > 3→ "SECOND" reminder → office + sales rep
Subject differs: "Pending Review: Acceptance Status - {SO}" (first) vs "Follow-up Review: Acceptance Status - {SO}" (second).
mail_activity_type_* records (data/mail_activity_type_data.xml): adp_delivery (sale.order), adp_billing (account.move), adp_correction (account.move, danger decoration), mod_followup (sale.order, 14-day delay).
18.2 MOD VOD request + handoff email mechanics
_send_mod_vod_request_email (fired when submitted_by first set to internal, also manual button):
- TO: authorizer; CC: sales rep
- Subject: "Verification of Disability needed - {client} - {SO}"
- Attaches the blank VOD form stored on
res.company.x_fc_mod_vod_form(via_mod_company_attachmenthelper) - Stamps
x_fc_mod_vod_requested_date = today
_send_mod_handoff_email (fired by action_mod_handoff_to_client, only when submitted_by ∈ ('client', 'authorizer')):
submitted_by |
TO | CC | Subject | Tone |
|---|---|---|---|---|
client |
client | authorizer + sales rep | "Your March of Dimes Application Package - {SO}" | Direct to client, numbered next-steps list, "Call us once you've submitted" |
authorizer |
authorizer | client + sales rep | "MOD Application Package for {client} - {SO}" | Professional, "Please confirm with us once submitted" |
Attaches: x_fc_mod_proposal_doc + x_fc_mod_drawing + blank MOD Application Form from res.company.x_fc_mod_application_form.
_mod_company_attachment(field_name, filename_field, default_name) — reusable helper that pulls a Binary from res.company and creates an ir.attachment for outbound emails. Returns [attachment_id] or [] if the company field is blank.
19. PDF reports (15+ templates)
All declared in report/report_actions.xml and bound to their models. Custom paperformat paperformat_a4_landscape (A4 Landscape, margins 20/20/7/7, header spacing 20, dpi 90).
| Report | Model | Type | XML file |
|---|---|---|---|
| Quotation / Order (Portrait - ADP) | sale.order | Portrait | sale_report_portrait.xml |
| Quotation / Order (Landscape - ADP) | sale.order | Landscape | sale_report_landscape.xml |
| Invoice (Portrait) | account.move | Portrait | invoice_report_portrait.xml |
| Invoice (Landscape - ADP) | account.move | Landscape (no menu binding) | invoice_report_landscape.xml |
| ADP Proof of Delivery | sale.order | report_proof_of_delivery.xml | |
| Proof of Delivery (Standard) | sale.order | report_proof_of_delivery_standard.xml | |
| Proof of Pickup | sale.order | report_proof_of_pickup.xml | |
| Approved Items | sale.order | report_approved_items.xml | |
| Grab Bar Waiver | sale.order | report_grab_bar_waiver.xml | |
| Accessibility Contract | sale.order | report_accessibility_contract.xml | |
| MOD Quotation | sale.order | report_mod_quotation.xml | |
| MOD Invoice | account.move | report_mod_invoice.xml |
19.1 Shared report-template snippets (report/report_templates.xml)
The following QWeb templates are t-call-able from any report:
| Template ID | Purpose |
|---|---|
report_header_fusion_claims |
Company logo + name + x_fc_store_address_1/2 + x_fc_company_tagline (3-col / 9-col Bootstrap row) |
report_address_boxes |
Bordered billing + delivery address columns (fallback to billing when no shipping address) |
report_serial_numbers |
Polymorphic — handles BOTH sale.order and account.move. Extracts x_fc_serial_number from order_line / invoice_line_ids and renders as a bulleted list inside a bordered box. |
report_payment_terms |
Outputs company.x_fc_payment_terms_html, preceded by "Payment Communication: {doc.name}" |
report_refund_policy_page |
Gated by company.x_fc_include_refund_page — separate page (page-break-before: always) with header + refund policy HTML |
report_footer_fusion_claims |
Phone & Fax + email + HST VAT + website (single centered row) |
report_styles_fusion_claims |
Inline <style> block; reads primary color var (set by caller) for .fc-table th background, totals row, etc. |
Color convention for all reports (2026-04): each report sets primary / secondary near the top from company.primary_color (default #0066a1) and company.secondary_color (default #90be6d), then references them inside the <style> block and inline style="" attributes via <t t-out="primary"/>.
19.2 Default Odoo report enhancements
report_templates.xml also inherits the default account.report_invoice_document and sale.report_saleorder_document to add a SKU column before Description AND strip [internal_ref] prefixes from line descriptions:
# clean_desc logic
if '] ' in line.name:
clean_desc = line.name.split('] ', 1)[1]
else:
clean_desc = line.name
This makes the default Odoo invoice/SO reports show [MXA-1618] GEOMATRIX SILVERBACK MAX BACKREST - ACTIVE as separate SKU: MXA-1618 + Description: GEOMATRIX SILVERBACK MAX BACKREST - ACTIVE columns instead of a single squashed line. Touches default reports too, not just the custom landscape/portrait ones — keep this in mind if the user reports default-report formatting differences.
Brand colours: every report opens with <t t-set="primary" t-value="_co.primary_color or '#0066a1'"/> and <t t-set="secondary" t-value="_co.secondary_color or '#90be6d'"/>. The fallback hexes preserve legacy rendering for databases that have never set company brand colours. Helper templates live in report/report_templates.xml (header + address boxes).
report/report_saleorder_adp.xml and report/report_invoice_adp.xml exist as empty stubs — do not delete (loaded by the manifest? — they're declared but currently empty).
20. Frontend assets
20.1 JS (__manifest__.py:166-178, registered into web.assets_backend)
| File | Purpose |
|---|---|
document_preview.js |
OWL component DocumentPreviewDialog — wraps Odoo's PDF.js viewer in a Dialog (xl / fullscreen toggle) |
preview_button_widget.js |
View widget preview_button — opens DocumentPreviewDialog from a button |
status_selection_filter.js |
Field filtered_status_selection — hides workflow-controlled statuses from the dropdown |
gallery_preview.js |
Patches Many2ManyBinaryField to use Odoo's native FileViewer in .fc-gallery-content sections (so clicking an attachment opens viewer instead of downloading) |
tax_totals_patch.js |
Defensively sets totals.subtotals = [] when undefined — fixes "Invalid loop expression" crash on tax-less invoices |
google_address_autocomplete.js |
1506-line address autocomplete widget |
calendar_store_hours.js |
Patches CalendarRenderer to clamp fusion.technician.task calendar to 8 AM–7 PM |
attachment_image_compress.js |
Patches FileUploader.onFileChange — compresses images > 500 KB to ≤1280px / JPEG 0.80 via Canvas BEFORE base64 conversion. Fixes iPhone Safari crash on 4+ photo upload |
debug_required_fields.js |
Patches Record._displayInvalidFieldNotification to log missing field labels + technical names to console |
20.2 SCSS
static/src/scss/fusion_claims.scss (771 lines): status button utility classes (fc-btn-status-good / -bad / -neutral), ADP/client portion column tints (fc-adp-portion / fc-client-portion), card hover effects, gallery section styling.
Dark mode caveat: the existing SCSS uses
html.dark/.o_darkselectors. The project-wide CLAUDE.md states this approach does not fire reliably in Odoo 19 — new SCSS in this module should branch on$o-webclient-color-schemeat compile time and be registered in bothweb.assets_backendandweb.assets_web_dark.
20.3 OWL XML templates
static/src/xml/document_preview.xml (204 lines) — markup for DocumentPreviewDialog, PreviewButtonWidget.
static/src/xml/fusion_task_map_view.xml (250 lines) — referenced from static/src/js/fusion_task_map_view.js (1197 lines, not bundled by THIS module — declared by fusion_tasks).
20.4 SCSS detail (771 lines)
The stylesheet covers more than just status pills — five distinct sections:
| Section | What it does |
|---|---|
| Status button utilities | .fc-btn-status-good / -bad / -neutral — Bootstrap-compatible tinted action buttons with hover states |
| Status-pill dark mode | Three different dark-mode strategies (html.dark, .o_dark, AND @media (prefers-color-scheme: dark)) — see gotcha #54 below |
| SOL list column widths | .o_field_one2many[name="order_line"] .o_list_table { table-layout: fixed } with explicit pixel widths for every column (serial, qty, UoM, price, tax, discount, subtotal, ADP/Client portion, sale_margin columns). Fragile — required because Odoo 19 ignores the XML width attribute on list fields. If you rename a SOL field, you must update the matching th[data-name="..."] selector here. |
| Document tiles | .fc-document-tiles / .fc-document-tile — 220px-wide cards with PDF/XML icons, hover upload overlay, required-field star indicator, green border in filled state, yellow border for required-but-empty |
| Approval photos gallery | .fc-gallery-section / .fc-gallery-content — 80×80px thumbnails on the Many2Many binary widget. Hover scales 1.05× with blue border. Delete button hidden until hover. |
| LTC kanban data-attr styling | Uses CSS :has(main[data-stage="info"]) to color-code outer kanban cards from inner <main> data attributes. Stages: info, warning, success, danger, secondary. Plus data-priority="1" (warm amber bottom accent) and data-emergency="1" (override red border). |
| Google Places fix | .pac-container { z-index: 100000 } — required so the autocomplete dropdown floats above Odoo modals |
| XML viewer styling | VS Code dark theme inspired colors (.xml-tag, .xml-attr, .xml-value) for XML preview |
.adp_file_preview |
Monospace code block for displaying raw ADP export .txt file content in the export record form |
20.5 Account move form inheritance (5 priorities)
views/account_move_views.xml inherits account.view_move_form five times with different priorities:
| Priority | id | What it adds |
|---|---|---|
| 49 | view_move_form_fusion_claims_billing_status |
ADP billing-status statusbar in header (waiting → submitted → payment_issued, clickable). Plus "Send to Case Worker" button for MOD invoices |
| 50 | view_move_form_fusion_claims_header |
Portion badge (floats top-right of title), invoice type field, authorizer-required field, authorizer field, client type field — all after journal_div, all with visibility expressions tied to move_type and x_fc_show_* flags |
| 51 | view_move_form_fusion_claims_verification_alert |
Yellow alert banner above sheet when x_fc_needs_device_verification is True |
| 52 | view_move_form_fusion_claims_button |
"Verify Device Approval" + "Export ADP" buttons in header. Export only visible for posted ADP-portion invoices. |
| 55 | view_move_form_fusion_claims_adp_tab |
ADP Case Details notebook tab (claim info + dates + export status) |
| 60 | view_move_form_fusion_claims_tab |
ADP Summary notebook tab (deduction alert + per-line table) |
21. Menus (app structure)
Root menu: ADP Claims (id menu_adp_claims_root).
ADP Claims
├── All Orders
│ ├── All Sales Orders
│ └── All Invoices
├── ADP
│ ├── All ADP Orders
│ ├── Billing
│ │ ├── ADP Invoices
│ │ ├── Client Invoices
│ │ ├── Ready for Billing
│ │ ├── Billed to ADP
│ │ └── Claim Submission Files
│ ├── Pre-Submission
│ │ ├── Quotation Stage / Assessment Scheduled / Waiting for Application
│ │ ├── Application Received / Ready for Submission
│ ├── Application Review
│ │ ├── Application Submitted / Accepted / Approved / Rejected / Needs Correction
│ ├── Fulfillment
│ │ ├── Ready for Delivery / Case Closed
│ └── Special Statuses
│ └── On Hold / Withdrawn / Denied / Cancelled / Expired
├── ODSP
│ ├── All ODSP Cases / ODSP Invoices
│ ├── ODSP Standard (8 stage sub-menus + Special Statuses)
│ ├── SA Mobility (8 stage sub-menus + Special Statuses)
│ └── Ontario Works (stage sub-menus)
├── MOD / WSIB / Insurance / MDC / Hardship (sub-menus per funder)
├── Dashboard → fusion.claims.dashboard (4 configurable panels)
├── Client Profiles → fusion.client.profile
├── Application Data → fusion.adp.application.data
├── Submission History → fusion.submission.history
├── ADP Device Codes → fusion.adp.device.code (Mobility Manual)
├── Page 11 Signing → fusion.page11.sign.request
├── Technician Tasks → fusion.technician.task (extends fusion_tasks menu)
└── Configuration
├── Settings → res.config.settings
├── Field Mapping → fusion_claims.field_mapping_config wizard
├── Device Code Import → fusion_claims.device.import.wizard
├── XML Import → fusion.xml.import.wizard
└── ADP Import → fusion_claims.adp.import.wizard
Dashboard model: fusion.claims.dashboard (TransientModel, models/dashboard.py) — 4 configurable panels each showing top 50 cases for any of 8 case types (ADP, ODSP, MOD, Hardship, ACSD, Muscular Dystrophy, Insurance, WSIB), rendered as HTML tables with direct links to the SO.
21.5 Case-close audit gate
fusion_claims.case_close_verification_wizard (wizard/case_close_verification_wizard.py) is mandatory for the case_closed transition. It verifies four things:
| Check | Source |
|---|---|
| Signed Pages 11 & 12 | x_fc_has_signed_pages_11_12 (bundled flag OR file OR signed remote request) |
| Final Application | x_fc_final_submitted_application set |
| Proof of Delivery | x_fc_proof_of_delivery set |
| Vendor Bills | At least one record in x_fc_vendor_bill_ids (Many2many to vendor account.move) |
The wizard offers two paths:
action_close_case— strict, raisesUserErrorif any item is missing.action_close_anyway— escape hatch, closes the case AND posts a yellow warning card to chatter listing what was missing (for audit).
x_fc_vendor_bill_ids is a manual Many2many — the user links the vendor bills that supplied the products to this case via the ADP Order Trail tab. There's no automatic linkage from purchase orders.
21.6 Contact types (res.partner)
res.partner.x_fc_contact_type has 22 values — used to drive conditional form rendering, smart buttons, and search filters:
adp_customer, adp_odsp_customer, odsp_customer, mod_customer,
private_customer, wsib_customer, acsd_customer, private_insurance,
adp_agent, odsp_agent, muscular_dystrophy,
occupational_therapist, physiotherapist, accessibility_specialist,
vendor, funding_agency, government_agency, company_contact,
long_term_care_home, retirement_home, odsp_office, other
Form view (views/res_partner_views.xml):
- Contact Type field above the address.
- Smart button "ADP Applications" — visible only when
x_fc_adp_application_count > 0. Opens applications filtered by either the authorizer's ADP registration number or by name match (_get_authorizer_application_domain). x_fc_authorizer_number("ADP Reg. Number" e.g.3000001234) — visible only foroccupational_therapist,physiotherapist,adp_agent. Used by the XML parser to auto-link the authorizer on imported applications.- ODSP notebook tab — visible only for
odsp_customer,adp_odsp_customer,odsp_agent,odsp_office. Capturesx_fc_odsp_member_id,x_fc_case_worker_id,x_fc_date_of_birth,x_fc_healthcard_number. - Search filters: "ODSP Customers", "ODSP Offices".
22. Security model
Two groups + one special override group (security/security.xml):
| Group | Implies | Purpose |
|---|---|---|
group_fusion_claims_user |
base.group_user + sales_team.group_sale_salesman |
Standard user |
group_fusion_claims_manager |
user + sales_team.group_sale_manager, default for admin/root |
Administrator |
group_document_lock_override |
(none — manually assigned) | Allows editing locked documents on legacy cases when fusion_claims.allow_document_lock_override = True. |
Module category module_category_fusion_claims + privilege res_groups_privilege_fusion_claims group everything under a FUSION CLAIMS section in user settings (Odoo 19 pattern).
Manager-only fields (groups='fusion_claims.group_fusion_claims_manager'):
fc_twilio_account_sidfc_twilio_auth_token- (Sensitive credentials)
Public model access: fusion.page11.sign.request grants base.group_public read-only access for the /page11/sign/<token> endpoint to resolve.
security/ir.model.access.csv has 65+ ACLs — every wizard model has user+manager rules, the device codes table is sales-team writable / base-user readable.
23. Configuration parameters
Defaults in data/ir_config_parameter_data.xml (all noupdate="1" — never overwritten on upgrade). Settings UI in models/res_config_settings.py + views/res_config_settings_views.xml.
Billing
fusion_claims.vendor_code— ADP vendor ID for exports.fusion_claims.adp_posting_base_date—2026-01-23.fusion_claims.adp_posting_frequency_days—14.fusion_claims.adp_approval_expiry_months—12.fusion_claims.adp_billing_reminder_user_id— single user (stored manually).fusion_claims.adp_correction_reminder_user_ids— comma-separated user IDs.
fusion_claims.enable_email_notifications—True.fusion_claims.application_reminder_days—4.fusion_claims.application_reminder_2_days—4.fusion_claims.adp_hold_reminder_interval_days—30.fusion_claims.adp_hold_final_warning_days_before_expiry—30.
AI
fusion_claims.ai_api_key— OpenAI key.fusion_claims.ai_model—gpt-4o-mini/gpt-4o/gpt-4.1-mini/gpt-4.1.fusion_claims.auto_parse_xml—True.
Twilio
fusion_claims.twilio_enabled—False.fusion_claims.twilio_account_sid/_auth_token— manager-only.fusion_claims.twilio_phone_number.
MOD
fusion_claims.mod_default_email—hvmp@marchofdimes.ca.fusion_claims.mod_vendor_code.fusion_claims.mod_followup_interval_days—14.fusion_claims.mod_followup_escalation_days—3.fusion_claims.mod_followup_max_per_month—2.fusion_claims.mod_followup_window_days—30.fusion_claims.mod_followup_max_per_cron_run—10.
ODSP
fusion_claims.sa_mobility_email—samobility@ontario.ca.fusion_claims.sa_mobility_phone—1-888-222-5099.fusion_claims.odsp_default_office_id.
Workflow
fusion_claims.allow_sale_type_override— bypasses sale-type lock.fusion_claims.allow_document_lock_override— required for the Document Lock Override group to take effect.fusion_claims.designated_vendor_signer— user who signs Page 12 on behalf of the company.fusion_claims.store_open_hour/store_close_hour— defaults9.0/18.0.
Portal branding
fusion_claims.portal_gradient_preset—green_teal/blue_purple/orange_red/dark_slate/custom.fusion_claims.portal_gradient_start/_mid/_end— custom hex colours.
Field mapping (~25 keys)
fusion_claims.field_sale_type,field_so_client_type,field_so_authorizer,field_so_claim_number,field_so_client_ref_1/2,field_so_delivery_date,field_so_adp_status,field_so_service_start/end,field_so_primary_serialfusion_claims.field_invoice_type,field_inv_client_type,field_inv_authorizer,field_inv_claim_number,field_inv_client_ref_1/2,field_inv_delivery_date,field_inv_service_start/end,field_inv_primary_serialfusion_claims.field_sol_serial,field_sol_placementfusion_claims.field_aml_serial,field_aml_placementfusion_claims.field_product_code,field_product_adp_price
24. Naming conventions
Follows the repo-wide rule: x_fc_* for new fields, x_studio_* for legacy Studio-created fields (kept for migration).
| Pattern | Used for |
|---|---|
x_fc_is_*_sale, x_fc_show_*_fields |
Computed booleans deriving from x_fc_sale_type |
x_fc_*_status |
State machine fields (per funder) |
x_fc_*_status_locked |
Computed locks derived from current status |
x_fc_previous_status_before_hold/withdrawal/cancel/delivery |
Audit-trail strings used to restore status after a special branch |
x_fc_*_date, x_fc_*_datetime |
Workflow event timestamps |
x_fc_trail_has_*, x_fc_*_trail_* |
Computed boolean audit trail flags (drawing received, POD sent, etc.) |
x_fc_source_sale_order_id, x_fc_adp_invoice_id, x_fc_client_invoice_id |
Cross-document linking |
x_fc_*_filename |
Required companion field for every Binary(attachment=True) field |
fusion_claims.field_* |
ICP keys for the legacy field mapping system |
Sale type selection values are kebab-snake (adp_odsp, march_of_dimes, muscular_dystrophy, direct_private) — match them exactly when domain-filtering or comparing strings.
All user-facing text is Canadian English (per repo CLAUDE.md). All monetary values are CAD with the $ sign + Monetary field + explicit currency_id.
25. Common gotchas
-
Never write
x_fc_adp_application_status(or any controlled status) directly. Use the correspondingaction_*method onsale.orderso emails, chatter, activity scheduling, and history records fire correctly. Thefiltered_status_selectionwidget already hides controlled statuses from the dropdown; only passwith_context(skip_status_validation=True)for legitimate framework calls (sync hooks, cron, test fixtures). -
Context skip flags are everywhere — using or omitting them wrongly causes infinite recursion or silent data loss. The vocabulary:
skip_sync— don't bounce values back through SO↔invoice sync.skip_status_emails— silence outgoing transition emails.skip_status_validation— bypass status-machine guards.skip_document_chatter— don't re-post the "document uploaded" chatter line.skip_page11_check— bypass the Page 11 signature requirement check.skip_payment_status_update— invoice write skips auto-billing-status flip.mark_ready_for_delivery,mark_odsp_ready_for_delivery— used on technician task create to advance the SO.
-
x_fc_sale_typeis locked once any funder workflow leaves quotation. Settingfusion_claims.allow_sale_type_override = Trueis the only escape hatch — don't add custom bypasses. -
Client invoice can be created BEFORE device verification. ADP invoice cannot. Modification-reason cases (
mod_non_adp,mod_adp) block client invoice creation until ADP approval — this is intentional and surfaces as adanger-styled sticky notification. -
Non-ADP funded product codes route to client 100% regardless of sale type. The whitelist (prefix match, case-insensitive):
NON-ADP,NON-FUNDED,UNFUNDED,NOT-FUNDED,ACS,ODS,OWP. If a product needs to be ADP-funded with a code that prefix-matches one of these, rename the code. -
ADP price source order matters. When invoking calculations, always go through
product.product.get_adp_price()/.get_adp_device_code(). Reading the field directly skips both the device-code database lookup AND the legacy field-mapping layer — silently produces wrong totals in legacy databases. -
Split invoice sync is two-way. Editing a field on one invoice (
x_fc_claim_number, dates, authorizer, client_type, primary_serial) can rewrite the SO and the sibling invoice. Setis_manually_modified = Trueon an invoice to opt it out of SO→invoice direction; serial numbers always sync via the line-by-line helper. -
Page 11 signing writes both an attachment and
x_fc_signed_pages_11_12. Regenerating a signed PDF doesn't delete the previous attachment — the old version stays in chatter. Use the wizard's cancel/resend flow rather than direct writes. -
Field-mapping getters are not optional. If you read
record.x_fc_sale_typedirectly in code you ship to production, you'll skip the mapping layer and break legacy databases. Userecord._get_sale_type(). -
Post-init hook idempotence —
_load_adp_device_codesruns on every upgrade. TheUPDATESQL in_link_products_to_device_codesis guarded byIS NULLchecks; preserve those when editing or you'll clobber manual product↔device-code remappings on every upgrade. -
MOD follow-up cap is shared state. Don't create
fusion.activityrecords of typemod_followupdirectly — call_mod_followup_cap_statefirst to check the rolling 30-day window. The cron defends against burst sends, but ad-hoc code can blow past the cap. -
fusion_pdf_previewmigration is incomplete in this module. Per repo CLAUDE.md, attachments opened by custom buttons should route throughatt.action_fusion_preview(title='...')instead ofir.actions.act_urlwithtarget=new/download=true. When you touch a callsite here, migrate it. Existing report-style downloads are already intercepted by the JS layer. -
tax_totals_patch.jsis load-bearing. Removing it crashes the JS view on any invoice without tax configuration. If you ship a fix upstream, audit the patch's removal carefully. -
action_invoice_sentoverride onaccount.moveauto-selects theemail_template_adp_invoicetemplate for ADP invoices. Removing this override without migrating the email-template selection breaks the ADP invoice send UX. -
Sale order
display_nameis overridden to"<name> - <partner.name>"(models/sale_order.py:22-28). Don't rely ondisplay_namebeing just the order number — usenameif that's what you need. -
Sale order display name search (
_rec_names_search) matches bothnameandpartner_id.name— useful forMany2oneautocomplete; gotcha if you're doing a Domain ondisplay_name. -
The auto-payment ODSP advancement runs in
account.move._compute_payment_state(override). A paid invoice on a linked ODSP order at statuspod_submitted/submitted_to_ow/payment_receivedauto-advances topayment_receivedand (Ontario Works only) schedules a delivery activity. -
fusion_claims.scssuseshtml.dark/.o_dark— per repo CLAUDE.md, this doesn't reliably fire in Odoo 19. New SCSS should branch on$o-webclient-color-schemeat compile time and be registered in bothweb.assets_backendANDweb.assets_web_dark. -
The legacy
x_fc_adp_statusfield (7-state, simpler) is kept for backward compatibility. Read fromx_fc_adp_application_status(22-state, comprehensive) instead. Don't write both. -
fusion_claims.configisTransientModel—models/fusion_central_config.py. Don't expectid-stable records. The model exists purely to host action methods callable from the settings UI. -
Pages 11 & 12 have THREE valid intake states — don't check
x_fc_signed_pages_11_12directly to gate workflow steps. Use the computedx_fc_has_signed_pages_11_12which returns True for: bundled flag (x_fc_pages_11_12_in_original=True), separate file uploaded, ORfusion.page11.sign.requestin statesigned. Same forx_fc_trail_has_signed_pages. Check the existing tests intests/test_signed_pages_gate.pybefore adding new gates. -
x_fc_case_lockedis global — when True, thewriteoverride blocks everyx_fc_*write except the lock itself + Odoo plumbing (message_main_attachment_id,message_follower_ids,activity_ids). This is different from per-document status locks. Context bypass:skip_all_validations=True. -
Document status locks vs case lock vs sale type lock — three separate mechanisms with different rules:
- Sale type lock — blocks
x_fc_sale_typewrites after any non-quotation funder status. Bypass:fusion_claims.allow_sale_type_override = TrueOR (fusion_claims.allow_document_lock_override = TrueAND user ingroup_document_lock_override) OR contextskip_sale_type_lock=True. - Document status locks — blocks specific document fields based on workflow status (see §4.5). Bypass: (
fusion_claims.allow_document_lock_override = TrueAND user ingroup_document_lock_override) OR contextskip_document_lock_validation=True. - Case lock — blocks all
x_fc_*writes whenx_fc_case_locked=True. Bypass: contextskip_all_validations=True.
- Sale type lock — blocks
-
Document audit trail preservation in chatter — when you write to a document binary field (
x_fc_original_applicationetc.) onsale.order, the old binary value is automatically copied into a chatter post before being overwritten. Usewith_context(skip_document_chatter=True)for legitimate re-saves (e.g., re-rendering Page 11 PDF with the same content). -
Replacement <5 year warning —
x_fc_under_5_yearsflag (computed fromx_fc_previous_funding_date) drives a chatter warning when creating a client invoice forreplace_status/replace_size/replace_wornreasons. Don't suppress the warning — it's a hint to verify the approval letter for ADP deductions. -
reason_for_applicationprevious-funding gate —ready_for_submission_wizardrequiresprevious_funding_datefor every reason exceptfirst_accessandmod_non_adp. Don't skip the check on programmatic transitions. -
account.payment.x_fc_card_last_fouris exactly 4 digits —account_payment_registervalidates.isdigit() and len()==4. Don't store dashed strings or full PANs. The "is card payment" detection preferspayment_method_line.x_fc_requires_card_digitsflag over keyword matching — set the flag on the journal form, don't rely on method-name heuristics. -
sale.advance.payment.invadds ADP options — the standard "Create Invoice" wizard now hasadp_client(25%) andadp_portion(75%/100%) selections that route to_create_adp_split_invoice.adp_clientraises if the client_type is not REG; both raise if the order is not an ADP sale. Use these instead of bypassing to direct_create_invoicesfor split scenarios. -
PDF magic-byte validation —
application_received_wizard._validate_pdf_byteschecks that the base64-decoded payload starts with%PDF-. Test fixtures useb'%PDF-1.4\n%fake pdf for tests'. If you add a new PDF upload field elsewhere, copy this defence — the filename.pdfconstraint alone is not enough. -
web_savedebug override —sale.order.web_savehas a_logger.warningshim active in production to diagnose "Missing required fields" errors on legacy orders. Don't remove it without coordinating with whoever's tracking the issue; if you do, also remove the matching_logger.errorin the except branch. -
ready_submissiongate uses rawx_fc_signed_pages_11_12, not the computedx_fc_has_signed_pages_11_12(models/sale_order.py:7706-7707). This means a direct write (without going throughapplication_received_wizard+ready_for_submission_wizard) will reject bundled-mode and remote-signed orders even though they're functionally complete. The wizards bypass this by ensuringx_fc_signed_pages_11_12is populated for separate/remote modes — but bundled-mode orders rely on the wizard's own gate (which uses the computed). If you callwrite()directly withx_fc_adp_application_status='ready_submission', you must also passwith_context(skip_status_validation=True)for bundled-mode orders to succeed. -
MOD partial PCA invoices reuse
x_fc_adp_invoice_portion='adp'for the MOD invoice — disambiguate viax_fc_invoice_type('march_of_dimes'vs'adp'). Searching only byx_fc_adp_invoice_portion='adp'will catch MOD invoices too. -
ADP invoice customer is NOT the SO partner —
_create_adp_split_invoiceswitchespartner_idto the ADP partner (searched by name) and moves the original client topartner_shipping_id. Aggregations grouped bypartner_idonaccount.movewill lump all ADP invoices together under the ADP partner — group byx_fc_source_sale_order_id.partner_idfor client-level aggregates. -
Price-mismatch auto-correction is silent for products with
x_fc_adp_device_code_id— those products are managed via the Many2one and the write override onfusion.adp.device.codepushes price changes to them. For products WITHOUT the Many2one,_create_adp_split_invoicewill silently overwrite theirx_fc_adp_priceif the device-code DB says otherwise. The chatter warning is the only signal. -
Stage 2 device approval syncs to existing invoices —
_sync_approval_to_invoicesrewrites lineprice_uniton existing client and ADP invoices to match the new approval state. If you've manually adjusted invoice line prices, they'll be overwritten. Useis_manually_modified=Trueon the invoice if you need to lock it. -
mark_as_approvedmode attachment garbage collection — many2many_binary to a TransientModel is GC'd when the wizard closes.device_approval_wizard.action_confirm_approvalcopies the bytes into new persistentir.attachmentrecords. Any wizard you write that takes many2many_binary on a transient model must do the same — DO NOT pass the original attachment IDs into a non-transient many2many; copy first. -
MOD
quote_submitted/funding_approvedauto-stamp dates — the write override auto-fillsx_fc_case_submitted/x_fc_case_approvedwith today if blank. Don't rely on those dates as "user-entered" — they may be auto-stamped by status transitions. -
Soft
fusion_faxesdependency —odsp_submit_to_odsp_wizard.action_send_faxandaction_send_fax_and_emailwill fail at click-time iffusion_faxesis not installed. Add it to the manifest's depends if you're moving to a database without it, or guard the actions explicitly in views. -
action_adp_reopen_expiredis a back-compat shim — it just callsaction_adp_duplicate_for_reassessment. Per 2026-04 policy, expired cases CANNOT be self-renewed (authorizer must reassess after 12 months). The shim is kept so old Studio views with the button still resolve the method name. -
The 3-month assessment expiry blocks resume-from-hold —
x_fc_assessment_expired = Truewhen(today - x_fc_assessment_end_date).days > 90. Resuming fromon_holdto any other status will hit this gate. The OT must redo the assessment, which (in practice) means usingassessment_completed_wizardin override mode to record the new assessment date. -
ADP export verification BLOCKS, not warns — when stored line portions don't match the recomputed values from the device-code DB (tolerance
$0.01 × qty),adp_export_wizard.action_exportraises aUserErrorlisting every mismatch. The export does NOT complete with warnings — it hard-stops. Fix the invoice calculations (account.move.line.action_recalculate_portions) or update the device codes DB before re-trying. -
ADP export expands
qtyto per-unit rows — a line withqty=3exports 3 rows, each withqty=1andunit_portion = stored_portion / qty. If you build downstream tooling against the CSV, expect per-unit rows, not per-line rows. -
ADP rejects renamed export files — the filename is
{vendor_code}_{YYYY-MM-DD}.txt. If you re-export on the same day (same filename), the wizard warns but proceeds; you must manually rename outside Odoo before submitting to ADP, otherwise ADP rejects the submission. -
_sync_approval_to_invoicesre-posts posted invoices — Stage 2 approval flips runinvoice.button_draft() → write → action_post()on posted invoices. This can re-fire post-actions, invalidate sequence assumptions, and is destructive if the invoice has been exported. Setis_manually_modified=Trueon the invoice to opt it out. -
Three footer voices — emails carry one of three footer lines depending on which wrapper builds them:
_email_build(ADP),_mod_email_build(Accessibility Case),_odsp_email_build(ODSP). Use the right wrapper or your funder context disappears from the email. -
MOD activity dedup — every MOD cron checks for an existing open
mail.activityof typemail_activity_type_mod_followupon the order before creating a new one. Don't bypass this by creating activities directly — useactivity_scheduleor the cron's pattern, otherwise you'll get duplicate activities every day. -
automated=Trueonmail.activity.create— suppresses Odoo's default "activity assigned" email notification. Three MOD crons use this so the assignee sees the activity on their dashboard but doesn't get spammed in their inbox. If you create activities programmatically and the user complains about inbox noise, add this. -
_schedule_or_renew_adp_activityupdates instead of duplicating — the pattern for ADP billing/correction reminders. Finds an existing activity of the same type for the same user and updates its date/summary/note. Use this helper, notactivity_scheduledirectly, for repeating reminders. -
Twilio SMS hard-codes "Westin Healthcare" — the
assessment_scheduledmessage template (models/sale_order.py:9825) has the company name baked into a string literal, not pulled fromself.company_id.name. Multi-tenant deployments will read wrong here — fix before going past a single Westin install. -
Per-MOD-method recipient rules vary —
contract_receivedis client-only (no authorizer);invoice_submittedis MOD contact only;pod_submittedincludes client + authorizer + MOD contact. Don't assume "all MOD emails CC the authorizer" — see the table in §16.5. -
MOD funding denied wizard requires a category — 5 enum values (
income_too_high,residency,project_scope,missing_docs,funding_depleted, plusother). The category label is prepended to the free-text reason as'[{label}] {text}'and stored inx_fc_mod_funding_denial_reason. Don't write the field directly — go through the wizard so the format is consistent. -
MOD resubmit can clear documents —
mod_resubmit_wizard.clear_old_documentswipesx_fc_mod_drawingandx_fc_mod_proposal_docafter posting copies to chatter. If you've built tooling that assumes those fields stay populated through the lifecycle, document the resubmission cycle explicitly. -
odsp_pre_approved_wizardroutes by division — same wizard, two different target fields depending onx_fc_odsp_division:sa_mobility→x_fc_sa_approval_form;standard→x_fc_odsp_approval_document. Ontario Works (x_fc_odsp_division == 'ontario_works') is NOT handled here — OW has its own discretionary flow. -
SCSS uses THREE dark-mode strategies simultaneously:
html.dark,.o_dark, AND@media (prefers-color-scheme: dark). Per repo CLAUDE.md, NONE of these fire reliably in Odoo 19 — new SCSS should branch on$o-webclient-color-schemeat compile time and be registered in bothweb.assets_backendANDweb.assets_web_dark. The existing patterns work in some browsers and fail in others; treat them as legacy. -
SOL list column widths are pinned in SCSS —
.o_field_one2many[name="order_line"]forcestable-layout: fixedwith explicit pixel widths for everyth[data-name="..."]selector. Odoo 19 ignores the XMLwidthattribute on list fields, so this is the only way to control column widths. If you rename a SOL field, update the matching SCSS selector — otherwise the column collapses to whatever the browser auto-sizes. -
Default Odoo SO + invoice reports are also modified —
report_templates.xmlinheritsaccount.report_invoice_documentandsale.report_saleorder_documentto add an SKU column AND strip[internal_ref]prefixes. This affects reports you may not realize this module touches. If users report formatting differences on the standard invoice/SO PDFs, this is the cause. -
Acceptance reminder cron has 14-day backlog guard —
fusion_claims.acceptance_reminder_max_age_days(default 14) excludes cases submitted > 14 days ago. First cron run after a long deploy outage won't email every old stucksubmittedcase. Per-run cap (acceptance_reminder_max_per_cron_run, default 10) further spreads large backlogs. Both can be tuned via settings if you need to flush a backlog quickly. -
Acceptance reminder skips weekends with 2-day lower bound — hard-coded
cutoff_date = today - 2 days. Don't expect reminders the day after submission; the cron treats "1 business day" as "2 calendar days" so Friday submissions don't trigger Saturday emails. -
Acceptance reminder is two-tier —
≤3 dayssince submission → office only;>3 days→ office + sales rep. The one-shotx_fc_acceptance_reminder_sentflag means each cycle gets AT MOST ONE email; the flag resets on resubmission (see §4.9). -
_send_approval_emailattaches an "Approved Items" PDF — generated on-the-fly via_generate_approved_items_pdf(callsaction_report_approved_itemsQWeb report). If you suppress this for performance reasons (e.g., for very large orders), the email body still has_build_approved_items_htmlinline asextra_html— so the user sees the table in the email even if the PDF generation fails. -
_send_application_reminder_2_emailmentions 90-day assessment validity — content callout to the OT that the assessment may need to be redone if too much time passes. Tied to thex_fc_assessment_expiredcomputed field (>90 days → True). -
Report color convention — every report sets
primaryandsecondaryfromcompany.primary_color(default#0066a1) andcompany.secondary_color(default#90be6d) at the top, then references them via<t t-out="primary"/>. If you ship a new report, follow this pattern so it picks up the company's brand colors. -
Ontario Works auto-close uses
delivered, notpayment_received— because for OW, payment comes BEFORE delivery (the wholesaler pays the vendor, then the vendor delivers to the client). So_cron_auto_close_odsp_paid_casescloses 7 days afterdeliveredfor OW; 7 days afterpayment_receivedfor SA Mobility and ODSP Standard. Important to remember when building OW reports/KPIs. -
Default-report SKU split logic —
[internal_ref]prefix is split on the first occurrence of'] '(close-bracket-space) — not just']'. A product name like[MXA-1618]GEOMATRIX...(no space) would NOT be split correctly. If users complain about descriptions still containing brackets, check the product name format. -
_send_application_reminder_emailsets a one-shot flag —x_fc_application_reminder_sent=Trueafter the first send. The flag does NOT reset on resubmission (unlike the acceptance reminder flag). If you need a fresh reminder cycle after a workflow reset, clear the flag manually viawith_context(skip_all_validations=True).write({...}). -
The 2026-04 authorizer email policy excludes the OT from
acceptedandready_for_deliveryemails — by design (see §16.4.1). Don't add the authorizer back into those without checking the audit policy first; the rule is that the OT is only in the loop for actionable states. -
_send_ready_for_delivery_emailadds delivery address frompartner_shipping_id.contact_address— comma-joined, newlines replaced. Falls back topartner_idif shipping address is blank. Technicians are CC'd; the email also includesScheduleddatetime from the wizard. -
_send_withdrawal_emailhas three different intents —cancel(red, permanent),resubmit(amber, back to ready_submission), and (None) (amber, plain).action_adp_withdrawdefaults tocancelbutstatus_change_reason_wizardexposes both. -
_sync_fields_to_invoicesis defensive about Studio fields — checksif 'x_fc_*' in invoice._fieldsbefore writing each key. Don't remove these checks even though they look redundant; legacy databases without certain studio fields rely on them. -
_sync_serial_numbers_to_invoicesdoes NOT use header fallback — each SO line syncs its OWN serial to its linked invoice lines viasale_line_idslink. If the SOL is blank, the AML is left alone. Don't assume there's a header-to-line fallback; serials are per-line throughout. -
adp_export_record._get_posting_period_for_fileuses strict<=comparison — a file dated EXACTLY on the posting date falls into THAT posting; a file dated one day later falls into the NEXT posting. Important foradp_import_wizardhistorical imports. -
migrate_from_documentsis run from a settings button, not auto. The settings UI inres_config_settingshas anaction_migrate_adp_export_filesbutton. The migration is one-time by design (and idempotent if rerun) — but the source documents get archived (active=False), so a rerun won't find them. -
SA Mobility gov form has a
Text1field that collides withText 1(with space) — they're different fields on different pages. Don't normalize whitespace when building the mapping or you'll write to the wrong field. -
odsp_sa_mobility_wizard._get_template_path()uses rawos.pathinstead of Odoo'stools.misc.file_path. If the module is ever deployed as a zip (rare in Odoo deployments but possible), this will fail. Migrate tofile_path('fusion_claims/static/src/pdf/sa_mobility_form_template.pdf')if you ship this for multi-tenant. -
PDF template field positions for ODSP signing live in
fusion.pdf.template(category=odsp) — managed via a drag-and-drop editor that lives infusion_authorizer_portal. The OWL editor reads field positions per-page;_apply_pod_signature_to_approval_formconsumes them. If the gov SA form layout changes, edit the template via the visual editor, not by changing Python coordinates. -
SA Mobility wizard limits rows: 6 parts, 5 labour, 4 fees. The gov PDF only has that many slots. If the SO has more lines, the rest are silently dropped from the form fill (but still appear in the invoice). The wizard truncates via slicing in
default_get. -
migrate_from_documentsarchives source documents on success (active=False). This means a second run won't re-migrate. If you need to re-migrate intentionally, unarchive the documents first via the Documents app. -
OW Discretionary uses PyPDF2, NOT pdfrw — because the gov form is AES-encrypted. The wizard handles decryption (empty password) and AcroForm preservation. Both
pdfrwANDPyPDF2are de-facto required Python deps for ODSP workflows: pdfrw for SA Mobility, PyPDF2 for OW Discretionary. Onlypdfrwandpdf2image/PILare declared in the manifest; PyPDF2 is implicit (used as a transitive dep via PdfFileReader from odoo.tools.pdf in some places). -
OW Discretionary form field names DON'T match their labels —
txt_email[0]is the Phone field,txt_emp_phone[0]is the Email field,txt_CITY[1]is the Member ID. Don't normalize these names when writing the mapping or you'll write to the wrong field. The mapping in_build_field_mappingis the canonical reference — keep it in sync with the gov form, not with semantic intuition. -
OW Discretionary checkbox annotations are mutated directly —
annot[NameObject('/V')] = NameObject('/1')for checked,NameObject('/Off')for unchecked. PyPDF2 has no high-level checkbox API; if you add new checkboxes to the form, update the mapping AND the annotation-walking loop. -
send_to_mod_wizard subjects use HVMP reference when available —
f'{prefix} - {ref} - {client_name}'for cases withx_fc_case_reference, otherwisef'{prefix} - {client_name} - {order.name}'. Don't write code that hard-codes the order name into the subject — preserve the reference-first pattern. -
_get_field_attmutates filenames in-place — finds the existing Odoo auto-generatedir.attachmentfor a binary field and renames it to the "pro" format (Drawing - John_Doe - S29958.pdf). Don't bypass this and create a duplicate attachment; the helper preserves Odoo's binary-field linkage. -
send_to_mod_wizardrequires DIFFERENT files per mode — drawing mode needsdrawing_file; completion mode needs BOTHcompletion_photos_fileANDpod_file. The wizard raisesUserErrorif missing — don't try to bypass by passingmod_wizard_mode='completion'programmatically without uploading both.
26. Local development
Per the per-user memory and current local setup (overrides the repo-root README defaults):
# Container & DB
docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --stop-after-init
# Tests (filter by the module tag)
docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --test-enable --test-tags fusion_claims --stop-after-init
# URL
http://localhost:8069
26.1 Existing tests (tests/, tagged '-at_install', 'post_install', 'fusion_claims')
tests/test_signed_pages_gate.py(108 lines, 11 test methods) — coversx_fc_has_signed_pages_11_12andx_fc_trail_has_signed_pagesacross all 3 intake modes (bundled flag, separate file, remote sent vs. remote signed). Also coversready_for_submission_wizardaccepting bundled-only flag (no separate file) andcase_close_verification_wizard.has_signed_pagesaccepting the bundled flag.tests/test_application_received_wizard.py(191 lines, 17 test methods) — full coverage ofapplication_received_wizard: bundled/separate/remote intake modes, PDF magic-byte validation (rejects fake.pdffiles), status gate (only fromassessment_completed/waiting_for_application), default_get mode selection logic, mode-specific chatter messages.
When adding new workflow features, follow the existing pattern — tag tests with '-at_install', 'post_install', 'fusion_claims' so they're easy to filter and don't block module install if the test suite breaks.
26.2 Scripts (scripts/ — not part of the addon, run via odoo-shell)
scripts/import_adp_mobility_manual.py(6.4 KB) — standalone CLI for importing the ADP Mobility Manual JSON into the running database. Use when the packaged file gets out of date and you need to push an updated manual without bumping module version.scripts/import_demo_pool.py(9.9 KB) — loads a curated demo product pool (for staging / training environments).scripts/cleanup_demo_pool.py(7 KB) — reverses the demo pool import (removes demo products without touching production data).
26.3 Orphan / unused files (be aware before refactoring)
These files exist in the source tree but are not loaded by the manifest — don't assume they're active code:
| File | Notes |
|---|---|
views/client_chat_views.xml (87 lines) |
Legacy chat UI — replaced by native ai.agent chat |
static/src/css/fusion_task_map_view.scss |
Belongs to fusion_tasks — bundled by THAT module, not by this one |
static/src/js/fusion_task_map_view.js (1197 lines) |
Same — bundled by fusion_tasks |
static/src/xml/fusion_task_map_view.xml (250 lines) |
Same — bundled by fusion_tasks |
report/report_saleorder_adp.xml, report/report_invoice_adp.xml |
Empty stubs |
data/stock_location_data.xml |
Empty <data/> placeholder kept for upgrade-safety |
wizard/temp_serial_migration_views.xml |
No corresponding Python — historical migration aid |
If you're removing dead code, these are safe candidates; if you're adding features, double-check the manifest before editing one of these expecting it to be live.
27. Key files (bird's-eye)
models/sale_order.py 10,631 lines — workflow spine (all 8 funder lifecycles)
models/account_move.py 1,219 lines — split invoicing + billing lifecycle
models/account_move_line.py 247 lines — invoice line portion compute (mirror of SOL)
models/sale_order_line.py 418 lines — primary calculation logic
models/fusion_adp_device_code.py 428 lines — Mobility Manual + JSON/CSV import
models/dashboard.py 162 lines — 4-panel home page
models/adp_application_data.py 670 lines — every XML field (sections 1, 2, 2a-2d)
models/xml_parser.py 772 lines — XML→JSON→model with round-trip fidelity
models/client_profile.py 298 lines — client master with AI summary/risk flags
models/client_chat.py 350 lines — legacy OpenAI chat (gpt-4o-mini default)
models/ai_agent_ext.py 164 lines — native ai.agent tools
models/page11_sign_request.py 389 lines — public-token signing for Page 11
models/technician_task.py 674 lines — sale_order_id/purchase_order_id + rental inspection
models/submission_history.py 237 lines — per-submission audit trail
models/res_config_settings.py 679 lines — settings UI + ICP field mapping
models/adp_posting_schedule.py 262 lines — mixin for posting-cycle date maths
models/adp_export_record.py 449 lines — exported file archive
models/fusion_central_config.py 126 lines — Detect Existing Fields action
models/product_template.py 151 lines — x_fc_is_adp_product, device-code link
models/product_product.py 189 lines — get_adp_price / get_adp_device_code / is_non_adp_funded
models/res_company.py 124 lines — company-level MOD form storage + office CC list
models/res_partner.py 129 lines — contact_type, ODSP/authorizer fields
wizard/status_change_reason_wizard.py 543 lines — guards reject/deny/withdraw/hold/cancel
wizard/device_approval_wizard.py 724 lines — Stage 2 verification
wizard/submission_verification_wizard.py 397 lines — Stage 1 verification
wizard/adp_export_wizard.py 452 lines — TXT generator + verifier
wizard/odsp_sa_mobility_wizard.py 560 lines — fills gov SA form via pdfrw
wizard/send_to_mod_wizard.py 335 lines — drawing/quotation/POD MOD composer
wizard/odsp_discretionary_wizard.py 395 lines — OW Discretionary Benefits
wizard/field_mapping_config_wizard.py 460 lines — legacy Studio field mapper UI
wizard/mod_pca_received_wizard.py 304 lines — PCA upload + lifecycle advancement
wizard/application_received_wizard.py 304 lines — capture application from authorizer
wizard/application_received_wizard.py 304 lines — 3-mode intake (bundled/separate/remote) + PDF magic-byte check
wizard/ready_for_submission_wizard.py 196 lines — required fields gate + previous-funding rule + reason_for_application
wizard/ready_for_delivery_wizard.py 219 lines — assigns techs + auto-creates fusion.technician.task with pod_required=True
wizard/case_close_verification_wizard.py 211 lines — 4-check audit gate + strict/anyway paths
wizard/account_payment_register.py 79 lines — card last-4 digits validation
wizard/sale_advance_payment_inv.py 125 lines — adds adp_client (25%) + adp_portion (75%/100%) options
wizard/send_page11_wizard.py 92 lines — public-token signing composer
wizard/xml_import_wizard.py 107 lines — bulk XML import via fusion.xml.parser
wizard/odsp_discretionary_wizard.py 395 lines — OW Discretionary Benefits form
wizard/mod_pca_received_wizard.py 304 lines — PCA upload + lifecycle advancement
data/device_codes/adp_mobility_manual.json 528 KB — packaged Mobility Manual snapshot (~hundreds of records)
data/ai_agent_data.xml — native AI agent + 3 tools
data/ir_cron_data.xml — 13 cron jobs
data/mail_template_data.xml — 3 ADP mail.templates
data/mail_activity_type_data.xml — 4 activity types
data/ir_config_parameter_data.xml — initial settings (noupdate=1)
data/product_labor_data.xml — pre-seeded LABOR product
data/pdf_template_data.xml — empty placeholder (SA templates retired, see views/pdf_template_inherit_views.xml)
data/stock_location_data.xml — empty placeholder
security/security.xml — groups + privilege + module category
security/ir.model.access.csv — 66 ACL rows (one for every wizard/model)
static/src/scss/fusion_claims.scss — 771 lines, status pill colours
static/src/js/document_preview.js — OWL PDF viewer dialog
static/src/js/preview_button_widget.js — view widget that opens the dialog
static/src/js/status_selection_filter.js — filtered_status_selection field (hides controlled statuses)
static/src/js/gallery_preview.js — patches Many2ManyBinaryField for FileViewer
static/src/js/tax_totals_patch.js — defensive subtotals=[] on tax-less invoices
static/src/js/google_address_autocomplete.js 1506 lines — address autocomplete widget
static/src/js/calendar_store_hours.js — clamps technician task calendar to 8 AM-7 PM
static/src/js/attachment_image_compress.js 192 lines — mobile crash fix via Canvas image compression
static/src/js/debug_required_fields.js — logs missing required field labels
static/src/xml/document_preview.xml — OWL template for preview dialog
static/src/pdf/sa_mobility_form_template.pdf 482 KB — government SA Mobility form (filled by pdfrw)
static/src/pdf/sa_mobility_page2_sample.pdf 241 KB — reference sample
static/src/pdf/discretionary_benefits_form_template.pdf 1.1 MB — government OW Discretionary Benefits form (filled by pdfrw)
report/sale_report_portrait.xml — ADP-aware quotation/order portrait
report/sale_report_landscape.xml — ADP-aware quotation/order landscape
report/invoice_report_portrait.xml — invoice portrait
report/invoice_report_landscape.xml — invoice landscape (no menu binding)
report/report_proof_of_delivery.xml — ADP POD
report/report_mod_quotation.xml — MOD quote
report/report_mod_invoice.xml — MOD invoice
report/report_templates.xml — shared header + address-boxes templates
views/sale_order_views.xml 2768 lines — main SO form (tabs, buttons, conditional sections per funder)
views/adp_claims_views.xml 2013 lines — root menu tree (5 levels deep) + per-stage actions
views/client_profile_views.xml 723 lines — profile form + AI chat + claim history
views/res_config_settings_views.xml 563 lines — settings form
views/account_move_views.xml 454 lines — invoice form + sync buttons + ADP billing fields
views/dashboard_views.xml — 9 gradient cards + 4 configurable panels (2x2 grid)
views/page11_sign_request_views.xml — list + form with Resend/Cancel/New Signature header buttons
views/product_template_adp_views.xml — ADP Product toggle + ADP Information section
views/res_partner_views.xml — Contact Type (22 values) + ODSP tab + ADP Reg. Number for OTs
views/technician_task_views.xml — adds SO/PO stat buttons + Rental Inspection tab (pickup only)
views/account_journal_views.xml — "Req. Card #" column on inbound/outbound payment methods
tests/test_application_received_wizard.py 191 lines — 17 tests for 3-mode intake
tests/test_signed_pages_gate.py 108 lines — 11 tests for x_fc_has_signed_pages_11_12
scripts/import_adp_mobility_manual.py — standalone Mobility Manual importer (run via odoo-shell)
scripts/import_demo_pool.py — demo product pool loader
scripts/cleanup_demo_pool.py — reverses the demo pool import
27.3 Small status-transition wizards (one-shot wizards on a single button)
These wizards are minimal — most are ~50-100 lines — but they encode crucial business rules:
| Wizard | Purpose | Allowed FROM | Notes |
|---|---|---|---|
mod_awaiting_funding_wizard |
Records the application submission date | (any non-funded state, called via action_mod_awaiting_funding) |
Just captures the date + optional notes, writes status awaiting_funding, stamps x_fc_mod_application_submitted_date. |
mod_funding_denied_wizard |
Captures denial category + free-text reason | awaiting_funding, quote_submitted, handoff_to_client |
5 categories: income_too_high, residency, project_scope, missing_docs, funding_depleted, other. Stored in x_fc_mod_funding_denial_reason as '[{Category Label}] {details}' — used by the denial email body. |
mod_resubmit_wizard |
Revise + resubmit a denied MOD case | funding_denied ONLY |
Required revision_notes. Optional clear_old_documents toggle: when True, clears x_fc_mod_drawing and x_fc_mod_proposal_doc (after posting the originals to chatter for preservation). Always advances status → processing_drawings. |
mod_submission_confirmed_wizard |
Office confirms client/authorizer actually submitted to MOD | handoff_to_client ONLY |
5 confirmation sources: phone_call, email, client_portal, authorizer, other. Advances status → awaiting_funding, stamps x_fc_mod_application_submitted_date. Pre-fills the submitted_by_label from x_fc_mod_submitted_by. |
odsp_pre_approved_wizard |
Upload the ODSP approval PDF | (called via action_odsp_pre_approved) |
Single binary field. Routes the file by division: sa_mobility → x_fc_sa_approval_form; standard → x_fc_odsp_approval_document. Always creates a persistent ir.attachment and advances ODSP status via _odsp_advance_status('pre_approved', ...). |
account_payment_register (override) |
Capture last-4 card digits at payment time | always — extends the standard wizard | See §7.4. Returns the same action; just adds fields + validation. |
sale_advance_payment_inv (override) |
Adds adp_client / adp_portion options to Create Invoice |
always — extends the standard wizard | See §7.3. |
schedule_assessment_wizard |
Create calendar.event + advance to assessment_scheduled |
quotation ONLY |
Pre-fills location from partner_id address; optional 1-day email alarm. |
assessment_completed_wizard |
Advance to assessment_completed, optional scheduling override |
quotation (override mode) or assessment_scheduled |
See §4.3.1. override_reason required from quotation; validates completion_date >= assessment_start_date. |
application_received_wizard |
3-mode intake of Pages 11/12 | assessment_completed or waiting_for_application |
See §4.4. |
ready_for_submission_wizard |
Required-field gate before submission | application_received ONLY |
See §4.9 required-field matrix. |
submission_verification_wizard |
Stage 1 verification — what device types are being submitted | (before submitted) |
See §4.3. Dual-purpose with submit_application=True context (writes status + final PDF + XML). |
device_approval_wizard |
Stage 2 verification — what ADP approved | (after ADP responds) | See §4.3 + §7.10. mark_as_approved=True context advances status. |
ready_to_bill_wizard |
POD upload + delivery date capture | approved or approved_deduction ONLY |
Blocks if x_fc_device_verification_complete=False. POD must be .pdf. Creates a separate ir.attachment for the POD and posts it in chatter. |
ready_for_delivery_wizard |
Assigns technicians + creates delivery task | (any non-terminal post-approval state) | See §13. Auto-creates fusion.technician.task with pod_required=True, lead + additional technicians on same task. |
case_close_verification_wizard |
4-check audit gate before case closure | (called via "Close Case" button) | See §21.5. Strict (action_close_case) or anyway (action_close_anyway) paths. |
send_page11_wizard |
Compose a Page 11 remote-signing request | (any active state) | See §14.1. Default 7-day expiry. |
mod_submission_path_wizard |
Choose who submits to MOD | (any pre-handoff MOD state) | 3 paths: internal/client/authorizer. Auto-fires _send_mod_vod_request_email when internal selected for the first time. |
mod_funding_approved_wizard |
Records case worker + HVMP ref on approval | (any pre-approval MOD state) | Bare write; stamps x_fc_case_approved. |
mod_pca_received_wizard |
PCA upload + full/partial split invoice creation | (called via action_mod_contract_received) |
See §7.12. Creates 1 invoice (full) or 2 invoices (partial). |
send_to_mod_wizard |
Multi-mode MOD email composer | (multiple — context-driven mod_wizard_mode) |
See §5.4. Modes: drawing/quotation/completion. |
odsp_sa_mobility_wizard |
Generate filled SA Mobility government PDF | (called from SA Mobility flow) | See §14.3. Uses pdfrw to fill static/src/pdf/sa_mobility_form_template.pdf. Has 3 transient line types: part lines, labour lines, fee lines (each with own tax calc). |
odsp_discretionary_wizard |
Generate filled OW Discretionary Benefits PDF | (called from Ontario Works flow) | Uses pdfrw to fill static/src/pdf/discretionary_benefits_form_template.pdf. |
odsp_submit_to_odsp_wizard |
Send quotation + authorizer letter to ODSP office | (any pre-submitted_to_odsp ODSP state) |
See §14.4. Email / Fax / Email+Fax paths. |
odsp_ready_delivery_wizard |
Configure signature page + open delivery task | (after ODSP pre-approval) | See §14.5. PDF preview with colored markers per field. |
status_change_reason_wizard |
Capture reason for status changes that need one | (controlled statuses — rejected/denied/withdrawn/on_hold/cancelled/needs_correction) | Single wizard with branching by new_status. Different selection fields for rejection vs denial reasons. |
field_mapping_config_wizard |
Visual editor for the field-mapping ICP layer | (manual access from Settings) | Uses DEFAULT_FIELD_MAPPINGS constant — 25+ mapping definitions. Each line tracks field_name, default_fc_field, config_param_key, plus a computed field_exists flag that checks ir.model.fields to validate the user's pick. Five action buttons: action_save, action_save_and_close, action_reset_defaults (back to x_fc_*), action_auto_detect (uses AUTO_DETECT_PATTERNS keyword dict to match non-x_fc_* Studio fields), action_close. |
27.4 AUTO_DETECT_PATTERNS (field-mapping auto-detect)
wizard/field_mapping_config_wizard.py:194-227 defines the keyword dictionary used by action_auto_detect. Each mapping is config_param_key → list of substring patterns. The matcher iterates manual custom fields (not x_fc_* prefixed), lowercase-checks each pattern against the field name, and picks the first match.
Synonyms per field (sample):
'fusion_claims.field_sale_type': ['sale_type', 'saletype', 'type_of_sale', 'order_type']
'fusion_claims.field_so_claim_number': ['claim_number', 'claimnumber', 'claim_no', 'adp_claim', 'claim_num']
'fusion_claims.field_so_client_ref_1': ['client_ref_1', 'clientref1', 'reference_1', 'client_reference_1', 'ref1', 'ref_1']
'fusion_claims.field_so_delivery_date': ['delivery_date', 'deliverydate', 'adp_delivery', 'deliver_date', 'date_delivery']
'fusion_claims.field_sol_serial': ['serial_number', 'serial', 'sn', 'serialno']
'fusion_claims.field_product_code': ['adp_code', 'adp_device', 'device_code', 'adp_sku', 'product_code']
Add your own pattern to this dict if you find a Studio installation that uses an unrecognized naming convention. Keep the substring-match semantics in mind — 'sn' matches 'business_no' (oops), so order matters; more-specific patterns should come first if there's a conflict risk.
27.5 Import wizards (one-shot imports outside the normal workflow)
| Wizard | Purpose | Notes |
|---|---|---|
xml_import_wizard |
Bulk-import ADP XML files → fusion.client.profile + fusion.adp.application.data |
Per-file try/except, builds a result_message with created/updated/errors. Manager-only. |
device_import_wizard |
Import device codes from JSON or CSV → fusion.adp.device.code |
Auto-detects file type from extension. Handles UTF-8 BOM (utf-8-sig), Latin-1 fallback. CSV column detection tries multiple price column names ("ADP Price", "Approved Price" with various spacings) + partial match on any column containing "price". |
adp_import_wizard |
Import historical ADP export .txt files (or ZIP) → fusion_claims.adp.export.record |
Idempotent (skips by filename). ZIP support scans subfolders, ignores __macosx, dedupes by base name. Auto-parses vendor code + file date from filename, computes posting period. |
field_mapping_config_wizard |
Visual editor for the field-mapping ICP layer (~25 mappings) | Uses a DEFAULT_FIELD_MAPPINGS constant — list of {model_name, label, default_fc_field, config_param_key} dicts. Per-row, lets the user pick which actual field on the model the mapping should point at. |
28. Quick reference: things that surprise newcomers
- Sale type values use kebab-snake:
adp_odsp,march_of_dimes,muscular_dystrophy,direct_private,march_of_dimes(NOTmod, NOTadp/odsp). x_fc_is_adp_saleis True for bothadpANDadp_odsp(the_compute_is_adp_salechecks for'adp' in sale_type).x_fc_sale_type_lockedis True for ANY funder workflow's non-quotation status — not just ADP's.- The
display_nameof a SO is overridden to"<name> - <partner.name>"(models/sale_order.py:22-28). is_manually_modifiedopts an invoice out of SO→invoice sync but NOT out of invoice→SO sync. Set it before editing if you want decoupled fields.- The
pdfrwPython package is optional — if missing, only the SA Mobility and OW Discretionary wizards lose their PDF filling capability. The module still installs. fusion.page11.sign.requestaccess is granted tobase.group_public— needed so the public sign URL/page11/sign/<token>can resolve the request server-side. Don't tighten this without changing the public controller.- Many2many serial sync (
_sync_line_fields_to_sale_order) syncs across sibling invoices linked to the samesale.order.line— editing a serial on the ADP invoice rewrites it on the client invoice and the SO line. Avoid blocking this withskip_syncunless you really want decoupled serials. - The kanban kanban ordering is enforced via
_read_groupoverride that sorts by_STATUS_ORDER— don't sort byx_fc_adp_application_statusdirectly in custom views; sort byx_fc_status_sequence(computed, indexed). - MOD has its own invoice template (
fusion_claims.action_report_mod_invoice) —account.move.action_mod_send_invoicerenders + attaches it before emailing the case worker; don't use the ADP landscape template for MOD invoices.
29. Cross-module integration
This module is the lower-level engine. Two sibling modules layer on top of it and one sits below as infrastructure. Understanding the integration matrix is critical when changing field names, signatures of helpers, or model schemas.
29.1 fusion_tasks (infrastructure layer — declared in our manifest)
| Lives in fusion_tasks | Used by fusion_claims as |
|---|---|
fusion.email.builder.mixin |
Mixed into sale.order, account.move, fusion.technician.task, fusion.page11.sign.request via _inherit. Provides _email_build(title, summary, sections, note, ...) — every _send_*_email method in the module routes through it. |
fusion.technician.task (base, 3,208 lines) |
Inherited (_inherit, NOT _inherits) by models/technician_task.py to add sale_order_id + purchase_order_id + rental inspection fields. fusion_tasks owns the calendar, GPS tracking, scheduling, push notifications, cross-instance sync. |
fusion.task.sync.config (cross-instance sync) |
The Westin↔Mobility task-sync feature. Each instance has a fusion_claims.sync_instance_id ICP (e.g. westin). Tasks marked with x_fc_sync_source are read-only "shadow" tasks from the other instance. Match between instances by x_fc_tech_sync_id on res.users. JSON-RPC API key auth. Terminal statuses (completed, cancelled) stop sync. |
fusion.technician.location |
Per-technician GPS history for the admin map. |
fusion.push.subscription |
Web Push subscriptions for technician phones. |
x_fc_is_field_staff on res.users |
Filter for the technician_id domain on tasks. |
x_fc_tech_sync_id on res.users |
Cross-instance technician matching. |
group_field_technician |
Auto-populated with all internal users on install (_fusion_tasks_post_init). |
| ICP defaults set by post-init | fusion_claims.google_maps_api_key, store_open_hour (9.0), store_close_hour (18.0), push_enabled, push_advance_minutes (30), sync_instance_id, technician_start_address |
res.company settings |
x_fc_mod_followup_assignee_mode, x_fc_mod_followup_office_contact_id — owned by fusion_tasks but referenced by fusion_claims MOD follow-up crons (see memory [[project_fusion_tasks_sync]]). |
_email_is_enabled etc. |
Helpers on sale.order come from the mixin chain. |
MEMORY: [[project_fusion_tasks_sync]] (Westin/Mobility task sync) |
When "tasks not syncing", check res.users.x_fc_tech_sync_id first — silent failure when missing or duplicated. |
fusion.technician.task lifecycle hooks that fusion_claims overrides:
_create_vals_fill— pre-fill address from SO/PO during create._on_create_post_actions— chatter notice + optional SOready_deliveryadvancement._check_completion_requirements— rental pickup must have inspection done._on_complete_extra— ODSPready_delivery→delivered; rental security-deposit refund / damage activity._on_cancel_extra— delivery cancellation reverts SO tox_fc_status_before_delivery.
The whole technician task → sale order coupling lives in fusion_claims/models/technician_task.py:674 — and the calendar / map / scheduling logic stays in the base fusion.technician.task model in fusion_tasks.
29.2 fusion_authorizer_portal (portal layer — undeclared but co-installed)
fusion_authorizer_portal manifest declares fusion_claims + fusion_tasks + fusion_loaners_management as hard deps. fusion_claims uses APIs that only exist when fusion_authorizer_portal is installed — see the dependency note at the top of §2.
| Provided by fusion_authorizer_portal | Used by fusion_claims |
|---|---|
PDFTemplateFiller class (utils/pdf_filler.py) |
sale.order._apply_pod_signature_to_approval_form imports it. Same pattern as Odoo Enterprise Sign module — overlays text/checkmarks/signatures via reportlab Canvas + mergePage(). |
fusion.pdf.template model + fusion.pdf.template.field + fusion.pdf.template.preview |
Drag-and-drop visual editor for placing fields on PDF preview images. Categories: adp, mod, odsp, hardship, other. fusion_claims searches for (category='odsp', state='active') for SA Mobility / OW signature overlays. The Page 11 wizard searches for name ilike 'adp_page_11' or 'page 11'. |
fusion.assessment model (1,636 lines) |
The OT assessment form, captures all wheelchair specs + signatures. page11_sign_request._generate_signed_pdf reads fusion.assessment records linked by sale_order_id to populate signing context (client name, address, type, signatures). |
fusion.accessibility.assessment model (966 lines) |
For MOD/accessibility cases — separate from ADP OT assessment. |
fusion.adp.document (183 lines) |
Document tracking on portal cases. |
fusion.authorizer.comment (85 lines) |
Authorizer/sales-rep comments shown on case pages. |
/page11/sign/<token> public route |
Handles the URL that fusion.page11.sign.request._send_signing_email sends to clients/agents. 3 routes: form display, submit (writes signature_data + agent fields + state='signed', triggers _generate_signed_pdf + _update_sale_order), download. Public auth — depends on base.group_public ACL on fusion.page11.sign.request. |
/my/authorizer/* routes |
OT portal — list cases, view case, upload documents, add comments, download signed pages. |
/my/sales/* routes |
Sales rep portal — list cases, view case, add comments. |
/my/technician/* routes |
Technician portal — task list, action buttons (start/complete/cancel), tomorrow's schedule, location logging, push subscribe, delivery POD signing. |
/my/assessment/* routes |
Assessment creation/edit, signature capture, "express" mode (quick wheelchair specs without full form), complete + auto-create-SO. |
/my/pod/* routes |
Client-facing POD signing on delivery. |
/my/accessibility/* routes |
MOD-style accessibility forms (stairlift straight/curved, VPL, ramp, bathroom). |
/my/timezone/detect |
JS-driven timezone detection for portal users. |
Why this matters for fusion_claims development:
- Renaming a field on
sale.orderlikely affects portal templates (portal_templates.xml,portal_assessment_express.xml,portal_accessibility_*.xml) that reference it via QWeb. - Adding a new
x_fc_adp_application_statusvalue may need a portal-side handler inportal_main.pyto render the new state. - The
fusion.pdf.templateschema (page-positioned fields) is the ground truth for ODSP signature placement — DON'T hard-code coordinates in fusion_claims when you could create a template field instead. - The
_reactivate_viewspost-init hook on fusion_authorizer_portal exists specifically because the inheritance from this module's views is fragile — if you rename a field referenced by an xpath in fusion_authorizer_portal, that view goes dead and stays dead.
29.3 Other co-installed Nexa modules
| Module | What it provides | Used by fusion_claims |
|---|---|---|
fusion_ringcentral |
RingCentral softphone, click-to-dial widget, fax composer | Click-to-dial works on any phone field — no direct API calls from this module |
fusion_faxes |
fusion_faxes.send.fax.wizard + partner.x_ff_fax_number |
Hard-soft-dep: odsp_submit_to_odsp_wizard calls the fax wizard for ODSP submissions |
fusion_loaners_management |
Loaner equipment lending | fusion_authorizer_portal depends on this; fusion_claims doesn't touch it directly |
fusion_pdf_preview |
PDF preview client action + report intercept | Project CLAUDE.md says prefer this over act_url+target=new for attachments. fusion_claims still has legacy attachment buttons using the old pattern — see gotcha #12 |
30. Per-funder workflow state machines
For every funder, the same four artifacts are documented: status enum, action methods or write-driven transitions, emails fired per transition, wizards that gate the transitions. The ADP workflow (§4) is the most elaborate; the table below cross-references the others.
30.1 ADP (x_fc_adp_application_status, 22 states)
See §4 for the full spec. Status order via _STATUS_ORDER dict. Kanban groups via _expand_adp_application_statuses keep core states always visible. Status sequence (kanban left-to-right):
quotation → assessment_scheduled → assessment_completed → waiting_for_application →
application_received → ready_submission → submitted → accepted → resubmitted →
needs_correction → approved/approved_deduction → ready_delivery → ready_bill → billed →
case_closed. Special: on_hold, rejected, denied, withdrawn, cancelled, expired.
| Transition | Wizard | Side effects | |
|---|---|---|---|
quotation → assessment_scheduled |
schedule_assessment_wizard |
_send_assessment_scheduled_email |
Creates calendar.event with 1-day alarm |
assessment_scheduled → assessment_completed |
assessment_completed_wizard |
_send_assessment_completed_email |
Auto-transitions to waiting_for_application after write |
quotation → assessment_completed (override) |
assessment_completed_wizard with override_reason required |
(same) | Yellow chatter card with override reason |
waiting_for_application → application_received |
application_received_wizard (3 intake modes: bundled/separate/remote) |
_send_application_received_email |
PDF magic-byte check, sets x_fc_pages_11_12_in_original for bundled mode |
application_received → ready_submission |
ready_for_submission_wizard |
none | Validates client refs, claim auth date, reason, prev funding (if applicable), original PDF, signed pages |
ready_submission → submitted/resubmitted |
submission_verification_wizard w/ submit_application=True context |
_send_submission_email (with PDF+XML attachments) |
Creates fusion.submission.history record. Resets x_fc_acceptance_reminder_sent |
submitted → accepted |
device_approval_wizard from "Accepted" button |
_send_accepted_email (no authorizer) |
Updates submission history result='accepted' |
submitted → rejected |
status_change_reason_wizard |
_send_rejection_email |
Updates submission history result='rejected' w/ reason |
accepted → approved/approved_deduction |
device_approval_wizard w/ mark_as_approved=True context |
_send_approval_email (with Approved Items PDF) |
Stage 2 verification, claim number + approval letter saved, deductions set; if any deduction → approved_deduction |
* → needs_correction |
status_change_reason_wizard |
_send_correction_needed_email |
Clears x_fc_final_submitted_application, xml_file, claim_submission_date (preserved in chatter first) |
approved → ready_delivery |
(auto via task creation or ready_for_delivery_wizard) |
_send_ready_for_delivery_email (no authorizer) |
Creates fusion.technician.task w/ pod_required=True. Adds delivery technicians, scheduled datetime |
ready_delivery → ready_bill |
ready_to_bill_wizard |
none | Requires POD + delivery date + verification complete |
ready_bill → billed |
"Mark as Billed" button | _send_billed_summary_email (no client) |
Stamps x_fc_billing_date |
billed → case_closed |
case_close_verification_wizard (4-check gate) OR _cron_auto_close_billed_cases (30d after billed) |
_send_case_closed_email |
None |
approved/approved_deduction → on_hold |
status_change_reason_wizard (hold reason) |
_send_on_hold_email |
Records x_fc_previous_status_before_hold. Hold ONLY allowed from approved per 2026-04 |
on_hold → previous status |
"Resume" button | none | 3-month assessment expiry block — rejects if x_fc_assessment_end_date > 90 days ago |
* → withdrawn (cancel) |
status_change_reason_wizard w/ intent='cancel' |
_send_withdrawal_email(intent='cancel') (red urgent) |
Permanent |
* → withdrawn (resubmit) |
status_change_reason_wizard w/ intent='resubmit' |
_send_withdrawal_email(intent='resubmit') (amber attention) |
Back to ready_submission |
* → denied |
status_change_reason_wizard |
_send_denial_email |
Records x_fc_denial_reason + free text |
denied → ready_submission |
"Resubmit from Denied" button | none | Fresh attempt |
* → cancelled |
status_change_reason_wizard |
_send_cancelled_email |
Records x_fc_previous_status_before_cancel. x_fc_cancel_reported_to_adp controls reopen behaviour |
cancelled → previous status |
"Reopen Cancelled" button | none | Only if x_fc_cancel_reported_to_adp=False. Otherwise redirects to action_adp_duplicate_for_reassessment |
expired/cancelled → new SO |
action_adp_duplicate_for_reassessment |
none | Creates new SO with x_fc_previous_sale_order_id back-pointer |
| (12 months after approved) | _cron_adp_expire_approved |
_send_expired_email |
Auto-expired |
30.2 MOD — March of Dimes (x_fc_mod_status, 16 states, default need_to_schedule)
need_to_schedule → assessment_scheduled → assessment_completed → processing_drawings →
quote_submitted ─┬─ awaiting_funding → funding_approved → contract_received → in_production →
│ └─ funding_denied → resubmit/cancel
└─ handoff_to_client → awaiting_funding (after mod_submission_confirmed_wizard)
project_complete → pod_submitted → case_closed. Special: on_hold, cancelled.
| Transition | Wizard / Method | Notes | |
|---|---|---|---|
* → assessment_scheduled |
action_mod_schedule_assessment (bare write) |
_send_mod_assessment_scheduled_email + Twilio SMS assessment_scheduled |
Stamps x_fc_mod_assessment_scheduled_date |
→ assessment_completed |
action_mod_complete_assessment (bare write) |
_send_mod_assessment_completed_email |
Stamps x_fc_mod_assessment_completed_date |
→ processing_drawings → quote_submitted |
action_mod_processing_drawing opens send_to_mod_wizard (mod_wizard_mode='drawing') |
_send_mod_quote_submitted_email (Client + Authorizer + MOD contact) |
Stamps x_fc_mod_drawing_submitted_date. Status moves through processing_drawings only briefly. |
→ awaiting_funding (internal path) |
mod_awaiting_funding_wizard |
none | Captures x_fc_mod_application_submitted_date |
quote_submitted → handoff_to_client |
action_mod_handoff_to_client (requires submitted_by ∈ {client, authorizer}, proposal_doc, drawing) |
_send_mod_handoff_email (client+authorizer variants differ) |
Triggers _cron_mod_handoff_followup activities |
handoff_to_client → awaiting_funding |
mod_submission_confirmed_wizard (5 confirmation sources) |
none | Confirms client/authorizer submitted |
| Set submission path | mod_submission_path_wizard (internal/client/authorizer) |
First-time internal → _send_mod_vod_request_email (with blank VOD form attached) |
Sets x_fc_mod_submitted_by. VOD email goes to authorizer with the form template from company.x_fc_mod_vod_form |
→ funding_approved |
mod_funding_approved_wizard |
_send_mod_funding_approved_email + Twilio SMS funding_approved |
Records case worker + HVMP ref. Auto-stamps x_fc_case_approved=today if blank. |
→ funding_denied |
mod_funding_denied_wizard (5 denial categories) |
_send_mod_funding_denied_email |
Writes x_fc_mod_funding_denial_reason as '[{category}] {details}' |
funding_denied → processing_drawings |
mod_resubmit_wizard (revision_notes req'd, optional clear_old_documents) |
none | Resubmission path |
funding_denied → cancelled |
action_mod_cancel_from_denied |
none | Permanent |
cancelled → need_to_schedule |
action_mod_reopen_cancelled |
none | Clears x_fc_mod_funding_denial_reason |
→ contract_received |
mod_pca_received_wizard (full or partial approval — creates 1 or 2 invoices) |
_send_mod_contract_received_email (Client only) |
Uploads PCA, splits invoice if partial. Stamps x_fc_mod_pca_received_date, x_fc_mod_payment_commitment. |
→ in_production |
action_mod_in_production (bare write) |
Twilio SMS initial_payment_received (no email) |
Stamps x_fc_mod_production_started_date |
→ project_complete |
action_mod_project_complete (bare write) |
_send_mod_project_complete_email + Twilio SMS project_complete |
Stamps x_fc_mod_project_completed_date |
→ pod_submitted |
action_mod_pod_submitted opens send_to_mod_wizard (mod_wizard_mode='completion') |
_send_mod_pod_submitted_email (Client + Authorizer + MOD contact, 2026-04 fix) |
Requires completion photos + POD |
→ case_closed |
action_mod_close_case (bare write) |
_send_mod_case_closed_email |
Stamps x_fc_mod_case_closed_date. Email mentions 1-year warranty. |
→ on_hold |
action_mod_on_hold |
none | Records x_fc_mod_previous_status_before_hold (2026-04 fix; was hardcoded to in_production) |
on_hold → previous status |
action_mod_resume |
Green chatter card | Restores from x_fc_mod_previous_status_before_hold |
Built-in action_cancel |
(override) | _send_mod_cancelled_email |
When SO is cancelled, MOD status also goes to cancelled |
| Standalone "Send to MOD" | send_to_mod_wizard (no mode-driven status change, just sends) |
_send_mod_*_email based on send_mode |
Used outside the regular flow for ad-hoc sends |
| Daily cron | _cron_mod_schedule_followups |
none (creates activity) | Creates bi-weekly follow-up activities for quote_submitted/awaiting_funding. Cap 2/30 days. |
| Daily cron | _cron_mod_escalate_followups |
_send_mod_followup_email (when not capped) |
Auto-emails 3 days after activity overdue. Unlinks activity on send. |
| Daily cron | _cron_mod_handoff_followup |
none (creates activity) | Creates office-call activities for handoff_to_client orders |
30.3 ODSP — SA Mobility division (x_fc_sa_status, 12 states)
quotation → form_ready → submitted_to_sa → pre_approved → ready_delivery → delivered →
pod_submitted → payment_received → case_closed. Special: on_hold, cancelled, denied.
| Transition | Wizard / Method | Notes |
|---|---|---|
quotation → form_ready |
odsp_sa_mobility_wizard (fills gov form 13007E with pdfrw) |
Generates filled SA Mobility PDF. 3 transient line types: parts, labour, fees, each with tax calc. |
form_ready → submitted_to_sa |
odsp_submit_to_odsp_wizard (Email / Fax / Email+Fax modes) — calls _sa_mobility_submit_documents |
Email goes to fusion_claims.sa_mobility_email (default samobility@ontario.ca). Attaches signed SA form + POD + invoice PDFs. |
submitted_to_sa → pre_approved |
odsp_pre_approved_wizard (upload approval PDF — routes to x_fc_sa_approval_form) |
Stores PDF as attachment |
pre_approved → ready_delivery |
odsp_ready_delivery_wizard (configure signature page + PDF preview overlay) |
Opens delivery task form. Auto-advances via task hook mark_odsp_ready_for_delivery=True |
ready_delivery → delivered |
Auto when delivery task completes (technician_task hook _on_complete_extra) |
OR _apply_pod_signature_to_approval_form fires when POD signature is set on SO |
delivered → pod_submitted |
"POD Submitted" button (probably action_odsp_pod_submitted) |
none |
pod_submitted → payment_received |
Auto when paid invoice's payment_state ∈ {paid, in_payment} (account_move hook _auto_advance_odsp_on_payment) |
Triggers _send_odsp_submission_email |
payment_received → case_closed |
_cron_auto_close_odsp_paid_cases (7 days later) |
OR action_odsp_close_case |
→ on_hold |
action_odsp_on_hold |
Records x_fc_odsp_previous_status_before_hold |
on_hold → previous status |
action_odsp_resume |
2026-04 fix; was hardcoded to quotation |
→ denied |
action_odsp_denied |
none |
denied → submitted_to_sa |
(manual selection) | "Move back to Submitted to SA Mobility to reapply" per the help text |
30.4 ODSP — Standard division (x_fc_odsp_std_status, 11 states)
quotation → submitted_to_odsp → pre_approved → ready_delivery → delivered →
pod_submitted → payment_received → case_closed. Special: on_hold, cancelled, denied.
Same pattern as SA Mobility but no form_ready step (no gov form filling — uses regular quotation PDF). Submission flow uses _odsp_std_submit_documents (attaches approval doc + POD + invoice; auto-creates invoice if missing).
| Transition | Wizard / Method | Notes |
|---|---|---|
quotation → submitted_to_odsp |
odsp_submit_to_odsp_wizard |
Email/Fax/Email+Fax. Quote PDF + authorizer letter. |
submitted_to_odsp → pre_approved |
odsp_pre_approved_wizard (routes to x_fc_odsp_approval_document) |
|
pre_approved → ready_delivery |
odsp_ready_delivery_wizard |
Same as SA |
payment_received → case_closed |
_cron_auto_close_odsp_paid_cases 7 days after |
30.5 ODSP — Ontario Works division (x_fc_ow_status, 10 states — PAYMENT BEFORE DELIVERY)
quotation → documents_ready → submitted_to_ow → payment_received → ready_delivery →
delivered → case_closed. Special: on_hold, cancelled, denied.
Critical: OW pays UPFRONT. payment_received comes BEFORE ready_delivery. The auto-close cron treats delivered as the closer trigger for OW (not payment_received).
| Transition | Wizard / Method | Notes |
|---|---|---|
→ documents_ready |
odsp_discretionary_wizard (fills gov Discretionary Benefits form via PyPDF2) |
Encrypted PDF — pdfrw can't decrypt; PyPDF2 handles it. Field names misleading (see §14.3.1). |
documents_ready → submitted_to_ow |
odsp_submit_to_odsp_wizard Email/Fax |
|
submitted_to_ow → payment_received |
action_odsp_payment_received → _ow_payment_create_invoice |
Auto-confirms SO if needed, calls _create_invoices(), advances status. Chatter mentions invoice ref. |
payment_received → ready_delivery |
(manual or via delivery task) | None |
ready_delivery → delivered |
technician_task _on_complete_extra hook |
OW Ontario Works funder emails per _HARDSHIP/MDC patterns N/A here — OW has bespoke flow |
delivered → case_closed |
_cron_auto_close_odsp_paid_cases 7 days after delivered |
NOT payment_received for OW |
30.6 WSIB (x_fc_wsib_status, 15 states)
quotation → assessment_scheduled → assessment_completed → documents_ready →
submitted_to_wsib → pre_approved → ready_delivery → delivered → pod_submitted →
invoice_submitted → payment_received → case_closed. Special: on_hold, denied, cancelled.
Email triggers via _WSIB_EMAIL_TRIGGERS dict (models/sale_order.py:10572):
| Status target | Emails fired (via _fire_funder_emails) |
|---|---|
documents_ready |
_send_funder_package_ready_client_email, _send_funder_package_ready_authorizer_email |
pre_approved |
_send_funder_approval_client_email, _send_funder_approval_authorizer_email (attaches x_fc_wsib_approval_letter) |
delivered |
_send_funder_delivered_client_email, _send_funder_delivered_authorizer_email |
case_closed |
_send_funder_case_closed_client_email (client only) |
denied |
_send_funder_denial_client_email, _send_funder_denial_authorizer_email |
Identifier fields: x_fc_wsib_claim_number, x_fc_wsib_adjudicator_name, x_fc_wsib_form_7_date, x_fc_wsib_approval_date, x_fc_wsib_approval_letter.
30.7 Insurance (x_fc_insurance_status, 16 states — BRANCHED FLOW)
Two divergent paths from documents_ready: client-submit vs direct-bill.
quotation → home_assessment_scheduled → home_assessment_completed → documents_ready
├─ Client-submit: → submitted_by_client → approval_received → payment_received_from_client → ...
└─ Direct-bill: → pre_auth_submitted → pre_auth_approved → ...
Both converge: → product_ordered → delivered → pod_to_client / pod_invoice_submitted →
payment_received → case_closed. Special: on_hold, denied, cancelled.
x_fc_insurance_submission_mode controls which branch (probably). Identifier fields: x_fc_insurance_company_id, x_fc_insurance_policy_number, x_fc_insurance_claim_number, x_fc_insurance_pre_auth_amount, x_fc_insurance_pre_auth_expiry, x_fc_insurance_home_assessment_required, x_fc_insurance_letter_source, x_fc_insurance_approval_letter.
Email triggers via _INSURANCE_EMAIL_TRIGGERS:
documents_ready→ client onlyapproval_received/pre_auth_approved→ client + authorizerdelivered→ client + authorizercase_closed→ client onlydenied→ client + authorizer
30.8 MDC — Muscular Dystrophy Canada (x_fc_mdc_status, 16 states)
quotation → awaiting_ot_letter → documents_ready → submitted_to_mdc → po_received →
product_ordered → delivered → pod_invoice_submitted → awaiting_payment → payment_received →
case_closed. Special: not_enrolled, on_hold, denied, withdrawn, cancelled.
Distinctive: awaiting_ot_letter step, dedicated po_received (instead of approved), and not_enrolled terminal-like state (client needs to be a MDC member). Help text: "From Client Not Enrolled, move back to Quotation once enrollment is verified."
Identifier fields: x_fc_mdc_client_id_number, x_fc_mdc_enrollment_verified, x_fc_mdc_enrollment_verified_date, x_fc_mdc_submitted_by, x_fc_mdc_po_number, x_fc_mdc_po_date, x_fc_mdc_po_amount, x_fc_mdc_payment_due_date, x_fc_mdc_letter_source, x_fc_mdc_po_document.
Email triggers via _MDC_EMAIL_TRIGGERS:
documents_ready→ client + authorizerpo_received→ client + authorizer (this is the "approval" trigger — attachesx_fc_mdc_po_document)delivered→ client + authorizercase_closed→ client onlydenied→ client + authorizer
30.9 Hardship Funding (x_fc_hardship_status, 16 states)
quotation → awaiting_pre_assessment → pre_assessment_complete → application_package_ready →
submitted_to_hf → eligibility_interview → approval_received → product_ordered → delivered →
pod_invoice_submitted → payment_received → case_closed.
Special: eligibility_failed, on_hold, denied, cancelled.
Distinctive: pre-assessment + eligibility interview steps. Identifier fields: x_fc_hardship_funder_id, x_fc_hardship_submitted_by, x_fc_hardship_interview_date, x_fc_hardship_approval_date, x_fc_hardship_approval_amount, x_fc_hardship_client_portion, x_fc_hardship_approval_received_via, x_fc_hardship_pre_assessment_source, x_fc_hardship_approval_letter.
Email triggers via _HARDSHIP_EMAIL_TRIGGERS:
application_package_ready→ client + authorizer (note: different status name from other funders — notdocuments_ready)approval_received→ client + authorizerdelivered→ client + authorizercase_closed→ client onlydenied/eligibility_failed→ client + authorizer (special —eligibility_failedalso fires the denial path)
30.10 Direct/Private, Rental, Other (no funder workflow)
These sale types have NO funder-specific status field. They use the standard Odoo SO lifecycle (draft → sent → sale → done). The split-invoicing logic still applies via _create_adp_split_invoice if _is_adp_sale() is True (which it isn't for these types), but they typically bill the client directly via the standard "Create Invoice" wizard.
Rental specifically has rental-product extensions on product.template (x_fc_security_deposit_type/amount/percent) and integrates with the technician task's rental inspection flow (task_type='pickup' triggers inspection requirements; see §13).
30.11 Funder workflow comparison
| Aspect | ADP | MOD | ODSP-SA | ODSP-Std | ODSP-OW | WSIB | Insurance | MDC | Hardship |
|---|---|---|---|---|---|---|---|---|---|
| # states | 22 | 16 | 12 | 11 | 10 | 15 | 16 | 16 | 16 |
| Authorizer required | Yes (always) | Yes (always) | Optional | Optional | Optional | Yes (always) | Optional | Yes (always) | Optional |
| Pays the vendor or client? | Vendor | Vendor (90% upfront, 10% on POD) | Vendor | Vendor | Vendor (upfront) | Vendor | Either (branched) | Vendor | Vendor |
| Order of payment vs delivery | Delivery → bill → pay | Payment 90% → deliver → 10% | Deliver → bill → pay | Deliver → bill → pay | Pay → deliver | Deliver → bill → pay | Either order | Deliver → bill → pay | Deliver → bill → pay |
| Government PDF form filled | Yes (Page 11/12) | No (proposal docs) | Yes (SA Mobility 13007E via pdfrw) | No | Yes (Discretionary Benefits via PyPDF2 — AES-encrypted) | No (uses Form 7 manually) | No | No | No |
| Custom email senders | ~25 (_adp_send_*) |
~14 (_send_mod_*) |
_send_sa_mobility_*, _send_odsp_* |
_send_odsp_* |
(uses standard SO confirm) | Generic funder emails | Generic funder emails | Generic funder emails | Generic funder emails |
| Twilio SMS | No | Yes (4 templates) | No | No | No | No | No | No | No |
| Split invoicing | Yes (Client 25% + ADP 75% for REG) | Yes (Client + MOD on partial PCA) | No | No | No | No | No | No | No |
| Cron auto-close | 30d after billed |
(none) | 7d after payment_received |
7d after payment_received |
7d after delivered |
(none) | (none) | (none) | (none) |
| Cron auto-expire | 12 months after approved |
No | No | No | No | No | No | No | No |
| Submission history records | Yes (fusion.submission.history) |
No | No | No | No | No | No | No | No |
| Document locks by stage | Yes (3 stages: submitted/approved/billed) | No | No | No | No | No | No | No | No |
| Case lock available | Yes (x_fc_case_locked) |
No | No | No | No | No | No | No | No |
31. fusion.technician.task base model deep dive (lives in fusion_tasks)
The 3,208-line base. fusion_claims hooks override 5 methods; the rest is the scheduling + GPS + push notification engine.
31.1 Task state machine
status = fields.Selection([
('pending', 'Pending'),
('scheduled', 'Scheduled'), # default
('en_route', 'En Route'),
('in_progress', 'In Progress'),
('completed', 'Completed'),
('cancelled', 'Cancelled'),
('rescheduled', 'Rescheduled'),
])
Transitions (action_* methods, fusion_tasks/models/technician_task.py):
| From | Action | To | Side effects |
|---|---|---|---|
scheduled |
action_start_en_route |
en_route |
Writes GPS, posts chatter, sends _send_task_en_route_email, recalculates travel from current location, sends push notification to tech ("En Route to {client}, {N} more task(s) today"), pushes status to remote if shadow |
scheduled / en_route |
action_start_task |
in_progress |
Writes started_latitude/longitude, posts chatter |
scheduled / en_route / in_progress |
action_complete_task |
completed |
Sets completion_datetime + completed GPS, calls hook _check_completion_requirements, posts chatter, calls _post_completion_to_linked_order (fusion_claims hook), _notify_scheduler_on_completion, sends _send_task_completion_email, recalculates travel for remaining tasks, calls hook _on_complete_extra (fusion_claims hook) |
| any non-completed | action_cancel_task |
cancelled |
Writes GPS, posts chatter, pushes status to remote if shadow, otherwise calls hook _on_cancel_extra (defaults to _send_task_cancelled_email; fusion_claims overrides to also revert SO status if delivery) |
| any | action_reschedule |
(opens form in reschedule_mode context) |
On save, write() detects changes vs old_date/old_time_start/old_time_end and sends _send_task_rescheduled_email |
| any | action_reset_to_scheduled |
scheduled |
None |
All actions call _check_previous_tasks_completed() first — blocks the tech from starting a later task before completing earlier ones for the same date. Considers tasks where the tech is either lead OR additional.
31.2 Scheduling algorithm (_find_next_available_slot)
The 120-line gem that solves "where can this new task fit?":
- Domain: tasks for
(technician_id OR additional_technician_ids)on the given date, not cancelled, excludingexclude_task_id(for edits). - Build intervals clamped to store hours (
fusion_claims.store_open_hour/_close_hour, default 9–18). - Merge calendar events for the tech via
_get_calendar_busy_intervals— pullscalendar.eventrecords where the tech is an attendee, excluding events linked to a fusion task (to avoid double-counting). - Walk gaps starting from
preferred_start: for each booked interval, check ifcursor + duration + travel_to_nextfits before it. Travel time computed by_quick_travel_time(from_lat, from_lng, to_lat, to_lng)and rounded up to the next 15-minute boundary. - Jump past booked intervals adding
travel_from_prev. Snap cursor to nearest 15 min. - If no gap found from
preferred_startto close, wraps and retries from store_open to catch earlier slots. - Returns
(False, False)if fully booked.
The _get_available_gaps function returns the inverse — list of free intervals for the "available slots" badges on the form.
31.3 Calendar integration
_get_calendar_busy_intervalscross-checks againstcalendar.eventso a tech blocked by an OT meeting also gets blocked in the scheduler.- Tasks linked to a calendar event (
calendar_event_idset) are EXCLUDED from the busy-interval check — otherwise they'd double-count themselves. _sync_calendar_event(in thewriteoverride) creates/updates acalendar.eventwhenever scheduling data changes.
31.4 Travel time + route optimization
_quick_travel_time(lat, lng, lat, lng)— uses Google Distance Matrix via_calculate_travel_time(API key fromfusion_claims.google_maps_api_keyICP)._osrm_travel— open-source fallback using OSRM._nominatim_geocode— open-source fallback for geocoding._recalculate_day_travel_chains— when a task moves, rebuilds travel times for ALL tasks for that tech+date._recalculate_travel_from_current_location— when a tech goesen_route, recalculates travel from their actual GPS location (not from previous task's destination)._recalculate_remaining_tasks_travel— when a task completes, recalculates travel chain for remaining tasks from the completion location._cron_calculate_travel_times— periodic background recalculation.
31.5 Overlap detection (_check_no_overlap)
Hard constraint that fires on write. Prevents booking the same tech across overlapping time windows. Considers additional_technician_ids — overlap with a task where the tech is additional also blocks.
_snap_if_overlap is a soft variant invoked during create — attempts to snap the new task to the next free slot via _find_next_available_slot instead of raising.
31.6 Late arrival cron (_cron_check_late_arrivals)
Scans scheduled tasks whose time_start was more than X minutes ago without an en_route transition. Sends a notification to the scheduler. Sets x_fc_late_notified=True to prevent re-firing.
31.7 Push notifications (_send_push_notification)
Uses fusion.push.subscription records (browser push API). Cron _cron_send_push_notifications fires per-task notifications fusion_claims.push_advance_minutes (default 30) before time_start. Setting fusion_claims.push_enabled=False disables all push.
31.8 Hook seam for fusion_claims
def _check_completion_requirements(self):
"""Hook: override in fusion_claims for rental inspection."""
pass
def _on_complete_extra(self):
"""Hook: override in fusion_claims for ODSP advancement + rental inspection."""
pass
def _on_cancel_extra(self):
"""Hook: defaults to sending cancel email; fusion_claims overrides to also revert SO status."""
self._send_task_cancelled_email()
def _post_completion_to_linked_order(self):
"""Hook: post completion notes to linked SO/PO chatter. Override in fusion_claims."""
pass
def _create_vals_fill(self, vals):
"""Hook: pre-fill address from linked SO/PO during create. Override in fusion_claims."""
pass
fusion_claims/models/technician_task.py overrides all five.
32. fusion.task.sync.config cross-instance sync deep dive (lives in fusion_tasks)
The Westin↔Mobility task-sync mechanic (770 lines, full coverage below).
32.1 Configuration model
fusion.task.sync.config records:
name— display label ("Westin Healthcare", "Mobility Specialties")instance_id— short identifier (westin,mobility) — used asx_fc_sync_sourcevalue on shadow tasksurl— remote Odoo URL (e.g.http://192.168.1.40:8069)database— remote DB nameusername+api_key— for JSON-RPC authlast_sync+last_sync_error— health tracking
Local instance identifies itself via the ICP setting fusion_claims.sync_instance_id.
32.2 Technician matching (x_fc_tech_sync_id on res.users)
The same person on two instances has the SAME x_fc_tech_sync_id value (e.g. "gordy"). The sync uses three helpers:
_get_local_tech_map()→{local_user_id: sync_id}for active field staff with sync_id set._get_remote_tech_map()→{sync_id: remote_user_id}from the remote._get_local_syncid_to_uid()→ reverse{sync_id: local_uid}.
Duplicate detection: if two users on the same instance share a sync_id, only the LAST one is used — a warning is logged but the sync proceeds. Per memory [[project_fusion_tasks_sync]], this is a known silent-failure mode — when "tasks not syncing", check duplicates first.
32.3 Push direction (local → remote)
_push_tasks_to_remote(tasks, operation, local_instance_id):
- Filters tasks where the tech has a matched sync_id.
- Maps
additional_technician_idsvia sync_ids to the remote uids. - Builds a
task_datadict with 17 fields including GPS, travel data, completion datetime, name prefixed with[WESTIN]or[MOBILITY]to make shadow tasks visible. - Uses
x_fc_sync_uuidas the dedup key — searches by UUID before deciding create vs write. - On
unlinkoperation, writesstatus='cancelled', active=Falseon the remote shadow instead of deleting. - Sends with
context={'skip_task_sync': True, 'skip_travel_recalc': True}to prevent ping-pong.
Triggered from the write override on the local instance — every meaningful task change pushes immediately.
32.4 Shadow-status push (shadow → originator)
_push_shadow_status(shadow_tasks) — when a tech changes the status of a shadow task locally (en_route / complete / cancel), pushes the new status BACK to the originating instance so the originator's client gets the right emails:
- Looks up the originating instance via
x_fc_sync_source. - Writes
status+ GPS + completion_datetime to the original task on the remote. - Calls
_trigger_parent_notifications(config, task)which RPCs the appropriate_send_*_emailmethod on the originating instance —_send_task_completion_email,_send_task_en_route_email,_send_task_cancelled_email, or_notify_scheduler_on_completion.
This is how the "client gets emailed exactly once, from the originating instance only" rule is enforced.
32.5 Pull direction (remote → local) — _cron_pull_remote_tasks
Daily cron walks all active configs, pulls tasks + technician locations:
- Match by sync_id: only sync tasks whose tech is matched on BOTH sides.
- Cutoff: only fetch tasks with
scheduled_date >= today - 7 days(recent window). - Exclude shadows:
x_fc_sync_source = Falseon the remote (don't sync someone else's shadows back). - Per task: lookup or create local shadow by
x_fc_sync_uuid. Name gets prefixed with[WESTIN]/[MOBILITY]for visibility. - Re-map additional_technician_ids from remote uids → local uids via the sync_id table.
affected_combosset tracks(tech_id, date)pairs that changed — used by_recalculate_day_travel_chainsso route planning accounts for both local AND shadow tasks.- Stale-shadow cleanup: tasks whose UUID disappeared from the remote feed get archived (
active=False) by_cron_cleanup_old_shadows.
32.6 Per-tech location push (_push_technician_location)
When a tech triggers a status action (en_route, complete), action_* calls into this to push their GPS coordinates to the OTHER instance immediately — so the other instance's calendar sees where they are without waiting for the next pull cycle.
Creates a fusion.technician.location record on the remote with source='sync' and sync_instance=<local_instance_id>.
32.7 Sync trigger map
| Local event | Pushed to remote? | How |
|---|---|---|
| Task create | Yes | write() override calls _push_tasks_to_remote([task], 'create') |
| Task write (any change) | Yes | _push_tasks_to_remote([task], 'write') |
| Task unlink | Yes (as cancellation) | Sets remote active=False, status='cancelled' |
| Shadow task status change | Yes (back to originator) | _push_shadow_status(task) |
| Technician GPS update | Yes (broadcast) | _push_technician_location(user_id, lat, lng, accuracy) |
| Periodic full reconciliation | Yes (both directions) | _cron_pull_remote_tasks daily |
32.8 Skip-sync guard rails
context['skip_task_sync']— prevents infinite ping-pong between instances.context['skip_travel_recalc']— prevents the pull from triggering local recalculations.- Terminal-state tasks (
completed,cancelled) — push side does write, but pull side does NOT update existing shadow records that are already terminal (defensive against late race conditions).
33. fusion.assessment (OT assessment model — lives in fusion_authorizer_portal)
The 1,636-line model that captures an OT's assessment of a client + their equipment needs, then generates the draft sale order.
33.1 State machine
state = fields.Selection([
('draft', 'In Progress'),
('pending_signature', 'Pending Signatures'),
('completed', 'Completed'),
('cancelled', 'Cancelled'),
])
33.2 Equipment fields — by type
Type-driven UI: equipment_type ∈ {rollator, wheelchair, powerchair} controls which measurement fields are visible.
- Rollator:
rollator_type(Type 1/2/3),rollator_handle_height,rollator_seat_height,rollator_addons(comma-separated text) - Wheelchair:
wheelchair_type(Type 1–5 — Standard / Lightweight / Ultra Lightweight / Tilt / Dynamic Tilt),legrest_length,cane_height,frame_options,wheel_options,legrest_options,additional_adp_options,seatbelt_type(5 options including 4-Point + Chest Harness),cushion_info,backrest_info - Powerchair:
powerchair_type(Adult Power Base 1/2/3),powerchair_options,specialty_controls(rationale required)
additional_customization — free-form notes section.
33.3 Client type → sale_type mapping
The assessment captures client_type and _create_draft_sale_order translates it:
sale_type = 'adp'
if self.client_type in ['ods', 'acs', 'owp']:
sale_type = 'adp_odsp'
So an OT picking "ODS - ODSP" client type on the assessment yields an adp_odsp sale order on the fusion_claims side — and the ADP/ODSP workflow takes over.
33.4 Initial workflow status mapping
_create_draft_sale_order doesn't dump every new SO into quotation — it picks the right starting status based on what the OT has done:
if assessment_start_date AND assessment_end_date:
target_status = 'waiting_for_application' # assessment is done
elif assessment_start_date:
target_status = 'assessment_scheduled' # only start filled
else:
target_status = 'quotation' # blank assessment
This avoids the write override's auto-transition from firing (which would happen if the SO entered assessment_completed first), letting the assessment-portal flow set the final state directly.
33.5 Page 11/12 signature capture
The model has signature_page_11 + signature_page_12 binary fields. signatures_complete is computed True when both are present. action_complete() raises if signatures incomplete; action_complete_express() (used for quick wheelchair specs without full assessment) bypasses the signature check.
page11_sign_request._generate_signed_pdf reads the most-recent fusion.assessment record linked to the SO to populate the signing context (client_first_name, last_name, middle_name, health_card, health_card_version). If no assessment exists, it falls back to order._get_client_name_parts().
33.6 Output → sale.order
action_complete() does:
- Create or link partner (
_ensure_partner) — auto-createsres.partnerfrom the assessment's client_* fields ifcreate_new_partner=True. _create_draft_sale_order(partner)— full SO with:partner_id,user_id(sales_rep_id),state='draft',origin=f'Assessment: {reference}'x_fc_sale_type,x_fc_authorizer_id,x_fc_client_ref_1/2x_fc_adp_application_status= target_status from §33.4assessment_idback-link (added 2026-04 for traceability)
_generate_signed_documents()— renders signed Page 11/12 viafusion.pdf.template(category='adp', name like 'adp_page_11')._send_completion_notifications()— emails the office.
action_complete_express() skips step 3 (signatures) entirely — used for the "express" assessment route from the sales-rep portal where the rep just needs to spec a wheelchair without doing the full ADP assessment.
34. fusion.accessibility.assessment (MOD/accessibility assessment — lives in fusion_authorizer_portal)
The 966-line sibling for accessibility modifications (not ADP).
34.1 Assessment types
assessment_type = fields.Selection([
('stairlift_straight', 'Straight Stair Lift'),
('stairlift_curved', 'Curved Stair Lift'),
('vpl', 'Vertical Platform Lift'),
('ceiling_lift', 'Ceiling Lift'),
('ramp', 'Custom Ramp'),
('bathroom', 'Bathroom Modification'),
('tub_cutout', 'Tub Cutout'),
])
34.2 State machine
state = fields.Selection([
('draft', 'Draft'),
('scheduled', 'Visit Scheduled'),
('in_progress', 'Visit In Progress'),
('pending_review', 'Pending Review'),
('completed', 'Completed'),
('cancelled', 'Cancelled'),
])
Six states — _expand_states keeps all 6 visible on the kanban regardless of which states have records.
34.3 Funding source → sale_type
x_fc_funding_source (2026-04 portal audit fix) is REQUIRED and picks the downstream workflow:
('march_of_dimes', 'March of Dimes'),
('odsp', 'ODSP'),
('wsib', 'WSIB'),
('insurance', 'Private Insurance'),
('direct_private', 'Private Pay (Direct)'),
('other', 'Other'),
Maps directly to x_fc_sale_type on the generated SO — so a MOD-funded ramp project creates an SO with sale_type='march_of_dimes' and enters the MOD workflow (§30.2).
34.4 Booking source
booking_source = fields.Selection([
('phone_authorizer', 'Phone - Authorizer'),
('phone_client', 'Phone - Client'),
('walk_in', 'Walk-In'),
('portal', 'Online Booking'),
])
Drives the SMS confirmation flag (sms_confirmation_sent).
34.5 Per-type measurement fields
The model has hundreds of measurement fields, only some of which are visible per assessment_type (form view conditions). Examples for stairlift_straight:
stair_steps(Number of Steps)stair_nose_to_nose(inches)stair_side(left/right)stair_style(standard / heavy-duty / etc.)
stairlift_curved, vpl, ceiling_lift, ramp, bathroom, tub_cutout each have their own set of fields.
35. fusion_authorizer_portal controller routes — detailed
Full per-route inventory from portal_main.py (2,827 lines), portal_assessment.py (1,238), portal_page11_sign.py (206), pdf_editor.py (218).
35.1 Page 11 public signing (portal_page11_sign.py)
| Route | Type | Auth | What it does |
|---|---|---|---|
/page11/sign/<token> |
http | public | Renders signing form. Resolves request by access_token, branches on state (signed/cancelled/expired/ok). Pulls assessment data to pre-fill client name/health card. Pre-fills Google Maps API key for address autocomplete. |
/page11/sign/<token>/submit |
http POST | public | Writes signature_data + signer details + agent details (if consent_signed_by='agent'), sets state=signed, signed_date. Triggers _generate_signed_pdf + _update_sale_order. |
/page11/sign/<token>/download |
http | public | Downloads the signed PDF (only if state=signed and PDF generated). |
35.2 Authorizer (OT) portal (portal_main.py)
| Route | Type | Notes |
|---|---|---|
/my/authorizer/cases/search |
jsonrpc | Live search of cases — used by typeahead |
/my/authorizer/case/<int:order_id> |
http | Case detail page (sale_order view) — chatter, documents, status |
/my/authorizer/case/<int:order_id>/comment |
http POST | Add a comment (creates fusion.authorizer.comment record) |
/my/authorizer/case/<int:order_id>/upload |
http POST + csrf | Upload a document — creates fusion.adp.document record + binary on the SO |
/my/authorizer/document/<int:doc_id>/download |
http | Download an uploaded document |
35.3 Sales rep portal (portal_main.py)
| Route | Type | Notes |
|---|---|---|
/my/sales/cases/search |
jsonrpc | Live search |
/my/sales/case/<int:order_id> |
http | Sales rep view of an SO |
/my/sales/case/<int:order_id>/comment |
http POST | Add comment |
35.4 Technician portal (portal_main.py)
| Route | Type | Notes |
|---|---|---|
/my/technician/task/<int:task_id> |
http | Task detail page — directions, GPS, client info, action buttons |
/my/technician/task/<int:task_id>/action |
json | Trigger task actions (start_en_route / start / complete / cancel). Includes GPS coordinates in payload. |
/my/technician/tomorrow |
http | Next-day schedule preview |
/my/technician/schedule/<string:date> |
http | View any specific date's schedule |
/my/technician/admin/map |
http | Admin-only map of all techs' locations |
/my/technician/location/log |
json | Log technician GPS heartbeat (background) |
/my/technician/push/subscribe |
json | Subscribe to Web Push notifications |
/my/technician/delivery/<int:order_id> |
http | Delivery-specific page for a SO |
/my/technician/task/<int:task_id>/pod |
http | POD signing page (for any task) |
/my/technician/task/<int:task_id>/pod/sign |
json POST | Submit POD signature for the task |
35.5 Client-facing POD (portal_main.py)
| Route | Type | Notes |
|---|---|---|
/my/pod/<int:order_id> |
http | Client-facing POD page for direct signing on delivery |
/my/pod/<int:order_id>/sign |
json POST | Client submits POD signature |
35.6 Assessment portal (portal_assessment.py)
| Route | Type | Notes |
|---|---|---|
/my/assessment/new |
http | Create a new ADP assessment (full form) |
/my/assessment/<int:assessment_id> |
http | Edit an assessment |
/my/assessment/save |
http POST + csrf | Save changes |
/my/assessment/<int:assessment_id>/signatures |
http | Signature capture page (Page 11/12) |
/my/assessment/<int:assessment_id>/save_signature |
jsonrpc | Save Page 11 or Page 12 signature |
/my/assessment/<int:assessment_id>/complete |
http POST + csrf | Mark complete (triggers SO creation) — fails if signatures incomplete |
/my/assessment/express |
http | Quick assessment form (wheelchair specs without signatures) |
/my/assessment/express/<int:assessment_id> |
http | Edit express assessment |
/my/assessment/express/save |
http POST + csrf | Save express assessment + create SO via action_complete_express |
35.7 Accessibility (MOD) portal (portal_main.py)
| Route | Type | Notes |
|---|---|---|
/my/accessibility |
http | Landing page — pick assessment type |
/my/accessibility/list |
http | List of existing accessibility assessments |
/my/accessibility/stairlift/straight |
http | New straight stairlift assessment form |
/my/accessibility/stairlift/curved |
http | New curved stairlift assessment form |
/my/accessibility/vpl |
http | New VPL assessment |
/my/accessibility/ramp |
http | New custom ramp assessment |
/my/accessibility/bathroom |
http | New bathroom mod assessment |
/my/accessibility/save |
json POST + csrf | Save accessibility assessment (creates fusion.accessibility.assessment record) |
35.8 PDF template editor (pdf_editor.py)
| Route | Type | Notes |
|---|---|---|
/pdf_template/preview/<int:template_id> |
http | Render preview image of the PDF page |
/pdf_template/save_fields |
json POST | Save updated field positions from the visual editor |
/pdf_template/regenerate_previews/<int:template_id> |
http | Trigger preview regeneration |
35.9 Timezone detection (portal_main.py)
| Route | Type | Notes |
|---|---|---|
/my/timezone/detect |
jsonrpc | Logs the user's browser timezone — useful for displaying technician schedules in their local TZ |
36. sale.order constraint methods (@api.constrains)
10 constraint methods enforce field-level validation rules independent of the status-machine gates. These fire on EVERY write, not just status transitions, so a write to one of these fields with invalid data raises ValidationError regardless of context.
| Constraint | Field | Rule |
|---|---|---|
_check_odsp_division_change |
x_fc_odsp_division |
Blocks changing the division once the active status field is past quotation. Prevents orphaning workflow progress. No bypass — must close/cancel the current case first. |
_check_claim_number |
x_fc_claim_number |
Exactly 10 digits, numbers only. Regex ^\d{10}$. Whitespace stripped. |
_check_client_ref_1 |
x_fc_client_ref_1 |
Up to 4 letters total (comma allowed and excluded from count). Regex ^[A-Za-z,]+$. Convention: first 2 letters of first name + last 2 letters of last name (e.g. "John Doe" → "JODO"). |
_check_client_ref_2 |
x_fc_client_ref_2 |
Exactly 4 digits. Regex ^\d{4}$. Convention: last 4 of health card number. |
_check_original_application_pdf |
x_fc_original_application_filename |
Must end in .pdf (case-insensitive). |
_check_signed_pages_pdf |
x_fc_signed_pages_filename |
Must end in .pdf. |
_check_final_application_pdf |
x_fc_final_application_filename |
Must end in .pdf. |
_check_xml_file |
x_fc_xml_filename |
Must end in .xml. |
_check_proof_of_delivery_pdf |
x_fc_proof_of_delivery_filename |
Must end in .pdf. |
_check_delivery_date_after_approval |
x_fc_adp_delivery_date + x_fc_claim_approval_date |
delivery_date >= approval_date. For early delivery cases, the POD doc should show the approval date, NOT the actual delivery date — the error message tells the user this directly. |
Gotchas for these constraints:
- They fire on every write to the listed field — not just user-driven writes. Cron/sync code that touches these fields with invalid data will raise.
- The file-extension constraints check
*_filename, NOT the binary content. Combined with the application_received_wizard's PDF magic-byte check (§4.4), this gives two layers of defense. _check_client_ref_1allows commas — useful for clients with two-part last names ("De,Mo" for "De Souza Morales" →DEMO)._check_delivery_date_after_approvalis the reason for the "early delivery" pattern — the POD doc must use the approval_date as its delivery_date when the client takes physical possession before ADP approves. See §4.x and the early-delivery flag on the SO.- These constraints fire AFTER the
writeoverride's status-transition gates have run — so if both fail, the user sees the constraint error first (because it's raised inside the same write transaction).
37. PDF report templates — business logic
12 QWeb templates. All follow the same skeleton: <t t-call="web.html_container"> → web.external_layout → set is_* flags + primary/secondary colors → render page contents. The report_templates.xml shared snippets (§19.1) are t-call-able from any of them.
37.1 sale_report_landscape.xml — Quotation/Order (ADP)
Bound to sale.order. Distinguishes draft/sent (Quotation) vs confirmed (Sales Order) via doc.state in ['draft','sent']. Conditional sections:
| Block | Condition | Notes |
|---|---|---|
| ADP Info Table | is_adp (doc.x_fc_is_adp_sale) |
Shows Claim # / Application Type (Reason for Application — labelled via dict(...selection)) / Client Ref 2 / Delivery / Authorization / Approval dates. Blue #e3f2fd background. |
| PLCMT column | is_adp |
Adds the Device Placement (L/R/N/A) column to the order-lines table — only when ADP |
| ADP/Client portion columns | always | Two color-coded columns: ADP #1976d2 header / #e3f2fd row bg; Client #e65100 header / #fff3e0 row bg |
| Section/Note rows | display_type in ('line_section','line_note') |
Section rows get grey #f0f0f0 bg with bold; note rows get italic |
| Line description cleanup | always | Strips [internal_ref] prefix — same logic as §19.2: if '] ' in line.name: clean_name = line.name.split('] ', 1)[1] |
| Unit price | if x_fc_adp_price |
Shows ADP price if available, otherwise price_unit — i.e. the list price shown to the user is the ADP maximum, not the retail price |
| Totals table | always | Subtotal + Total ADP Portion + Total Client Portion + Taxes + Grand Total. ADP/Client rows use the matching pastel backgrounds |
| Payment Terms | doc.payment_term_id.note |
Left column |
| Signature | doc.signature |
Rendered as base64 image, max 4cm × 8cm, with doc.signed_by underneath |
| Terms and Conditions | doc.note |
Below totals if present |
Colspan adjustments: '10' if is_adp else '9' for section/note rows (the PLCMT column adds one).
37.2 invoice_report_landscape.xml — Invoice (ADP)
Bound to account.move. Same structure as sale_report_landscape with these additions:
| Block | Condition | Notes |
|---|---|---|
| Title | move_type == 'out_invoice' and state == 'posted' |
"Invoice" prefix; else just the name |
| Payment Reference | doc.payment_reference |
Displayed near payment terms |
| ADP Portion totals | x_fc_adp_invoice_portion == 'adp' |
Special ADP totals block — only for ADP-portion invoices |
| Amount Residual | amount_residual and != amount_total |
"Amount Due" row when partially paid |
| Payment Details | payment_state != 'invoicing_legacy' |
Bottom-of-report payment history. If payment_state == 'paid': green "✓ PAYMENT DETAILS - PAID IN FULL" banner. Lists all account.payment records linked to the invoice |
| Outstanding Balance | amount_residual > 0 and payment_state != 'paid' |
Highlight box with remaining due |
| Narration | doc.narration |
Notes section at the bottom |
37.3 invoice_report_portrait.xml
Portrait variant — same conditional logic, narrower column layout. ADP fields collapsed into a 2-column info table instead of 6.
37.4 sale_report_portrait.xml
Portrait variant of the quotation report. Same color conventions.
37.5 report_proof_of_delivery.xml — ADP POD
Bound to sale.order. No prices anywhere — POD is a delivery acknowledgment, not a price doc. Key blocks:
| Block | Notes |
|---|---|
| Title | "ADP PROOF OF DELIVERY" — centered |
| Reference line | doc.name + Claim # if set |
| Customer + Delivery address tables | Side-by-side bordered boxes |
| Order Info | Order Date / Delivery Date / Client Type / Sales Rep / Authorizer |
| Products Delivered | ADP Code / Description (cleaned) / Serial # / Placement (if ADP) / Qty / Device Type (no prices) |
| Acknowledgment block | Yellow #fff8e1 bg with left border. Full refund policy link, legal acknowledgment text, uppercase "I HAVE RECEIVED ALL OF THE PRODUCTS AND SERVICES PROMISED TO ME..." |
| Signature section | Captured digital signature OR empty signature lines. If captured: shows x_fc_pod_client_name, x_fc_pod_signature_date, base64-decoded x_fc_pod_signature image (max 80×300px), and "Collected by: {x_fc_pod_signed_by_user_id.name}" |
| Page 2 | Forced page break (page-break-before: always). Full RETURN AND REFUND POLICY — Change Order/Cancellation, Restocking Fees (30% for Patient Lifts/Hospital Beds/Transport Wheelchairs/Standard Rollators&Walkers), Non-Returnable Items list (~13 categories) |
37.6 report_proof_of_delivery_standard.xml
Non-ADP POD variant. Same structure but no ADP-specific columns. Uses doc.commitment_date instead of x_fc_adp_delivery_date.
37.7 report_proof_of_pickup.xml
For rental pickups. Same skeleton, "Proof of Pickup" title, references doc.commitment_date. Used at end of rental term when collecting equipment back.
37.8 report_approved_items.xml
The PDF version of the _build_approved_items_html table (§16.1). Generated standalone via _generate_approved_items_pdf and attached to the _send_approval_email. Conditional fields: x_fc_assessment_end_date, x_fc_claim_approval_date.
37.9 report_grab_bar_waiver.xml
Liability waiver for grab-bar installations. Standalone form clients sign acknowledging installation responsibility.
37.10 report_accessibility_contract.xml
Accessibility modification contract. For MOD/accessibility projects (stairlifts, ramps, etc.). Fields: validity date, payment terms, project notes.
37.11 report_mod_quotation.xml
MOD-specific quotation. Distinctive fields shown:
x_fc_mod_estimated_weeks— project duration estimatex_fc_estimated_completion_date— target completionx_fc_authorizer_id— assigned OT/specialist
37.12 report_mod_invoice.xml
MOD-specific invoice. Distinctive layout because MOD invoices go to the MOD partner (March of Dimes Canada (HVMP)) not the client — see §7.5. Includes:
- Company VAT in header
- Partner shipping = original client (delivery address)
- HVMP reference number prominent
38. views/sale_order_views.xml — form structure
The 2,768-line form view inheritance file. Key patterns:
38.1 Inheritance priorities
Multiple views inherit sale.view_order_form at different priorities to layer in fusion_claims fields:
- Header fields: sale_type, client_type, authorizer (visibility-gated by
_compute_show_*flags) - Status statusbar: clickable,
filtered_status_selectionwidget hides controlled statuses - Tabs: ADP Case Details / ADP Documents / MOD / ODSP / WSIB / Insurance / MDC / Hardship — each
invisible=driven byx_fc_is_*_saleorx_fc_show_*_fields - Smart buttons: Invoice counts, Vendor Bills, Submission History, Technician Tasks, MOD Invoices, Submission/Page11 counts
38.2 Conditional field visibility rules
The pattern throughout: invisible="not x_fc_show_<funder>_fields" on the per-funder tabs. The compute methods (_compute_show_*_fields) all check x_fc_sale_type and only return True for the matching funder.
This means an ADP order shows ONLY the ADP tab; a MOD order shows ONLY the MOD tab — preventing field clutter.
38.3 Kanban view data attributes
The kanban card has <main> with data-stage="info/warning/success/danger/secondary" / data-priority="1" / data-emergency="1" attributes that the SCSS :has() selectors (§20.4) style. Status → data-stage mapping is in the kanban template itself.
38.4 Search view
- Filters per status (Quotation / In Progress / Approved / Billed / Closed / On Hold / ...)
- Filters per funder (ADP / MOD / ODSP / WSIB / etc.)
- Group-by: Status, Sale Type, Sales Rep, Client Type, Authorizer
- Default search:
my_quotation(user_id = current user) on Quotation Stage list
39. views/adp_claims_views.xml — per-stage action inventory
~80 action records. Pattern: every stage has its own ir.actions.act_window with:
res_model='sale.order'view_mode='list,kanban,form'- Custom
view_idsreferencing the ADP-specific list and kanban views (view_sale_order_list_adp,view_sale_order_kanban_adp) - Custom
search_view_id(view_sale_order_search_adp) - Domain filter that pins the action to a specific status (and sale type)
39.1 ADP action domain matrix (sample)
'All ADP Orders' [('x_fc_is_adp_sale', '=', True)]
'Quotation Stage' [..., ('x_fc_adp_application_status', '=', 'quotation')]
'Assessment Scheduled' [..., ('x_fc_adp_application_status', '=', 'assessment_scheduled')]
'Waiting for Application' [..., ('x_fc_adp_application_status', 'in', ('assessment_completed', 'waiting_for_application'))]
'Application Received' [..., ('x_fc_adp_application_status', '=', 'application_received')]
'Ready for Submission' [..., ('x_fc_adp_application_status', '=', 'ready_submission')]
'Application Submitted' [..., ('x_fc_adp_application_status', 'in', ['submitted', 'resubmitted'])]
'Accepted by ADP' [..., ('x_fc_adp_application_status', '=', 'accepted')]
'Rejected by ADP' [..., ('x_fc_adp_application_status', '=', 'rejected')]
'Needs Correction' [..., ('x_fc_adp_application_status', '=', 'needs_correction')]
'Application Approved' [..., ('x_fc_adp_application_status', 'in', ['approved', 'approved_deduction'])]
'Ready for Delivery' [..., ('x_fc_adp_application_status', '=', 'ready_delivery')]
'Ready to Bill' [..., ('x_fc_adp_application_status', '=', 'ready_bill')]
'Billed to ADP' [..., ('x_fc_adp_application_status', '=', 'billed')]
'Case Closed' [..., ('x_fc_adp_application_status', '=', 'case_closed')]
'On Hold' [..., ('x_fc_adp_application_status', '=', 'on_hold')]
'Withdrawn' [..., ('x_fc_adp_application_status', '=', 'withdrawn')]
'Denied' [..., ('x_fc_adp_application_status', '=', 'denied')]
'Cancelled' [..., ('x_fc_adp_application_status', '=', 'cancelled')]
'Expired' [..., ('x_fc_adp_application_status', '=', 'expired')]
39.2 ODSP action domain matrix
The ODSP menu has THREE division-specific tracks. Each track has per-stage actions filtered by BOTH x_fc_odsp_division AND the division-specific status field:
'All ODSP Cases' [('x_fc_sale_type', 'in', ['odsp', 'adp_odsp'])]
'ODSP Standard' [..., ('x_fc_odsp_division', '=', 'standard')]
'SA Mobility' [..., ('x_fc_odsp_division', '=', 'sa_mobility')]
'Ontario Works' [..., ('x_fc_odsp_division', '=', 'ontario_works')]
# Per status under Standard:
'ODSP Standard - Quotation' [..., ('x_fc_odsp_division', '=', 'standard'), ('x_fc_odsp_std_status', '=', 'quotation')]
'ODSP Standard - Submitted' [..., ('x_fc_odsp_division', '=', 'standard'), ('x_fc_odsp_std_status', '=', 'submitted_to_odsp')]
# ... etc for all 11 SA states, 11 Standard states, 10 OW states
# OW notably:
'OW - Payment Received' [..., ('x_fc_ow_status', '=', 'payment_received')] # BEFORE delivery
'OW - Ready for Delivery' [..., ('x_fc_ow_status', '=', 'ready_delivery')]
39.3 MOD action domain matrix
'All MOD Cases' [('x_fc_sale_type', '=', 'march_of_dimes')]
'Need to Schedule' [..., ('x_fc_mod_status', '=', 'need_to_schedule')]
'Assessment Scheduled' [..., ('x_fc_mod_status', '=', 'assessment_scheduled')]
'Assessment Done' [..., ('x_fc_mod_status', '=', 'assessment_completed')]
'Processing Drawing' [..., ('x_fc_mod_status', '=', 'processing_drawings')]
'Quote Submitted' [..., ('x_fc_mod_status', '=', 'quote_submitted')]
'Awaiting Funding' [..., ('x_fc_mod_status', '=', 'awaiting_funding')]
'Funding Approved' [..., ('x_fc_mod_status', '=', 'funding_approved')]
'PCA Received' [..., ('x_fc_mod_status', '=', 'contract_received')]
'In Production' [..., ('x_fc_mod_status', '=', 'in_production')]
'Project Complete' [..., ('x_fc_mod_status', '=', 'project_complete')]
'POD Sent' [..., ('x_fc_mod_status', '=', 'pod_submitted')]
'Case Closed' [..., ('x_fc_mod_status', '=', 'case_closed')]
'On Hold' [..., ('x_fc_mod_status', '=', 'on_hold')]
'Funding Denied' [..., ('x_fc_mod_status', '=', 'funding_denied')]
'Cancelled' [..., ('x_fc_mod_status', '=', 'cancelled')]
39.4 Other funder actions (no per-stage drill-down)
WSIB / Insurance / MDC / Hardship / Rental / Direct-Private / Other each get ONE list action with [('x_fc_sale_type', '=', '<funder>')]. No per-stage actions — the user filters by status in-list.
39.5 Invoice action domain matrix
'All Funder Invoices' [('x_fc_invoice_type', '!=', False), ('x_fc_invoice_type', '!=', 'regular'), ...]
'ADP Client Invoices' [('x_fc_invoice_type', '=', 'adp_client'), ...]
'ODSP Invoices' [('x_fc_invoice_type', 'in', ['odsp', 'adp_odsp']), ...]
'MOD Invoices' [('x_fc_invoice_type', '=', 'march_of_dimes'), ...]
'WSIB Invoices' [('x_fc_invoice_type', '=', 'wsib'), ...]
'Insurance Invoices' [('x_fc_invoice_type', '=', 'insurance'), ...]
'Direct/Private Invoices' [('x_fc_invoice_type', '=', 'direct_private'), ...]
'Hardship Invoices' [('x_fc_invoice_type', '=', 'hardship'), ...]
'Rental Invoices' [('x_fc_invoice_type', '=', 'rental'), ...]
'Muscular Dystrophy Invoices' [('x_fc_invoice_type', '=', 'muscular_dystrophy'), ...]
'Other Invoices' [('x_fc_invoice_type', '=', 'other'), ...]
All filtered to move_type in ['out_invoice', 'out_refund'] (customer invoices + refunds only — not vendor bills).
39.6 Special non-funder action: ACSD
'ACSD Cases' [('x_fc_client_type', '=', 'ACS')]
ACSD (Assistance to Children with Severe Disabilities) is a CLIENT TYPE, not a sale type. The menu has a dedicated ACSD entry that catches any sale type but with client_type='ACS'.
40. fusion_authorizer_portal.sale_order extensions (266 lines)
Adds 6 fields to sale.order + 5 methods:
| Field | Purpose |
|---|---|
portal_comment_ids (O2m → fusion.authorizer.comment) |
Comments left by OT/sales rep on the portal |
portal_comment_count (computed) |
Smart-button count |
portal_document_ids (O2m → fusion.adp.document) |
Documents uploaded via the portal |
portal_document_count (computed) |
Smart-button count |
assessment_id (M2o → fusion.assessment) |
Back-link to the ADP equipment assessment that created this SO |
accessibility_assessment_id (M2o → fusion.accessibility.assessment) |
2026-04 fix — back-link to the accessibility assessment (stair lift / VPL / etc.) that created this SO |
portal_authorizer_id (computed) |
Consolidated authorizer reference — mirrors x_fc_authorizer_id for portal access checks |
write override fires _send_authorizer_assignment_notification whenever x_fc_authorizer_id changes to a new value.
action_message_authorizer opens a mail composer pre-filled with the authorizer as recipient.
JSON-RPC methods (called from portal JS):
get_authorizer_portal_cases(partner_id, search_query, limit, offset)— paginated list of cases the OT is authorizer forget_sales_rep_portal_cases(user_id, search_query, limit, offset)— same for sales reps_build_search_domain(query)— fuzzy search across name, partner name, claim number, client refs
get_portal_display_data() — returns a dict with all display data for one SO (called from portal page render).
_get_partner_address_display() — formatted address string.
_get_product_lines_for_portal() — product lines minus internal-only data.
41. fusion_authorizer_portal.res_partner extensions (767 lines)
Adds geolocation + portal access management:
| Field | Purpose |
|---|---|
x_fc_latitude, x_fc_longitude |
Geocoded coords for the partner's address (set by _geocode_address on write) |
(computed) assigned_case_count |
# of SOs where this partner is authorizer |
(computed) assessment_count |
# of fusion.assessment records for this partner |
(computed) assigned_delivery_count |
# of delivery tasks |
(computed) portal_access_status |
"Active" / "Pending" / "None" — depends on whether a portal user exists |
Key actions:
action_grant_portal_access— provisions ares.usersportal account, creates a welcome knowledge article (_create_welcome_article), sends invitation email (_send_portal_invitation_email)action_resend_portal_invitation— re-fires the invitationaction_view_assigned_cases/action_view_assessments/action_view_assigned_deliveries— smart-button targetsaction_mark_as_authorizer/action_mark_as_technician— setsx_fc_contact_typeand assigns the matching role groupaction_batch_send_portal_invitation/action_mark_and_send_invitation— bulk operations_assign_portal_role_groups(portal_user)/_assign_internal_role_groups(internal_user)— auto-assigns groups based onx_fc_contact_type(occupational_therapist → authorizer group, etc.)
write override calls _geocode_address whenever address fields change — keeps x_fc_latitude/x_fc_longitude in sync.
42. google_address_autocomplete.js (1,506 lines) — what it actually does
OWL patch on FormController that adds Google Places autocomplete to address fields on res.partner and other models. Key behaviors:
| Behavior | Details |
|---|---|
| Lazy API load | Loads Google Maps JS API dynamically only when first needed. Singleton — googleMapsLoaded flag prevents duplicate loads. API key read from ir.config_parameter['fusion_claims.google_maps_api_key']. |
| Conflict avoidance | Checks registry.category("fields").contains("google_address_autocomplete") — if Odoo Enterprise's own google_address_autocomplete module is installed, skips our patch (theirs handles it). |
| Canada-only | componentRestrictions: { country: 'ca' } — autocomplete suggestions limited to Canadian addresses. |
| Field parsing | Maps Google place components → Odoo fields: street_number + route → street; subpremise → street2; floor → 'Floor N' → street2; locality / sublocality_level_1 → city; administrative_area_level_1 → state code; postal_code → zip; country.short_name → country code |
| Saved record path | For records with resId, uses globalOrm.write('res.partner', [resId], updatePayload). Writes country first, then state in a separate write 100ms later (Odoo's state field depends on country onchange to populate domain). |
| New record path | For unsaved records, uses record.update() for text fields + simulateMany2OneSelection DOM hack for country/state (since Many2one updates on unsaved records don't propagate cleanly via update()). Waits 300ms between country and state for the onchange to settle. |
| Reload after write | Calls record.load() to refresh the form display after the ORM writes. |
The 1,506 lines are mostly DOM-manipulation fallback logic for the new-record case + debug logging. The core integration is the ~200-line initAutocompleteOnField function.
43. static/src/xml/document_preview.xml (204 lines) — OWL templates
Three OWL component templates:
fusion_claims.DocumentPreviewDialog— full-screen PDF/XML preview dialog. Uses<iframe>pointing at the attachment URL with PDF.js viewer parameters. Maximize toggle button (xl ↔ fullscreen). Loading spinner.fusion_claims.PreviewButtonWidget— button widget rendered inside list view cells. Click opens DocumentPreviewDialog with the attachment_id fromrecord.data.attachment_id.- Inline error states — "No document to preview" warning notification template.
Used by:
views/page11_sign_request_views.xmlfor preview button on the signed PDF- Any form/list view that uses
widget="preview_button"on an attachment-id field - The custom
fusion_claims.preview_documentclient action (referenced byodsp_ready_delivery_wizard.action_preview_full)
44. fusion_tasks smaller models
44.1 fusion.email.builder.mixin (241 lines) — full anatomy
The shared email builder. Two main public methods:
_email_build(title, summary, sections, note, note_color, email_type, attachments_note, button_url, button_text, sender_name, extra_html) — assembles the full email HTML. Builds in this order:
- Outer wrapper div (font + max-width 600px + centered)
- Accent bar (4px high, color from
_EMAIL_COLORS[email_type]) - Company name (uppercase, accent color)
- Title (h2)
- Summary (muted opacity)
- Sections (per
_email_section(heading, rows)— bordered table with label/value rows) - Note (per
_email_note(text, color)— left-border accent block) - Extra HTML (raw insert)
- Attachment note (per
_email_attachment_note(description)— dashed-border callout) - CTA button (per
_email_button(url, text, color)— centered, rounded, accent bg) - Sign-off (
Best regards, <strong>{signer}</strong><br/>{company}) - Footer (
{company} · {phone} · {email}+ "This is an automated notification from the ADP Claims Management System.")
The footer line is what _mod_email_build and _odsp_email_build overwrite to change the voice (§16.5.1).
Building-block helpers (callable independently):
_email_section(heading, rows)— labelled details table_email_note(text, color)— left-border accent note_email_button(url, text, color)— centered CTA button_email_attachment_note(description)— dashed-border attachment callout_email_status_badge(label, color)— inline pill badge (4 background tints mapped from foreground colors)
_get_company_info() — pulls name, phone, email from self.company_id or self.env.company.
_email_is_enabled() — reads fusion_claims.enable_email_notifications (default True).
_EMAIL_COLORS palette:
'info': '#2B6CB0' # blue
'success': '#38a169' # green
'attention': '#d69e2e' # amber
'urgent': '#c53030' # red
44.2 fusion.push.subscription (73 lines) — Web Push subscriptions
| Field | Purpose |
|---|---|
user_id |
Owner (technician) |
endpoint |
Web Push endpoint URL (browser-supplied) |
p256dh_key, auth_key |
Browser encryption keys for VAPID push |
browser_info |
User agent string |
active |
Soft-delete flag |
Unique constraint on endpoint so a user re-subscribing from the same browser updates the existing record instead of creating duplicates.
register_subscription(user_id, endpoint, p256dh_key, auth_key, browser_info) — idempotent upsert called from portal JS (/my/technician/push/subscribe route).
44.3 fusion.technician.location (131 lines) — GPS history
| Field | Purpose |
|---|---|
user_id |
Technician |
latitude, longitude |
10-digit precision |
accuracy |
GPS accuracy in meters |
logged_at |
Timestamp (indexed) |
source |
portal / app / sync |
sync_instance |
Origin instance ID if synced (e.g. westin, mobility) |
log_location(latitude, longitude, accuracy) — JSON-RPC entrypoint from portal JS heartbeat.
get_latest_locations() — admin map data. Uses Postgres DISTINCT ON (user_id) to fetch the most-recent point per technician within the last 24 hours. Includes both local + synced locations — the admin map shows all shared techs regardless of which Odoo instance they're clocked into. Falls back to local instance ID if sync_instance is blank.
_cron_cleanup_old_locations() — configurable retention via fusion_claims.location_retention_days:
- Empty / unset → 30 days (default)
"0"→ delete at end of day (keep today only)"N"→ keep N days
44.4 fusion.task.sync.config extras not covered earlier
_cron_cleanup_old_shadows— removes shadow tasks whose remote UUID has disappeared. Runs after each pull cycle.- Action buttons:
action_test_connection(logs in + counts matched techs),action_sync_now(manual force-sync trigger). - Settings UI:
fusion.task.sync.configrecords visible in Settings → Technical → Task Sync. New configs need URL + database + username + api_key + instance_id.
46. Production deployment — Westin + Mobility
When the user says "deploy to westin" or "deploy to mobility", the targets are:
| Target | VM | LAN IP | Proxmox node | Service URL | DB name | Containers |
|---|---|---|---|---|---|---|
| Westin | VM 101 odoo-westin |
192.168.1.40 |
pve-worker1 (192.168.1.6) |
erp.westinhealthcare.ca |
westin-v19 |
odoo-dev-app + odoo-dev-db |
| Mobility | VM 115 odoo-mobility |
192.168.1.102 |
pve-worker3 (192.168.1.8) |
erp.mobilityspecialties.ca |
mobility |
odoo-mobility-app + odoo-mobility-db |
Compose path on both VMs: /opt/odoo/docker-compose.yml (config at /opt/odoo/odoo.conf).
46.1 SSH access (via Tailscale jump host)
Jump host: supabase-prod at Tailscale IP 100.74.28.73 (root).
# Windows (use the SSH config aliases — ProxyCommand via supabase-prod)
ssh odoo-westin
ssh odoo-mobility
# Mac (explicit jump)
ssh -J root@100.74.28.73 root@192.168.1.40 # westin
ssh -J root@100.74.28.73 root@192.168.1.102 # mobility
# Run a one-off command
ssh odoo-westin "<command>"
ssh odoo-mobility "<command>"
Both VMs accept the ed25519 key at ~/.ssh/id_ed25519. Both have the windows-home and gurpreet@mac-studio keys authorized.
46.2 Deploy workflow
# 1. SSH in and pull the latest module code (modules live in /opt/odoo/addons/ — confirm path per VM)
# 2. Restart the Odoo container with -u to upgrade the module:
# Westin
ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 -u fusion_claims --stop-after-init && docker restart odoo-dev-app"
# Mobility
ssh odoo-mobility "docker exec odoo-mobility-app odoo -d mobility -u fusion_claims --stop-after-init && docker restart odoo-mobility-app"
For multiple modules: -u fusion_claims,fusion_tasks,fusion_authorizer_portal.
46.3 Database probes
# List all databases on a VM
ssh odoo-westin "docker exec odoo-dev-db psql -U odoo -d postgres -lqt | cut -d'|' -f1 | grep -v -e '^$' -e template -e postgres"
ssh odoo-mobility "docker exec odoo-mobility-db psql -U odoo -d postgres -lqt | cut -d'|' -f1 | grep -v -e '^$' -e template -e postgres"
# Run SQL against the production DB
ssh odoo-westin "docker exec odoo-dev-db psql -U odoo -d westin-v19 -c 'SELECT count(*) FROM sale_order;'"
ssh odoo-mobility "docker exec odoo-mobility-db psql -U odoo -d mobility -c 'SELECT count(*) FROM sale_order;'"
Westin has a fleet of databases (westin-v19, westin-v19-ref, westin-v19-cra-aligned, westin-v19-reconciled, westin-v19-assets-done, etc.) — these are test/reference copies for accounting work. Production is always westin-v19. The db_name = westin-v19 line in /opt/odoo/odoo.conf confirms it's the active DB.
46.4 Logs + restart
# Tail Odoo logs
ssh odoo-westin "docker logs -f --tail 100 odoo-dev-app"
ssh odoo-mobility "docker logs -f --tail 100 odoo-mobility-app"
# Hard restart (compose-aware)
ssh odoo-westin "cd /opt/odoo && docker compose restart odoo"
ssh odoo-mobility "cd /opt/odoo && docker compose restart odoo"
46.5 DNS / Cloudflare
Both production domains (erp.westinhealthcare.ca and erp.mobilityspecialties.ca) route through Cloudflare with proxied DNS pointing at the reverse-proxy IP 142.112.5.37 (production-caddy VM 104 on pve-worker1), which reverse-proxies to the VM's internal IP and Odoo port 8069.
For Cloudflare API operations (cache purge, WAF rules, DNS record changes), the Supabase MCP holds Cloudflare API access — query public.credentials_vault or use the MCP's Cloudflare tooling rather than embedding keys in this file or the repo. Don't commit any Cloudflare API tokens.
46.6 Other Nexa Odoo VMs (for reference — don't deploy here unless told)
| VM | Name | IP | Purpose |
|---|---|---|---|
| 311 | odoo-apex |
(worker1) | Apex Mobility Solutions production |
| 315 | odoo-nexa |
(worker1) | NEXA Systems internal Odoo |
| 316 | odoo-trial |
192.168.1.112 |
tryfusion.nexasystems.ca trial instance |
"Deploy to westin" or "deploy to mobility" means only VM 101 / VM 115 — never these.
45. Final inventory — what's definitely captured
After 9 rounds of deep diving, here's what CLAUDE.md covers vs the codebase:
Captured (everything that affects runtime behaviour):
- Every model field, computed and stored
- Every workflow state machine across 9 funders
- Every wizard with allowed-from + purpose + fields-set
- Every email sender with recipients + triggers
- Every cron job with cadence + logic
- Every constraint method with regex + rule
- Every special-character/edge-case behaviour I encountered
- Every cross-module integration point with both sibling modules (fusion_tasks, fusion_authorizer_portal)
- Every PDF report's conditional sections + business logic
- Every ICP setting (~60+)
- Every gotcha (~83)
- Every portal route (~40+)
- Every PDF template field-name quirk (SA Mobility 13007E + OW Discretionary Benefits)
Not captured (because it doesn't affect behaviour):
- Pure form-layout XPaths (which tab a field appears under)
- QWeb rendering styling (font sizes, table padding)
- Per-page CSS pixel positioning
- DOM-manipulation fallback paths in google_address_autocomplete.js
- Repeated XML parser field declarations (same pattern as 2a)
- Demo data scripts (
import_demo_pool.py,cleanup_demo_pool.py) - The orphan fusion_task_map_view.* files (loaded by fusion_tasks, not us)
Future Claude session reading this file should be able to:
- Predict the outcome of any status transition
- Understand which fields are required at which stage
- Know which constraint will fire on which field value
- Know which email goes to whom for any workflow event
- Trace the path from a sale_order field rename → portal templates that break
- Debug cross-instance sync issues with the Westin↔Mobility setup
- Build new reports following the established color/header/footer conventions
- Add new gotchas in the right format
- Understand the soft-dep on
fusion_faxes+fusion_pdf_preview - Know the deployment fact that fusion_authorizer_portal is always co-installed