Files
Odoo-Modules/fusion_claims/CLAUDE.md
gsinghpal 1314f4581d changes
2026-05-21 03:37:25 -04:00

246 KiB
Raw Blame History

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 to fusion_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 the ai.agent integration. The ai_agent_fusion_claims record + 3 server-action tools (_fc_tool_search_clients, _fc_tool_client_details, _fc_tool_claims_stats) live in data/ai_agent_data.xml and models/ai_agent_ext.py. Channels of type ai_chat are spawned from fusion.client.profile.action_open_ai_chat.
  • fusion_tasks (sibling Nexa module): provides
    • fusion.email.builder.mixin — the _email_build(title, summary, sections, note, email_type, button_url, ...) helper used by ~50 email methods in models/sale_order.py. Without this dependency, every email send breaks.
    • fusion.technician.task — the base model that models/technician_task.py inherits from to add sale_order_id / purchase_order_id links, 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_wizard creates calendar.event records 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_numberbut 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_portalfusion_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_form imports PDFTemplateFiller from odoo.addons.fusion_authorizer_portal.utils.pdf_fillerImportError if missing.
  • fusion.page11.sign.request renders PDFs using fusion.pdf.template records — that model lives in fusion_authorizer_portal, not here.
  • The /page11/sign/<token> URL that the Page 11 wizard generates is handled by fusion_authorizer_portal.controllers.portal_page11_sign — without it the public signing flow is dead.
  • page11_sign_request._generate_signed_pdf references fusion.assessment records — 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 by wizard/odsp_sa_mobility_wizard.py to 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:

  1. fusion.adp.device.code._load_packaged_device_codes() imports data/device_codes/adp_mobility_manual.json (~hundreds of records) via import_from_json.
  2. _link_products_to_device_codes() runs two SQL UPDATE statements: one links product_template.x_fc_adp_device_code_id for products that already have x_fc_adp_device_code set and matches a device code, and one toggles x_fc_is_adp_product = TRUE for products with a code but no flag. Both are guarded by IS NULL checks — 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:

  1. Sets status to approved (no deductions) or approved_deduction (any deduction applied).
  2. Saves claim_number, claim_approval_date, approval_letter (single PDF) — plus a Many2many approval_photo_ids of approval screenshots.
  3. 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.attachment records linked to sale.order so they persist. Both single-file attachments AND screenshots are posted in a SINGLE chatter card (alert-success) with all attachment_ids attached at once.
  4. 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. Tracks invoices_updated count 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_override computes True.
  • override_reason becomes mandatory (raises UserError if blank).
  • The chatter card uses yellow alert-warning styling 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-success styling.

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:

  1. Filename constraint — must end in .pdf (case-insensitive).
  2. Magic-byte check — base64-decoded payload must start with %PDF-. Test fixtures use b'%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_date is required for all reasons except first_access and mod_non_adp.
  • <5 years warning: x_fc_under_5_years (computed from previous_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 reaches approved or approved_deduction — surfaced as a danger-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:

  1. Posts the existing x_fc_final_submitted_application and x_fc_xml_file to chatter for preservation.
  2. Clears these fields plus x_fc_final_application_filename, x_fc_xml_filename, x_fc_claim_submission_date.
  3. 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 datesquote_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_deliverydelivered. 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 past fusion_claims.adp_approval_expiry_months (default 12) auto-transition to expired. Now scans approved, approved_deduction, and on_hold + ready_delivery (2026-04 update — funding window applies regardless of intermediate state).
  • _cron_auto_close_billed_cases (daily): billedcase_closed 1 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_email to 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_form is 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:

  1. Skip if not ADP saleorder._is_adp_sale() returns False unless x_fc_sale_type contains adp. ADP portion = 0, client portion = 0 (the line is "regular" billing).
  2. 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.
  3. 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 (see sale_order_line._get_adp_device_code):
    1. x_fc_adp_device_code (this module's field)
    2. x_adp_code (Studio/legacy field)
    3. default_code (internal reference)
    4. Code in parentheses in the product name, e.g. [MXA-1618] GEOMATRIX SILVERBACK MAX BACKREST - ACTIVE (SE0001109)SE0001109. The strict regex on account.move.line is r'\(([A-Z]{2}\d{7})\)'.
  4. Verification complete AND line not approved → client 100%. (x_fc_device_verification_complete=True and x_fc_adp_approved=False.)
  5. Otherwise — split by client type:
    • REG → 75% ADP / 25% client.
    • ODS, OWP, ACS, LTC, SEN, CCA → 100% ADP / 0% client.
  6. Deductions (applied after the base split):
    • PCTeffective_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.

6.1 Price-source priority for the calculation

When computing the ADP base (NOT price_unit × qty):

  1. product.product_tmpl_id.x_fc_adp_price (this module's stored price)
  2. line x_fc_adp_max_price (override at the line level)
  3. 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 <5y replacement 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_invoice requires 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_idsinvoice_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):

  1. For each non-cancelled invoice on the order:
  2. Build a vals dict of x_fc_* fields from the SO, but ONLY include each key if the field exists on account.move (in invoice._fields check). This is defensive — the module doesn't assume Studio fields are present.
  3. Write with skip_sync=True to prevent recursion.
  4. 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'] and mappings['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_ids link to find matching invoice lines.
  • Bypass: skip_sync=True context 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 method
  • x_fc_payment_note — free text
  • x_fc_is_card_payment (computed) — reads payment_method_line_id.x_fc_requires_card_digits (set on the journal form via views/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:

  1. Customer switch on ADP invoice — when invoice_type='adp', the invoice's partner_id is set to the ADP partner record (searched by name: 'ADP (Assistive Device Program)', 'Assistive Device Program', 'ADP', or 'ADP -'). The original client becomes partner_shipping_id. So an ADP invoice is billed to ADP, shipped to the client. Client invoices keep the original customer.
  2. x_fc_invoice_type is 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.
  3. x_fc_adp_billing_status='waiting' is auto-set on creation for ADP invoices (kicks off the billing-deadline cron).
  4. invoice_origin carries the portion suffixS29958 (Client 25%) or S29958 (ADP 75%). This is what surfaces in the user-facing breadcrumbs.
  5. Price-mismatch detection AND auto-correction — when the device-code DB price differs from the product's x_fc_adp_price by > $0.01:
    • Posts a chatter warning listing each mismatched product.
    • Auto-updates the product's x_fc_adp_price to the DB price (only for products WITHOUT a x_fc_adp_device_code_id link — products with the Many2one are kept managed via the link instead).
  6. [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.
  7. Unapproved + non-ADP-funded items are SKIPPED from the ADP invoice entirely (not even a $0 line). They appear only on the client invoice.
  8. Markup chatter 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 each attachment=True binary 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] and price_unit=0.

Live preview (preview_line_idsfusion_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 to out_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_portion against the recomputed values (tolerance $0.01 × qty). CRITICAL: Verification mismatches RAISE a UserError that 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=1 per line. A line with qty=3 generates 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 in fusion_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 via invoice_ids. Records are grouped by year / month / posting_period_label in 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.constrains requires a x_fc_adp_device_code_id when 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 — all related fields stored.
  • Also: x_fc_security_deposit_type/amount/percent for 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 existing raw_xml in 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):

  1. Health Card Number (exact).
  2. 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):

  1. Match res.partner by x_fc_authorizer_number (exact).
  2. Fallback: fuzzy name match (name ilike "{first} {last}" OR "{last}, {first}").
  3. 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.
  4. Skips if SO already has x_fc_authorizer_id set; 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.Cell15); POD (received_by, date); Note to ADP section markers (boolean per section: section1, section2ad, 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.server records with use_in_ai=True — call methods on ai.agent (extended via models/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 + model gpt-4.1 + analytical response 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 an ltc_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; if mark_ready_for_delivery context flag is set, advance SO to ready_delivery; if mark_odsp_ready_for_delivery, advance ODSP order.
    • _check_completion_requirements — rental pickup tasks block completion until inspection done.
    • _on_complete_extra — ODSP ready_deliverydelivered; 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 to x_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_build so 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.

fusion.page11.sign.request (models/page11_sign_request.py):

  • Standalone signing request: random access_token (UUID4), public URL /page11/sign/<token>, state of draft / 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) or agent (anyone else). send_page11_wizard auto-sets this based on signer_type.
  • Public security ACL: base.group_public has read-only access to fusion.page11.sign.request so the public sign page can resolve the token.
  • _generate_signed_pdf uses fusion.pdf.template (from another module; the active template is named adp_page_11 or page 11) to render a filled PDF, then writes the result to x_fc_signed_pages_11_12 on the SO + creates an ir.attachment.
  • send_page11_wizard opens the composer. Default expiry 7 days. Pre-fills signer from partner_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_signer setting) 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-Text59 in groups of 5 (qty, description, unit_price, taxes, amount). Total at Text60.
  • Labour table (up to 5 rows): Text61-Text80 in groups of 4 (hours, rate, taxes, amount). Total at Text81.
  • Additional Fees (up to 4 rows): Text82-Text97 in groups of 4 (description, rate, taxes, amount). Total at Text98.
  • Estimated totals summary: Text99 (parts), Text100 (labour), Text101 (fees), Text102 (grand total).
  • Page 2 Notes/Comments area: Text1 (collides with vendor Text 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, not pdfrw — because the gov PDF is AES-encrypted (no password, just "protected mode"). pdfrw cannot decrypt; PyPDF2 handles it via reader.decrypt(''). If pdfrw is the only PDF library available, the wizard will fail. Both are optional Python deps.

  • Preserves /AcroForm from 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 /V and /AS to /1 (checked) or /Off (unchecked) via NameObject. 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_readysubmitted_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):

  1. Auto-confirms the SO if state != 'sale'.
  2. Calls standard self._create_invoices().
  3. Writes x_fc_source_sale_order_id on the invoice.
  4. Advances ODSP status to payment_received with chatter note "Ontario Works payment confirmed. Invoice {name} created."
  5. 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:

  1. Loads field positions from the active fusion.pdf.template (category=odsp).
  2. Renders a preview image of the chosen signature_page using pdf2image.convert_from_bytes + PIL ImageDraw, 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
  3. Writes x_fc_sa_signature_page to the SO.
  4. Returns an action that opens the technician task form pre-filled with default_task_type='delivery', default_pod_required=True, and mark_odsp_ready_for_delivery=True context — the task model's _on_create_post_actions hook then advances ODSP status to ready_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_date
  • account.move._get_invoice_type / _get_client_type / _get_authorizer / _get_claim_number / _get_client_ref_1 / _get_client_ref_2 / _get_adp_delivery_date
  • sale.order.line._get_serial_number / _get_device_placement
  • account.move.line._get_serial_number / _get_device_placement
  • product.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 custom x_* fields on the canonical models, fuzzy-matches against keyword lists, and writes the discovered names into the matching ir.config_parameter keys. Surfaces unmapped fields for review.
  • field_mapping_config_wizard is 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.

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_sent is 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 to https://api.twilio.com/2010-04-01/Accounts/{sid}/Messages.json with 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. Reads partner.mobile then falls back to partner.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_scheduled template hard-codes the string "Westin Healthcare" — it's NOT pulled from self.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 billedcase_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_receivedcase_closed. Ontario Works: 7 days after delivered (payment comes BEFORE delivery for OW)
First Application Reminder 9 AM daily _cron_send_application_remindersassessment_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_2assessment_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 approvedexpired 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 only
  • days_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_attachment helper)
  • 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 AM7 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_dark selectors. 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-scheme at compile time and be registered in both web.assets_backend and web.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, raises UserError if 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 for occupational_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. Captures x_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_sid
  • fc_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_date2026-01-23.
  • fusion_claims.adp_posting_frequency_days14.
  • fusion_claims.adp_approval_expiry_months12.
  • fusion_claims.adp_billing_reminder_user_id — single user (stored manually).
  • fusion_claims.adp_correction_reminder_user_ids — comma-separated user IDs.

