# 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_number` — **but `fusion_faxes` is NOT in `__manifest__.py.depends`**. The fax actions are guarded by `hasattr` checks so the wizard still loads if `fusion_faxes` is missing, but the "Send Fax" / "Send Email + Fax" buttons will fail at click-time. If you're moving this module to a new database, install `fusion_faxes` alongside it. ### ⚠ Reverse-dependency: `fusion_authorizer_portal` always installed alongside The dependency direction is **`fusion_authorizer_portal` → `fusion_claims`** (hard, declared in fusion_authorizer_portal's manifest), but fusion_claims uses APIs that only exist when fusion_authorizer_portal is installed: - `sale.order._apply_pod_signature_to_approval_form` imports `PDFTemplateFiller` from `odoo.addons.fusion_authorizer_portal.utils.pdf_filler` — `ImportError` 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/` 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: ```javascript 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/` | `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 dates** — `quote_submitted` stamps `x_fc_case_submitted` if blank; `funding_approved` stamps `x_fc_case_approved` if blank. **POD signature auto-overlay** — when `x_fc_pod_signature` is set, calls `_apply_pod_signature_to_approval_form` (overlays the client signature onto the SA Mobility government PDF), and auto-advances SA Mobility from `ready_delivery` → `delivered`. Bypass via `skip_pod_signature_hook=True` context. **Field-mapping aware recompute** — reads ICP `fusion_claims.field_sale_type` / `field_so_client_type` and triggers `_compute_is_adp_sale` + line-level `_compute_adp_portions` if those fields (or their mapped equivalents) changed. **Sync to invoices** — when sync fields change (`x_fc_claim_number`, `x_fc_client_ref_1/2`, `x_fc_adp_delivery_date`, `x_fc_authorizer_id`, `x_fc_client_type`, `x_fc_primary_serial`, `x_fc_service_start/end_date`), calls `_sync_fields_to_invoices` to push to all linked invoices. ### 4.10 Special transitions matrix Each special transition has its own allowed-from set (`models/sale_order.py:4426-4762`): | Action | Allowed FROM | Effect | |---|---|---| | `action_adp_put_on_hold` | `approved`, `approved_deduction` ONLY (2026-04 rule) | Status → `on_hold`, records previous status, resets hold-reminder flags | | `action_adp_withdraw` | `submitted`, `resubmitted`, `accepted`, `ready_submission`, `on_hold` | Status → `withdrawn`, records previous status, sends withdrawal email | | `action_adp_mark_rejected` | `submitted`, `resubmitted`, `accepted` | Status → `rejected` | | `action_adp_mark_denied` | `submitted`, `resubmitted`, `accepted`, `approved`, `approved_deduction` | Status → `denied` | | `action_adp_mark_needs_correction` | `submitted`, `resubmitted`, `accepted`, `approved`, `approved_deduction`, `rejected` | Status → `needs_correction` (clears submission docs, see §4.9) | | `action_adp_cancel` | Anything except `case_closed`, `cancelled`, `expired`, `billed` | Status → `cancelled`, records previous status | | `action_adp_reopen_cancelled` | `cancelled` AND `x_fc_cancel_reported_to_adp=False` | Restores to previous status. If `x_fc_cancel_reported_to_adp=True`, REJECTS and points to `duplicate_for_reassessment`. | | `action_adp_reopen_expired` | `expired` | **Back-compat shim** — just calls `action_adp_duplicate_for_reassessment` (per 2026-04 policy: expired cases cannot be self-renewed) | | `action_adp_duplicate_for_reassessment` | `expired`, `cancelled` | Creates a new SO with `x_fc_previous_sale_order_id` pointing here; new order starts at `quotation` | | `action_adp_resubmit_from_denied` | `denied` | Status → `ready_submission` for fresh attempt | `_adp_chatter_transition(title, icon, colour_class, details)` is the shared helper for posting Bootstrap-styled cards (`alert-warning` / `alert-danger` / `alert-secondary` / `alert-info` / etc.). ### 4.11 Cron-driven transitions - `_cron_adp_expire_approved` (3 AM daily): approved orders 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): `billed` → `case_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: ```python '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 sale** — `order._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): - **PCT** — `effective_adp_pct = base_adp_pct × (deduction_value / 100)`; client gets the rest. - **AMT** — subtract `deduction_value` (per unit) from the base ADP portion (floored at 0); client gets the rest. ### 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_ids` → `invoice_line_ids` mapping. Loop prevention: pass `with_context(skip_sync=True)` on the writes. Mark `is_manually_modified = True` on an invoice to opt it out of the SO → invoice sync direction (the model honours this flag in `_sync_fields_to_invoices`). **`_sync_fields_to_invoices` body** (`models/sale_order.py:8067-8145`): 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 suffix** — `S29958 (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 `_archived.` 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 `{label} uploaded by {user}` 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_ids` — `fusion_claims.mod.funding.approved.wizard.line` transient lines) updates via `_onchange_compute_preview` as the user types `approved_amount`. Both invoices flow through `sale.order._create_mod_invoice(partner_id, invoice_lines, portion_type, label)`. **`portion_type='adp'` for the MOD partial invoice**: the wizard reuses the `x_fc_adp_invoice_portion` field (set to `'adp'`) for the MOD invoice as well as the ADP one — so `x_fc_adp_invoice_portion == 'adp'` does NOT mean ADP. Always check `x_fc_invoice_type` (`'march_of_dimes'` vs `'adp'`) when disambiguating. ## 8. ADP billing lifecycle & posting schedule ### 8.1 Posting schedule mixin `fusion_claims.adp.posting.schedule.mixin` (`models/adp_posting_schedule.py`) is inherited by `sale.order`, `account.move`, and `fusion_claims.adp.export.record`. ``` posting cycle = every `fusion_claims.adp_posting_frequency_days` days (default 14) starting from `fusion_claims.adp_posting_base_date` (default 2026-01-23) submission deadline = Wednesday 6 PM of the posting week delivery reminder = Tuesday of the posting week billing reminder = Monday of the posting week payment processed = posting day + 7 payment received = posting day + 10 ``` Methods: `_get_next_posting_date`, `_get_current_posting_date`, `_get_posting_week_monday/tuesday/wednesday`, `_get_expected_payment_date`, `_get_payment_processed_date`, `_is_past_submission_deadline` (checks past 6 PM Wed), `_get_adp_billing_reminder_user`, `_get_adp_correction_reminder_users`. ### 8.2 Billing status (post-export) `x_fc_adp_billing_status` on `account.move`: ``` not_applicable → waiting → submitted → resubmitted / need_correction → payment_issued / cancelled ``` Cron `_cron_renew_billing_reminders` (`account.move`) reschedules overdue `mail_activity_type_adp_billing` activities on `waiting` invoices to the next posting week's Monday. `_cron_renew_correction_reminders` does the same for `need_correction` against `mail_activity_type_adp_correction` activities (scheduled for **all** users in `fusion_claims.adp_correction_reminder_user_ids`). Auto-write hook: when `payment_state` transitions to `paid` / `in_payment` on an ADP invoice currently `submitted` / `resubmitted` / `waiting`, the billing status flips to `payment_issued` automatically (`account.move.write` override at `models/account_move.py:884-892`). ## 9. ADP claim export `wizard/adp_export_wizard.py` (`fusion_claims.export.wizard`): - Pulls invoices via `active_ids`, filters 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.Cell1–5`); POD (received_by, date); Note to ADP section markers (boolean per section: `section1`, `section2a–d`, `section3and4`) plus vendor replacement / custom / funding chart / letter free-text notes. ## 12. AI integration ### 12.1 Native AI agent (Odoo 19 `ai` module) `data/ai_agent_data.xml`: - 3 `ir.actions.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_delivery` → `delivered`; rental pickup → write inspection back to SO and refund security deposit (if passed) or schedule activity (if flagged). - `_on_cancel_extra` — delivery cancellation reverts SO 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. ### 14.1 Page 11 (client consent) `fusion.page11.sign.request` (`models/page11_sign_request.py`): - Standalone signing request: random `access_token` (UUID4), public URL `/page11/sign/`, `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_ready` → `submitted_to_odsp`: | Action | What it does | |---|---| | `action_send_email` | Emails the ODSP office (`x_fc_odsp_office_id`) with both attachments. Uses `_send_odsp_submission_email` with `email_body_notes`. | | `action_send_fax` | Opens `fusion_faxes.send.fax.wizard` (soft-depended on) with attachments pre-loaded and the office's `x_ff_fax_number` if available. | | `action_send_fax_and_email` | Sends email first, then chains into the fax wizard via `display_notification.next`. | The wizard can override `x_fc_odsp_office_id` per-submission and syncs the choice back to the SO. ### 14.4.1 ODSP submission document bundling Two methods on `sale.order` build the email payload sent to the funder: | Method | Used by | Attaches | |---|---|---| | `_sa_mobility_submit_documents` | SA Mobility flow | (1) signed SA form (`x_fc_sa_signed_form` OR `x_fc_sa_physical_signed_copy`), (2) internal POD PDF via `_get_sa_pod_pdf`, (3) latest **posted** invoice PDF (Odoo's `account.account_invoices` report) | | `_odsp_std_submit_documents` | ODSP Standard flow | (1) `x_fc_odsp_approval_document`, (2) internal POD PDF, (3) **auto-confirms SO and creates invoice** if no posted one exists, then attaches invoice PDF | Both call the corresponding `_send_*_email` method (SA Mobility → `_send_sa_mobility_completion_email`; Standard → `_send_odsp_submission_email`). Both post a green chatter card listing the attachment names. ### 14.4.2 Ontario Works invoice flow `_ow_payment_create_invoice` (`action_odsp_payment_received` for OW orders): 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: ```python _adp_send_stage_email(title, subject, summary, note, note_color='#4f8cff', email_type='info', include_authorizer=True) ``` Defaults to client + authorizer + sales rep. `include_authorizer=False` is the one knob — used by `_send_accepted_email` and explained in the docstring with reference to the 2026-04 audit rule. ### 16.4.3 `_send_withdrawal_email` three-intent dispatch Takes `reason` (free text) and `intent` arguments: | `intent` | Title / subject | Note | Color | |---|---|---|---| | `'cancel'` | "Application Withdrawn & Cancelled" | "Permanently withdrawn and cancelled. SO and invoices cancelled." | `#dc3545` urgent | | `'resubmit'` | "Application Withdrawn for Correction" | "Will be resubmitted. Back to Ready for Submission." | `#d69e2e` attention | | (None) | "Application Withdrawn" | "Withdrawn from ADP." | `#d69e2e` attention | `action_adp_withdraw` calls with `intent='cancel'` by default; the `status_change_reason_wizard` exposes both intents. ### 16.5.1 Three footer voices The module branches the footer line of every transactional email by funder type, via two thin wrappers around `_email_build`: | Helper | Footer reads | |---|---| | `_email_build` (default) | "This is an automated notification from the **ADP Claims Management System**." | | `_mod_email_build` | "This is an automated notification from the **Accessibility Case Management System**." | | `_odsp_email_build` | "This is an automated notification from the **ODSP Case Management System**." | Both wrappers do a literal string replace on the result of `_email_build`. If you change the master footer text, update the two replace calls or the branching will silently break. ### 16.6 mail.template records (`data/mail_template_data.xml`) Three templates, all with `auto_delete=True` and a report attached: - `email_template_adp_quotation` — sale.order, landscape report. - `email_template_adp_sales_order` — confirmation, landscape report. - `email_template_adp_invoice` — account.move, landscape report. `account.move.action_invoice_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 | `billed` → `case_closed` 30 days after `x_fc_billing_date`. Posts chatter with `days_since_billed` | | Auto-Close ODSP Paid Cases | daily | SA Mobility / ODSP Standard: 7 days after `payment_received` → `case_closed`. **Ontario Works: 7 days after `delivered`** (payment comes BEFORE delivery for OW) | | First Application Reminder | 9 AM daily | `_cron_send_application_reminders` — `assessment_end_date == today - X` (default 4), status in `waiting_for_application`/`assessment_completed`, `x_fc_application_reminder_sent=False`. Sends to authorizer + sales rep + office | | Second Application Reminder | 9 AM daily | `_cron_send_application_reminders_2` — `assessment_end_date == today - (X+Y)`, first reminder sent, second not yet. Mentions 90-day assessment validity | | Acceptance Reminders | 9 AM daily | `_cron_send_acceptance_reminders` — see §18.1 below | | MOD Follow-up Scheduler | 8 AM daily | Bi-weekly MOD activity creation (capped). See §5.3 | | MOD Follow-up Escalation | 10 AM daily | Auto-email after activity 3 days overdue. See §5.3 | | MOD Handoff Follow-up | 9 AM daily | Office call activity for client/authorizer MOD paths. See §5.3 | | Expire Page 11 Signing Requests | 2 AM daily | Mark unsigned past-expiry requests `expired` | | ADP Expire Approved Applications | 3 AM daily | `approved` → `expired` 12 months later | | ADP Hold Expiry Reminders | 9:30 AM daily | Monthly client reminder + 30-day final warning | ### 18.1 `_cron_send_acceptance_reminders` mechanics Sends reminders for orders still `submitted` next business day after submission. Three layers of anti-spam protection (2026-04 update): | Layer | Setting | Effect | |---|---|---| | 14-day backlog guard | `fusion_claims.acceptance_reminder_max_age_days` (default 14) | Only reminds cases submitted **within the last 14 days** — first cron run after deploy doesn't blast every old stuck `submitted` case | | 2-day lower bound | hard-coded | `cutoff_date = today - 2 days` — skips weekend-only delays | | Per-run cap | `fusion_claims.acceptance_reminder_max_per_cron_run` (default 10) | Spreads large backlogs across multiple runs | | One-shot flag | `x_fc_acceptance_reminder_sent` | Each order gets at most ONE reminder per submission cycle. Reset to False on resubmission (see §4.9) | **FIRST/SECOND tier escalation**: - `days_since_submission ≤ 3` → "FIRST" reminder → **office 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 `