Email

  • fusion_claims.enable_email_notificationsTrue.
  • fusion_claims.application_reminder_days4.
  • fusion_claims.application_reminder_2_days4.
  • fusion_claims.adp_hold_reminder_interval_days30.
  • fusion_claims.adp_hold_final_warning_days_before_expiry30.

AI

  • fusion_claims.ai_api_key — OpenAI key.
  • fusion_claims.ai_modelgpt-4o-mini / gpt-4o / gpt-4.1-mini / gpt-4.1.
  • fusion_claims.auto_parse_xmlTrue.

Twilio

  • fusion_claims.twilio_enabledFalse.
  • fusion_claims.twilio_account_sid / _auth_token — manager-only.
  • fusion_claims.twilio_phone_number.

MOD

  • fusion_claims.mod_default_emailhvmp@marchofdimes.ca.
  • fusion_claims.mod_vendor_code.
  • fusion_claims.mod_followup_interval_days14.
  • fusion_claims.mod_followup_escalation_days3.
  • fusion_claims.mod_followup_max_per_month2.
  • fusion_claims.mod_followup_window_days30.
  • fusion_claims.mod_followup_max_per_cron_run10.

ODSP

  • fusion_claims.sa_mobility_emailsamobility@ontario.ca.
  • fusion_claims.sa_mobility_phone1-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 — defaults 9.0 / 18.0.

Portal branding

  • fusion_claims.portal_gradient_presetgreen_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_serial
  • fusion_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_serial
  • fusion_claims.field_sol_serial, field_sol_placement
  • fusion_claims.field_aml_serial, field_aml_placement
  • fusion_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

  1. Never write x_fc_adp_application_status (or any controlled status) directly. Use the corresponding action_* method on sale.order so emails, chatter, activity scheduling, and history records fire correctly. The filtered_status_selection widget already hides controlled statuses from the dropdown; only pass with_context(skip_status_validation=True) for legitimate framework calls (sync hooks, cron, test fixtures).

  2. 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.
  3. x_fc_sale_type is locked once any funder workflow leaves quotation. Setting fusion_claims.allow_sale_type_override = True is the only escape hatch — don't add custom bypasses.

  4. 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 a danger-styled sticky notification.

  5. 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.

  6. 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.

  7. 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. Set is_manually_modified = True on an invoice to opt it out of SO→invoice direction; serial numbers always sync via the line-by-line helper.

  8. 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.

  9. Field-mapping getters are not optional. If you read record.x_fc_sale_type directly in code you ship to production, you'll skip the mapping layer and break legacy databases. Use record._get_sale_type().

  10. Post-init hook idempotence_load_adp_device_codes runs on every upgrade. The UPDATE SQL in _link_products_to_device_codes is guarded by IS NULL checks; preserve those when editing or you'll clobber manual product↔device-code remappings on every upgrade.

  11. MOD follow-up cap is shared state. Don't create fusion.activity records of type mod_followup directly — call _mod_followup_cap_state first to check the rolling 30-day window. The cron defends against burst sends, but ad-hoc code can blow past the cap.

  12. fusion_pdf_preview migration is incomplete in this module. Per repo CLAUDE.md, attachments opened by custom buttons should route through att.action_fusion_preview(title='...') instead of ir.actions.act_url with target=new / download=true. When you touch a callsite here, migrate it. Existing report-style downloads are already intercepted by the JS layer.

  13. tax_totals_patch.js is 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.

  14. action_invoice_sent override on account.move auto-selects the email_template_adp_invoice template for ADP invoices. Removing this override without migrating the email-template selection breaks the ADP invoice send UX.

  15. Sale order display_name is overridden to "<name> - <partner.name>" (models/sale_order.py:22-28). Don't rely on display_name being just the order number — use name if that's what you need.

  16. Sale order display name search (_rec_names_search) matches both name and partner_id.name — useful for Many2one autocomplete; gotcha if you're doing a Domain on display_name.

  17. The auto-payment ODSP advancement runs in account.move._compute_payment_state (override). A paid invoice on a linked ODSP order at status pod_submitted / submitted_to_ow / payment_received auto-advances to payment_received and (Ontario Works only) schedules a delivery activity.

  18. fusion_claims.scss uses html.dark / .o_dark — per repo CLAUDE.md, this doesn't reliably fire in Odoo 19. New SCSS should branch on $o-webclient-color-scheme at compile time and be registered in both web.assets_backend AND web.assets_web_dark.

  19. The legacy x_fc_adp_status field (7-state, simpler) is kept for backward compatibility. Read from x_fc_adp_application_status (22-state, comprehensive) instead. Don't write both.

  20. fusion_claims.config is TransientModelmodels/fusion_central_config.py. Don't expect id-stable records. The model exists purely to host action methods callable from the settings UI.

  21. Pages 11 & 12 have THREE valid intake states — don't check x_fc_signed_pages_11_12 directly to gate workflow steps. Use the computed x_fc_has_signed_pages_11_12 which returns True for: bundled flag (x_fc_pages_11_12_in_original=True), separate file uploaded, OR fusion.page11.sign.request in state signed. Same for x_fc_trail_has_signed_pages. Check the existing tests in tests/test_signed_pages_gate.py before adding new gates.

  22. x_fc_case_locked is global — when True, the write override blocks every x_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.

  23. Document status locks vs case lock vs sale type lock — three separate mechanisms with different rules:

    • Sale type lock — blocks x_fc_sale_type writes after any non-quotation funder status. Bypass: fusion_claims.allow_sale_type_override = True OR (fusion_claims.allow_document_lock_override = True AND user in group_document_lock_override) OR context skip_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 = True AND user in group_document_lock_override) OR context skip_document_lock_validation=True.
    • Case lock — blocks all x_fc_* writes when x_fc_case_locked=True. Bypass: context skip_all_validations=True.
  24. Document audit trail preservation in chatter — when you write to a document binary field (x_fc_original_application etc.) on sale.order, the old binary value is automatically copied into a chatter post before being overwritten. Use with_context(skip_document_chatter=True) for legitimate re-saves (e.g., re-rendering Page 11 PDF with the same content).

  25. Replacement <5 year warningx_fc_under_5_years flag (computed from x_fc_previous_funding_date) drives a chatter warning when creating a client invoice for replace_status / replace_size / replace_worn reasons. Don't suppress the warning — it's a hint to verify the approval letter for ADP deductions.

  26. reason_for_application previous-funding gateready_for_submission_wizard requires previous_funding_date for every reason except first_access and mod_non_adp. Don't skip the check on programmatic transitions.

  27. account.payment.x_fc_card_last_four is exactly 4 digitsaccount_payment_register validates .isdigit() and len()==4. Don't store dashed strings or full PANs. The "is card payment" detection prefers payment_method_line.x_fc_requires_card_digits flag over keyword matching — set the flag on the journal form, don't rely on method-name heuristics.

  28. sale.advance.payment.inv adds ADP options — the standard "Create Invoice" wizard now has adp_client (25%) and adp_portion (75%/100%) selections that route to _create_adp_split_invoice. adp_client raises 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_invoices for split scenarios.

  29. PDF magic-byte validationapplication_received_wizard._validate_pdf_bytes checks that the base64-decoded payload starts with %PDF-. Test fixtures use b'%PDF-1.4\n%fake pdf for tests'. If you add a new PDF upload field elsewhere, copy this defence — the filename .pdf constraint alone is not enough.

  30. web_save debug overridesale.order.web_save has a _logger.warning shim 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.error in the except branch.

  31. ready_submission gate uses raw x_fc_signed_pages_11_12, not the computed x_fc_has_signed_pages_11_12 (models/sale_order.py:7706-7707). This means a direct write (without going through application_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 ensuring x_fc_signed_pages_11_12 is populated for separate/remote modes — but bundled-mode orders rely on the wizard's own gate (which uses the computed). If you call write() directly with x_fc_adp_application_status='ready_submission', you must also pass with_context(skip_status_validation=True) for bundled-mode orders to succeed.

  32. MOD partial PCA invoices reuse x_fc_adp_invoice_portion='adp' for the MOD invoice — disambiguate via x_fc_invoice_type ('march_of_dimes' vs 'adp'). Searching only by x_fc_adp_invoice_portion='adp' will catch MOD invoices too.

  33. ADP invoice customer is NOT the SO partner_create_adp_split_invoice switches partner_id to the ADP partner (searched by name) and moves the original client to partner_shipping_id. Aggregations grouped by partner_id on account.move will lump all ADP invoices together under the ADP partner — group by x_fc_source_sale_order_id.partner_id for client-level aggregates.

  34. 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 on fusion.adp.device.code pushes price changes to them. For products WITHOUT the Many2one, _create_adp_split_invoice will silently overwrite their x_fc_adp_price if the device-code DB says otherwise. The chatter warning is the only signal.

  35. Stage 2 device approval syncs to existing invoices_sync_approval_to_invoices rewrites line price_unit on existing client and ADP invoices to match the new approval state. If you've manually adjusted invoice line prices, they'll be overwritten. Use is_manually_modified=True on the invoice if you need to lock it.

  36. mark_as_approved mode attachment garbage collection — many2many_binary to a TransientModel is GC'd when the wizard closes. device_approval_wizard.action_confirm_approval copies the bytes into new persistent ir.attachment records. 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.

  37. MOD quote_submitted / funding_approved auto-stamp dates — the write override auto-fills x_fc_case_submitted / x_fc_case_approved with today if blank. Don't rely on those dates as "user-entered" — they may be auto-stamped by status transitions.

  38. Soft fusion_faxes dependencyodsp_submit_to_odsp_wizard.action_send_fax and action_send_fax_and_email will fail at click-time if fusion_faxes is 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.

  39. action_adp_reopen_expired is a back-compat shim — it just calls action_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.

  40. The 3-month assessment expiry blocks resume-from-holdx_fc_assessment_expired = True when (today - x_fc_assessment_end_date).days > 90. Resuming from on_hold to any other status will hit this gate. The OT must redo the assessment, which (in practice) means using assessment_completed_wizard in override mode to record the new assessment date.

  41. 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_export raises a UserError listing 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.

  42. ADP export expands qty to per-unit rows — a line with qty=3 exports 3 rows, each with qty=1 and unit_portion = stored_portion / qty. If you build downstream tooling against the CSV, expect per-unit rows, not per-line rows.

  43. 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.

  44. _sync_approval_to_invoices re-posts posted invoices — Stage 2 approval flips run invoice.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. Set is_manually_modified=True on the invoice to opt it out.

  45. 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.

  46. MOD activity dedup — 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. Don't bypass this by creating activities directly — use activity_schedule or the cron's pattern, otherwise you'll get duplicate activities every day.

  47. automated=True on mail.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.

  48. _schedule_or_renew_adp_activity updates 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, not activity_schedule directly, for repeating reminders.

  49. Twilio SMS hard-codes "Westin Healthcare" — the assessment_scheduled message template (models/sale_order.py:9825) has the company name baked into a string literal, not pulled from self.company_id.name. Multi-tenant deployments will read wrong here — fix before going past a single Westin install.

  50. Per-MOD-method recipient rules varycontract_received is client-only (no authorizer); invoice_submitted is MOD contact only; pod_submitted includes client + authorizer + MOD contact. Don't assume "all MOD emails CC the authorizer" — see the table in §16.5.

  51. MOD funding denied wizard requires a category — 5 enum values (income_too_high, residency, project_scope, missing_docs, funding_depleted, plus other). The category label is prepended to the free-text reason as '[{label}] {text}' and stored in x_fc_mod_funding_denial_reason. Don't write the field directly — go through the wizard so the format is consistent.

  52. MOD resubmit can clear documentsmod_resubmit_wizard.clear_old_documents wipes x_fc_mod_drawing and x_fc_mod_proposal_doc after posting copies to chatter. If you've built tooling that assumes those fields stay populated through the lifecycle, document the resubmission cycle explicitly.

  53. odsp_pre_approved_wizard routes by division — same wizard, two different target fields depending on x_fc_odsp_division: sa_mobilityx_fc_sa_approval_form; standardx_fc_odsp_approval_document. Ontario Works (x_fc_odsp_division == 'ontario_works') is NOT handled here — OW has its own discretionary flow.

  54. 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-scheme at compile time and be registered in both web.assets_backend AND web.assets_web_dark. The existing patterns work in some browsers and fail in others; treat them as legacy.

  55. SOL list column widths are pinned in SCSS.o_field_one2many[name="order_line"] forces table-layout: fixed with explicit pixel widths for every th[data-name="..."] selector. Odoo 19 ignores the XML width attribute 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.

  56. Default Odoo SO + invoice reports are also modifiedreport_templates.xml inherits account.report_invoice_document and sale.report_saleorder_document to 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.

  57. Acceptance reminder cron has 14-day backlog guardfusion_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 stuck submitted case. 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.

  58. 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.

  59. Acceptance reminder is two-tier≤3 days since submission → office only; >3 days → office + sales rep. The one-shot x_fc_acceptance_reminder_sent flag means each cycle gets AT MOST ONE email; the flag resets on resubmission (see §4.9).

  60. _send_approval_email attaches an "Approved Items" PDF — generated on-the-fly via _generate_approved_items_pdf (calls action_report_approved_items QWeb report). If you suppress this for performance reasons (e.g., for very large orders), the email body still has _build_approved_items_html inline as extra_html — so the user sees the table in the email even if the PDF generation fails.

  61. _send_application_reminder_2_email mentions 90-day assessment validity — content callout to the OT that the assessment may need to be redone if too much time passes. Tied to the x_fc_assessment_expired computed field (>90 days → True).

  62. Report color convention — every report sets primary and secondary from company.primary_color (default #0066a1) and company.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.

  63. Ontario Works auto-close uses delivered, not payment_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_cases closes 7 days after delivered for OW; 7 days after payment_received for SA Mobility and ODSP Standard. Important to remember when building OW reports/KPIs.

  64. 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.

  65. _send_application_reminder_email sets a one-shot flagx_fc_application_reminder_sent=True after 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 via with_context(skip_all_validations=True).write({...}).

  66. The 2026-04 authorizer email policy excludes the OT from accepted and ready_for_delivery emails — 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.

  67. _send_ready_for_delivery_email adds delivery address from partner_shipping_id.contact_address — comma-joined, newlines replaced. Falls back to partner_id if shipping address is blank. Technicians are CC'd; the email also includes Scheduled datetime from the wizard.

  68. _send_withdrawal_email has three different intentscancel (red, permanent), resubmit (amber, back to ready_submission), and (None) (amber, plain). action_adp_withdraw defaults to cancel but status_change_reason_wizard exposes both.

  69. _sync_fields_to_invoices is defensive about Studio fields — checks if 'x_fc_*' in invoice._fields before writing each key. Don't remove these checks even though they look redundant; legacy databases without certain studio fields rely on them.

  70. _sync_serial_numbers_to_invoices does NOT use header fallback — each SO line syncs its OWN serial to its linked invoice lines via sale_line_ids link. 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.

  71. adp_export_record._get_posting_period_for_file uses 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 for adp_import_wizard historical imports.

  72. migrate_from_documents is run from a settings button, not auto. The settings UI in res_config_settings has an action_migrate_adp_export_files button. 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.

  73. SA Mobility gov form has a Text1 field that collides with Text 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.

  74. odsp_sa_mobility_wizard._get_template_path() uses raw os.path instead of Odoo's tools.misc.file_path. If the module is ever deployed as a zip (rare in Odoo deployments but possible), this will fail. Migrate to file_path('fusion_claims/static/src/pdf/sa_mobility_form_template.pdf') if you ship this for multi-tenant.

  75. PDF template field positions for ODSP signing live in fusion.pdf.template (category=odsp) — managed via a drag-and-drop editor that lives in fusion_authorizer_portal. The OWL editor reads field positions per-page; _apply_pod_signature_to_approval_form consumes them. If the gov SA form layout changes, edit the template via the visual editor, not by changing Python coordinates.

  76. 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.

  77. migrate_from_documents archives 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.

  78. OW Discretionary uses PyPDF2, NOT pdfrw — because the gov form is AES-encrypted. The wizard handles decryption (empty password) and AcroForm preservation. Both pdfrw AND PyPDF2 are de-facto required Python deps for ODSP workflows: pdfrw for SA Mobility, PyPDF2 for OW Discretionary. Only pdfrw and pdf2image/PIL are declared in the manifest; PyPDF2 is implicit (used as a transitive dep via PdfFileReader from odoo.tools.pdf in some places).

  79. OW Discretionary form field names DON'T match their labelstxt_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_mapping is the canonical reference — keep it in sync with the gov form, not with semantic intuition.

  80. OW Discretionary checkbox annotations are mutated directlyannot[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.

  81. send_to_mod_wizard subjects use HVMP reference when availablef'{prefix} - {ref} - {client_name}' for cases with x_fc_case_reference, otherwise f'{prefix} - {client_name} - {order.name}'. Don't write code that hard-codes the order name into the subject — preserve the reference-first pattern.

  82. _get_field_att mutates filenames in-place — finds the existing Odoo auto-generated ir.attachment for 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.

  83. send_to_mod_wizard requires DIFFERENT files per mode — drawing mode needs drawing_file; completion mode needs BOTH completion_photos_file AND pod_file. The wizard raises UserError if missing — don't try to bypass by passing mod_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) — covers x_fc_has_signed_pages_11_12 and x_fc_trail_has_signed_pages across all 3 intake modes (bundled flag, separate file, remote sent vs. remote signed). Also covers ready_for_submission_wizard accepting bundled-only flag (no separate file) and case_close_verification_wizard.has_signed_pages accepting the bundled flag.
  • tests/test_application_received_wizard.py (191 lines, 17 test methods) — full coverage of application_received_wizard: bundled/separate/remote intake modes, PDF magic-byte validation (rejects fake .pdf files), status gate (only from assessment_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_mobilityx_fc_sa_approval_form; standardx_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 (NOT mod, NOT adp/odsp).
  • x_fc_is_adp_sale is True for both adp AND adp_odsp (the _compute_is_adp_sale checks for 'adp' in sale_type).
  • x_fc_sale_type_locked is True for ANY funder workflow's non-quotation status — not just ADP's.
  • The display_name of a SO is overridden to "<name> - <partner.name>" (models/sale_order.py:22-28).
  • is_manually_modified opts an invoice out of SO→invoice sync but NOT out of invoice→SO sync. Set it before editing if you want decoupled fields.
  • The pdfrw Python 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.request access is granted to base.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 same sale.order.line — editing a serial on the ADP invoice rewrites it on the client invoice and the SO line. Avoid blocking this with skip_sync unless you really want decoupled serials.
  • The kanban kanban ordering is enforced via _read_group override that sorts by _STATUS_ORDER — don't sort by x_fc_adp_application_status directly in custom views; sort by x_fc_status_sequence (computed, indexed).
  • MOD has its own invoice template (fusion_claims.action_report_mod_invoice) — account.move.action_mod_send_invoice renders + 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 SO ready_delivery advancement.
  • _check_completion_requirements — rental pickup must have inspection done.
  • _on_complete_extra — ODSP ready_deliverydelivered; rental security-deposit refund / damage activity.
  • _on_cancel_extra — delivery cancellation reverts SO to x_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.order likely 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_status value may need a portal-side handler in portal_main.py to render the new state.
  • The fusion.pdf.template schema (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_views post-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 Email 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 Email 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 only
  • approval_received / pre_auth_approved → client + authorizer
  • delivered → client + authorizer
  • case_closed → client only
  • denied → 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 + authorizer
  • po_received → client + authorizer (this is the "approval" trigger — attaches x_fc_mdc_po_document)
  • delivered → client + authorizer
  • case_closed → client only
  • denied → 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 — not documents_ready)
  • approval_received → client + authorizer
  • delivered → client + authorizer
  • case_closed → client only
  • denied / eligibility_failed → client + authorizer (special — eligibility_failed also 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?":

  1. Domain: tasks for (technician_id OR additional_technician_ids) on the given date, not cancelled, excluding exclude_task_id (for edits).
  2. Build intervals clamped to store hours (fusion_claims.store_open_hour / _close_hour, default 918).
  3. Merge calendar events for the tech via _get_calendar_busy_intervals — pulls calendar.event records where the tech is an attendee, excluding events linked to a fusion task (to avoid double-counting).
  4. Walk gaps starting from preferred_start: for each booked interval, check if cursor + duration + travel_to_next fits 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.
  5. Jump past booked intervals adding travel_from_prev. Snap cursor to nearest 15 min.
  6. If no gap found from preferred_start to close, wraps and retries from store_open to catch earlier slots.
  7. 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_intervals cross-checks against calendar.event so a tech blocked by an OT meeting also gets blocked in the scheduler.
  • Tasks linked to a calendar event (calendar_event_id set) are EXCLUDED from the busy-interval check — otherwise they'd double-count themselves.
  • _sync_calendar_event (in the write override) creates/updates a calendar.event whenever 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 from fusion_claims.google_maps_api_key ICP).
  • _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 goes en_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 as x_fc_sync_source value on shadow tasks
  • url — remote Odoo URL (e.g. http://192.168.1.40:8069)
  • database — remote DB name
  • username + api_key — for JSON-RPC auth
  • last_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_ids via sync_ids to the remote uids.
  • Builds a task_data dict with 17 fields including GPS, travel data, completion datetime, name prefixed with [WESTIN] or [MOBILITY] to make shadow tasks visible.
  • Uses x_fc_sync_uuid as the dedup key — searches by UUID before deciding create vs write.
  • On unlink operation, writes status='cancelled', active=False on 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:

  1. Looks up the originating instance via x_fc_sync_source.
  2. Writes status + GPS + completion_datetime to the original task on the remote.
  3. Calls _trigger_parent_notifications(config, task) which RPCs the appropriate _send_*_email method 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:

  1. Match by sync_id: only sync tasks whose tech is matched on BOTH sides.
  2. Cutoff: only fetch tasks with scheduled_date >= today - 7 days (recent window).
  3. Exclude shadows: x_fc_sync_source = False on the remote (don't sync someone else's shadows back).
  4. Per task: lookup or create local shadow by x_fc_sync_uuid. Name gets prefixed with [WESTIN]/[MOBILITY] for visibility.
  5. Re-map additional_technician_ids from remote uids → local uids via the sync_id table.
  6. affected_combos set tracks (tech_id, date) pairs that changed — used by _recalculate_day_travel_chains so route planning accounts for both local AND shadow tasks.
  7. 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 15 — 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:

  1. Create or link partner (_ensure_partner) — auto-creates res.partner from the assessment's client_* fields if create_new_partner=True.
  2. _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/2
    • x_fc_adp_application_status = target_status from §33.4
    • assessment_id back-link (added 2026-04 for traceability)
  3. _generate_signed_documents() — renders signed Page 11/12 via fusion.pdf.template (category='adp', name like 'adp_page_11').
  4. _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_1 allows commas — useful for clients with two-part last names ("De,Mo" for "De Souza Morales" → DEMO).
  • _check_delivery_date_after_approval is 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 write override'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 estimate
  • x_fc_estimated_completion_date — target completion
  • x_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_selection widget hides controlled statuses
  • Tabs: ADP Case Details / ADP Documents / MOD / ODSP / WSIB / Insurance / MDC / Hardship — each invisible= driven by x_fc_is_*_sale or x_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_ids referencing 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 for
  • get_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 a res.users portal account, creates a welcome knowledge article (_create_welcome_article), sends invitation email (_send_portal_invitation_email)
  • action_resend_portal_invitation — re-fires the invitation
  • action_view_assigned_cases / action_view_assessments / action_view_assigned_deliveries — smart-button targets
  • action_mark_as_authorizer / action_mark_as_technician — sets x_fc_contact_type and assigns the matching role group
  • action_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 on x_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 from record.data.attachment_id.
  • Inline error states — "No document to preview" warning notification template.

Used by:

  • views/page11_sign_request_views.xml for 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_document client action (referenced by odsp_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:

  1. Outer wrapper div (font + max-width 600px + centered)
  2. Accent bar (4px high, color from _EMAIL_COLORS[email_type])
  3. Company name (uppercase, accent color)
  4. Title (h2)
  5. Summary (muted opacity)
  6. Sections (per _email_section(heading, rows) — bordered table with label/value rows)
  7. Note (per _email_note(text, color) — left-border accent block)
  8. Extra HTML (raw insert)
  9. Attachment note (per _email_attachment_note(description) — dashed-border callout)
  10. CTA button (per _email_button(url, text, color) — centered, rounded, accent bg)
  11. Sign-off (Best regards, <strong>{signer}</strong><br/>{company})
  12. 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.config records 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