3107 lines
246 KiB
Markdown
3107 lines
246 KiB
Markdown
# 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/<token>` URL that the Page 11 wizard generates is handled by `fusion_authorizer_portal.controllers.portal_page11_sign` — without it the public signing flow is dead.
|
||
- `page11_sign_request._generate_signed_pdf` references `fusion.assessment` records — that model also lives in fusion_authorizer_portal.
|
||
|
||
In practice both modules are always installed together. See §29 for the full integration map.
|
||
|
||
### External Python (`__manifest__.py:89-91`)
|
||
|
||
- **`pdf2image`**, **`PIL`** — required (manifest declares).
|
||
- **`pdfrw`** — optional, used by `wizard/odsp_sa_mobility_wizard.py` to fill the SA Mobility government PDF form. Module logs a warning and disables that wizard if missing.
|
||
- **`requests`** — used implicitly by AI calls (`models/client_chat.py`) and SMS (`_twilio_send_sms`). Inherited from base Odoo deps.
|
||
|
||
### Post-init hook (`__init__.py:14-54`)
|
||
|
||
`_load_adp_device_codes` runs on install **and** every upgrade. Two idempotent steps:
|
||
|
||
1. `fusion.adp.device.code._load_packaged_device_codes()` imports `data/device_codes/adp_mobility_manual.json` (~hundreds of records) via `import_from_json`.
|
||
2. `_link_products_to_device_codes()` runs two SQL `UPDATE` statements: one links `product_template.x_fc_adp_device_code_id` for products that already have `x_fc_adp_device_code` set and matches a device code, and one toggles `x_fc_is_adp_product = TRUE` for products with a code but no flag. Both are guarded by `IS NULL` checks — preserve idempotence if you edit them.
|
||
|
||
## 3. Funder workflows (the architecture)
|
||
|
||
`x_fc_sale_type` on `sale.order` (`models/sale_order.py:320-339`) selects one of:
|
||
|
||
```
|
||
adp, adp_odsp, odsp, wsib, direct_private, insurance,
|
||
march_of_dimes, muscular_dystrophy, other, rental, hardship
|
||
```
|
||
|
||
Once any non-quotation status is set on a funder workflow, `x_fc_sale_type_locked` becomes True (`models/sale_order.py:347-371`) and the dropdown is read-only — override via setting `fusion_claims.allow_sale_type_override`.
|
||
|
||
Each funder has its own status field, wizards, kanban columns, emails, and (in some cases) submission helpers:
|
||
|
||
| Funder | Status field | Module is_* flag | show_* flag | Key wizards |
|
||
|---|---|---|---|---|
|
||
| **ADP** (Assistive Devices Program) | `x_fc_adp_application_status` (22 states) | `x_fc_is_adp_sale` | implicit | schedule_assessment, assessment_completed, application_received, ready_for_submission, submission_verification, device_approval, ready_for_delivery, ready_to_bill, case_close_verification, status_change_reason, send_page11 |
|
||
| **MOD** (March of Dimes HVMP) | `x_fc_mod_status` (14 states) | `x_fc_is_mod_sale` | `x_fc_show_mod_fields` | mod_submission_path, send_to_mod (drawing/quotation/POD), mod_awaiting_funding, mod_funding_approved, mod_funding_denied, mod_pca_received, mod_resubmit, mod_submission_confirmed |
|
||
| **ODSP Standard** | `x_fc_odsp_std_status` | `x_fc_is_odsp_sale` | `x_fc_show_odsp_fields` | odsp_submit_to_odsp, odsp_pre_approved, odsp_ready_delivery |
|
||
| **ODSP SA Mobility** | `x_fc_sa_status` | (subset of ODSP) | | odsp_sa_mobility (fills gov PDF via pdfrw) |
|
||
| **ODSP Ontario Works** | `x_fc_ow_status` | (subset of ODSP) | | odsp_discretionary |
|
||
| **WSIB** | `x_fc_wsib_status` | `x_fc_is_wsib_sale` | `x_fc_show_wsib_fields` | (generic funder transitions) |
|
||
| **Insurance** | `x_fc_insurance_status` | `x_fc_is_insurance_sale` | `x_fc_show_insurance_fields` | (generic funder transitions) |
|
||
| **MDC** (Muscular Dystrophy) | `x_fc_mdc_status` | `x_fc_is_mdc_sale` | `x_fc_show_mdc_fields` | (generic funder transitions) |
|
||
| **Hardship** | `x_fc_hardship_status` | `x_fc_is_hardship_sale` | `x_fc_show_hardship_fields` | (generic funder transitions) |
|
||
| Direct/Private, Other, Rental | — | — | — | invoiced directly, no funder lifecycle |
|
||
|
||
`x_fc_odsp_division` distinguishes the three ODSP sub-workflows; `_get_odsp_status()` returns whichever of `x_fc_sa_status`/`x_fc_odsp_std_status`/`x_fc_ow_status` is active.
|
||
|
||
## 4. The ADP workflow (the spine)
|
||
|
||
### 4.1 Status field
|
||
|
||
`x_fc_adp_application_status` (`models/sale_order.py:2302-2333`) — 22 states. Workflow sequence enforced by `_STATUS_ORDER` (`models/sale_order.py:2361-2384`) which drives the kanban column order (`_read_group` override at line 2393).
|
||
|
||
```
|
||
quotation → assessment_scheduled → assessment_completed → waiting_for_application →
|
||
application_received → ready_submission → submitted →
|
||
accepted (within 24h) / rejected → resubmitted →
|
||
needs_correction → (corrected then back to submitted/resubmitted) →
|
||
approved / approved_deduction → ready_delivery → ready_bill → billed → case_closed
|
||
|
||
Special branches:
|
||
on_hold (any time) ←→ resume_from_hold (back to previous status)
|
||
withdrawn → resubmit_from_withdrawn (back to ready_submission)
|
||
denied → resubmit_from_denied (back to ready_submission)
|
||
cancelled → reopen (only if not reported to ADP)
|
||
expired (12 months after approved with no delivery) → reopen / duplicate_for_reassessment
|
||
```
|
||
|
||
There is also a **legacy** `x_fc_adp_status` field (7-state, simpler) — keep it in mind but do NOT use it for new logic.
|
||
|
||
### 4.2 Status transitions are NEVER set via dropdown
|
||
|
||
Every controlled status (every transition that should fire an email, write to chatter, or update related records) lives on a button that opens a dedicated wizard. `static/src/js/status_selection_filter.js` registers a `filtered_status_selection` field that **hides** controlled statuses from the dropdown:
|
||
|
||
```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/<token>` |
|
||
|
||
`x_fc_has_signed_pages_11_12` is a **computed boolean** that returns True if ANY of the three conditions hold — DO NOT check `x_fc_signed_pages_11_12` directly to gate workflow steps, that misses bundled and remote modes. (`models/sale_order.py:2921-2942`.)
|
||
|
||
The wizard does **two layers of PDF validation**:
|
||
1. Filename constraint — must end in `.pdf` (case-insensitive).
|
||
2. **Magic-byte check** — base64-decoded payload must start with `%PDF-`. Test fixtures use `b'%PDF-1.4\n%fake pdf for tests'` to pass.
|
||
|
||
`default_get` picks the initial mode based on existing state: bundled flag set → bundled; separate file present → separate; pending sign request → remote; otherwise bundled.
|
||
|
||
The wizard has an inline button `action_request_page11_signature` that opens `send_page11_wizard` without leaving the parent wizard.
|
||
|
||
### 4.5 Document locks (separate from `x_fc_case_locked`)
|
||
|
||
`sale.order.write` enforces document-level locks based on workflow status (`models/sale_order.py:7289-7382`):
|
||
|
||
| Document field(s) | Locked when status ≥ |
|
||
|---|---|
|
||
| `x_fc_original_application`, `x_fc_signed_pages_11_12` (+ filenames) | `submitted` |
|
||
| `x_fc_final_submitted_application`, `x_fc_xml_file` (+ filenames) | `approved` |
|
||
| `x_fc_approval_letter` (+ filename) | `billed` |
|
||
| `x_fc_proof_of_delivery` (+ filename) | `billed` |
|
||
|
||
Bypass requires **both**: setting `fusion_claims.allow_document_lock_override = True` AND user in `group_document_lock_override`. Context flag `skip_document_lock_validation=True` bypasses for programmatic writes only.
|
||
|
||
### 4.6 Case-wide lock (`x_fc_case_locked`)
|
||
|
||
Distinct from the per-document status locks above — this is a **manual** lock toggled via the "Case Locked" switch in the ADP Order Trail tab. When True, the `write` override blocks **all** `x_fc_*` field writes except:
|
||
|
||
- `x_fc_case_locked` (so you can untoggle it)
|
||
- `message_main_attachment_id`, `message_follower_ids`, `activity_ids` (Odoo plumbing)
|
||
|
||
Used for archiving completed legacy cases. Bypass with `with_context(skip_all_validations=True)` (used by crons/email-tracking).
|
||
|
||
### 4.7 Document audit trail in chatter
|
||
|
||
The same `write` override (`models/sale_order.py:7384-7430`) preserves the OLD copy of any replaced document (`x_fc_original_application`, `x_fc_signed_pages_11_12`, `x_fc_final_submitted_application`, `x_fc_xml_file`, `x_fc_proof_of_delivery`, `x_fc_approval_letter`) in chatter **before** it gets overwritten. Set `with_context(skip_document_chatter=True)` to suppress.
|
||
|
||
### 4.8 `reason_for_application` field (12 values)
|
||
|
||
`x_fc_reason_for_application` controls invoicing rules and required fields:
|
||
|
||
```
|
||
first_access — First Time Access (NO previous ADP)
|
||
additions — Additions
|
||
mod_non_adp — Modification/Upgrade — original NOT through ADP
|
||
mod_adp — Modification/Upgrade — original through ADP
|
||
replace_status — Replacement — Change in Status
|
||
replace_size — Replacement — Change in Body Size
|
||
replace_worn — Replacement — Worn out (past useful life)
|
||
replace_lost — Replacement — Lost
|
||
replace_stolen — Replacement — Stolen
|
||
replace_damaged — Replacement — Damaged beyond repair
|
||
replace_no_longer_meets — Replacement — No longer meets needs
|
||
growth — Growth/Change in condition
|
||
```
|
||
|
||
Rules:
|
||
- `previous_funding_date` is **required** for all reasons except `first_access` and `mod_non_adp`.
|
||
- `<5 years` warning: `x_fc_under_5_years` (computed from `previous_funding_date`) — if True and reason ∈ {`replace_status`, `replace_size`, `replace_worn`}, posts a chatter warning when creating client invoice. (Surfaces possible ADP deductions.)
|
||
- **Modification reasons** (`mod_non_adp`, `mod_adp`) **block** client invoice creation until status reaches `approved` or `approved_deduction` — surfaced as a `danger`-styled sticky notification.
|
||
|
||
### 4.9 Status-driven side effects in `sale.order.write`
|
||
|
||
The 800-line `write` override (`models/sale_order.py:7225-8023`) does much more than save fields. When ADP status changes, it:
|
||
|
||
**Auto-populates dates** (only if not already in `vals`):
|
||
| Status target | Date field auto-set |
|
||
|---|---|
|
||
| `assessment_scheduled` | `x_fc_assessment_start_date` = today |
|
||
| `assessment_completed` | `x_fc_assessment_end_date` = today, then auto-advances to `waiting_for_application` |
|
||
| `submitted` / `resubmitted` | `x_fc_claim_submission_date` = today |
|
||
| `accepted` | `x_fc_claim_acceptance_date` = today |
|
||
| `approved` / `approved_deduction` | `x_fc_claim_approval_date` = today |
|
||
| `billed` | `x_fc_billing_date` = today |
|
||
|
||
**Required-field gates** (raise `UserError` if missing — also enforced by the dedicated wizards, but this is the second safety net):
|
||
|
||
| Status target | Required fields |
|
||
|---|---|
|
||
| `assessment_completed` | assessment_start_date, assessment_end_date |
|
||
| `application_received` | assessment_start_date |
|
||
| `ready_submission` | assessment dates, reason_for_application, client_ref_1, client_ref_2, claim_authorization_date, previous_funding_date (if reason needs it), original_application, **`x_fc_signed_pages_11_12`** — NOTE: this gate uses the raw field, NOT `x_fc_has_signed_pages_11_12`. The application_received_wizard sidesteps this by populating one of the three computed-sources, but a direct write may fail; see gotcha #21 below. |
|
||
| `submitted` / `resubmitted` | final_submitted_application, xml_file, claim_submission_date |
|
||
| `approved` / `approved_deduction` | claim_number, claim_approval_date |
|
||
| `ready_bill` | adp_delivery_date, proof_of_delivery |
|
||
| `billed` | billing_date |
|
||
| `case_closed` | billing_date |
|
||
| MOD `contract_received` | x_fc_case_reference (HVMP Reference Number) |
|
||
| MOD `pod_submitted` | x_fc_mod_proof_of_delivery |
|
||
|
||
**Authorizer required-field gate** (only fires when authorizer-related fields are in vals: `x_fc_sale_type`, `x_fc_authorizer_id`, `x_fc_authorizer_required`, `x_fc_adp_application_status`):
|
||
- Always required for: `adp`, `adp_odsp`, `wsib`, `march_of_dimes`, `muscular_dystrophy`
|
||
- Optional based on `x_fc_authorizer_required='yes'` for: `odsp`, `direct_private`, `insurance`, `other`
|
||
- Never required for: `rental`
|
||
|
||
**Resume from on_hold checks 3-month assessment validity** — if `x_fc_assessment_expired` is True (>90 days since `x_fc_assessment_end_date`), blocks resume with `UserError` and a chatter warning showing days past expiry. The OT must redo the assessment.
|
||
|
||
**`needs_correction` document clearing** — when status changes to `needs_correction`, the override:
|
||
1. Posts the existing `x_fc_final_submitted_application` and `x_fc_xml_file` to chatter for preservation.
|
||
2. **Clears** these fields plus `x_fc_final_application_filename`, `x_fc_xml_filename`, `x_fc_claim_submission_date`.
|
||
3. Posts a yellow warning notice.
|
||
|
||
**Submission history auto-creation** — on `submitted` / `resubmitted`, creates a `fusion.submission.history` record (type `initial` or `resubmission`) and **resets `x_fc_acceptance_reminder_sent`** so the acceptance reminder fires again for the new submission cycle (2026-04 anti-spam fix).
|
||
|
||
**Submission history result update** — on `accepted` or `rejected`, finds the most recent pending submission record (by date desc, limit 1) and calls `update_result()` to mark it.
|
||
|
||
**MOD follow-up counter reset** — on ANY real MOD status change (detected via pre-write snapshot `old_mod_status_by_id`), resets `x_fc_mod_followup_month_count`, `_month_start`, `_escalated`, `_cap_notified`. This is the "new chapter" reset that makes the rolling cap work correctly.
|
||
|
||
**MOD auto-stamp 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 `<name>_archived.<ext>` copy. Used when the original is about to be deleted/replaced (e.g., needs_correction clearing) and we need to snapshot before Odoo's attachment is removed.
|
||
- Posts a `<strong>{label}</strong> uploaded by <b>{user}</b>` chatter message with the attachment attached.
|
||
|
||
Companion utilities: `_build_attachment_name(field_name)` builds the user-facing filename, `_get_document_attachment(field_name)` resolves the existing attachment, `_prepare_attachment_for_email(attachment, field_name)` renames it for outbound mail.
|
||
|
||
### 7.9 MOD action method index
|
||
|
||
All on `sale.order`, all in `models/sale_order.py:8594-8901`:
|
||
|
||
| Method | What it does | Status target |
|
||
|---|---|---|
|
||
| `action_mod_schedule_assessment` | bare write | `assessment_scheduled`, stamps `x_fc_mod_assessment_scheduled_date` |
|
||
| `action_mod_complete_assessment` | bare write | `assessment_completed`, stamps `x_fc_mod_assessment_completed_date` |
|
||
| `action_mod_processing_drawing` | writes `processing_drawings`, then opens `send_to_mod_wizard` in `mod_wizard_mode='drawing'` | progresses to `quote_submitted` via the wizard |
|
||
| `action_mod_awaiting_funding` | opens `mod_awaiting_funding_wizard` | `awaiting_funding` |
|
||
| `action_mod_funding_approved` | opens `mod_funding_approved_wizard` (records case worker + HVMP ref) | `funding_approved` |
|
||
| `action_mod_funding_denied` | opens `mod_funding_denied_wizard` (category + reason) — 2026-04 was bare write, now captures denial reason | `funding_denied` |
|
||
| `action_mod_contract_received` | opens `mod_pca_received_wizard` (PCA upload + full/partial invoice split) | `contract_received` |
|
||
| `action_mod_in_production` | bare write | `in_production`, stamps `x_fc_mod_production_started_date` |
|
||
| `action_mod_project_complete` | bare write | `project_complete`, stamps `x_fc_mod_project_completed_date` |
|
||
| `action_mod_pod_submitted` | opens `send_to_mod_wizard` in `mod_wizard_mode='completion'` | `pod_submitted` via the wizard |
|
||
| `action_mod_close_case` | bare write | `case_closed`, stamps `x_fc_mod_case_closed_date` |
|
||
| `action_mod_on_hold` | saves previous status into `x_fc_mod_previous_status_before_hold` (2026-04 fix — was being lost) | `on_hold` |
|
||
| `action_mod_resume` | restores from `x_fc_mod_previous_status_before_hold` (default `in_production`) — 2026-04 fix — was hardcoded to `in_production` | previous status |
|
||
| `action_mod_set_submission_path` | opens `mod_submission_path_wizard` (internal/client/authorizer) | n/a — sets `x_fc_mod_submitted_by` |
|
||
| `action_mod_request_vod` | emails authorizer the blank VOD form (also auto-fired when internal path first selected) | n/a |
|
||
| `action_mod_handoff_to_client` | only when `submitted_by ∈ (client, authorizer)`; requires `proposal_doc` + `drawing` | `handoff_to_client`, stamps `x_fc_mod_handoff_date` |
|
||
| `action_mod_confirmed_submission` | opens `mod_submission_confirmed_wizard` | confirms client/authorizer submitted |
|
||
| `action_mod_resubmit_from_denied` | opens `mod_resubmit_wizard`; only from `funding_denied` | back to earlier status via wizard |
|
||
| `action_mod_cancel_from_denied` | only from `funding_denied` | `cancelled` |
|
||
| `action_mod_reopen_cancelled` | only from `cancelled` | `need_to_schedule`, clears `x_fc_mod_funding_denial_reason` |
|
||
| `action_cancel` (override on `sale.order`) | when the SO is cancelled (built-in), also force-sets `x_fc_mod_status='cancelled'` for MOD orders | `cancelled` |
|
||
|
||
`_get_mod_partner()` — finds or **creates** the MOD partner by email `fusion_claims.mod_default_email` (default `hvmp@marchofdimes.ca`). New record is created with name `'March of Dimes Canada (HVMP)'`, `is_company=True`.
|
||
|
||
`_create_mod_invoice(partner_id, invoice_lines, portion_type, label)` — the shared MOD invoice creator. Sets `x_fc_invoice_type='march_of_dimes'`, `x_fc_adp_invoice_portion=portion_type`, populates `narration` with HVMP reference + client + case worker + SO + vendor code as an HTML block.
|
||
|
||
### 7.10 Stage 2 invoice sync (`_sync_approval_to_invoices`) — re-posts posted invoices
|
||
|
||
When the device approval wizard's `mark_as_approved` mode runs `_sync_approval_to_invoices(updated_lines)`, the method walks every non-cancelled invoice linked to the order and rewrites line `price_unit` + suffixes the line name based on the new approval state:
|
||
|
||
| State | Client invoice line | ADP invoice line |
|
||
|---|---|---|
|
||
| Non-ADP funded item (code in NON-ADP/NON-FUNDED/etc.) | `price_unit = full subtotal`, name unchanged | `price_unit = 0`, name suffixed `[NON-ADP - Excluded]` |
|
||
| Unapproved ADP device | `price_unit = full subtotal`, name suffixed `[NOT APPROVED - 100% Client]` | `price_unit = 0`, name suffixed `[NOT APPROVED - Excluded]` |
|
||
| Approved ADP device | `price_unit = x_fc_client_portion / qty`, name unchanged | `price_unit = x_fc_adp_portion / qty`, name unchanged |
|
||
|
||
For **POSTED invoices**, the method calls `invoice.button_draft() → write → action_post()` — i.e. it resets to draft, rewrites the lines, then re-posts. If the posted invoice has already been exported to ADP or emailed to the client, **the reset-then-repost cycle can break sequence numbers, re-fire post-actions, or invalidate exports**. Set `is_manually_modified=True` on the invoice to opt it out of this sync direction if you need to lock it.
|
||
|
||
The wizard reports the count of `invoices_updated` in the success notification.
|
||
|
||
### 7.11 Activity scheduling pattern (`_schedule_or_renew_adp_activity`)
|
||
|
||
On `account.move`, the helper `_schedule_or_renew_adp_activity(activity_type_xmlid, user_id, date_deadline, summary, note)` is the shared pattern for ADP billing/correction activities:
|
||
|
||
- **Finds existing** activity of the same type AND same user_id on the record.
|
||
- **If found**: UPDATES `date_deadline`, `summary`, `note` (preserves existing note if new is blank).
|
||
- **If not found**: creates new via `activity_schedule`.
|
||
|
||
Companion `_complete_adp_activities(activity_type_xmlid)` calls `activity.action_feedback(feedback='Completed automatically')` on every activity of the given type — used to auto-close ADP Billing activities when the invoice flips to `submitted`/`payment_issued`, and ADP Correction activities when flipped to `resubmitted`/`payment_issued`.
|
||
|
||
This dedup pattern shows up in three places: ADP billing reminders, ADP correction reminders, and MOD follow-ups — different fields, same logic.
|
||
|
||
### 7.12 MOD PCA dual-invoice split (`wizard/mod_pca_received_wizard.py`)
|
||
|
||
When `contract_received` is set via this wizard, the user picks `approval_type`:
|
||
|
||
- **`full`** — creates ONE invoice (to the MOD partner) for the full order amount; client owes nothing.
|
||
- **`partial`** — creates TWO invoices simultaneously:
|
||
- **MOD invoice**: each line × `(approved_amount / order.amount_untaxed)` = MOD's share.
|
||
- **Client invoice**: each line × `(1 - ratio)` = client's share. If the client's per-line amount is ≤ 0 (fully covered), the line name is suffixed `\n[Covered by March of Dimes]` and `price_unit=0`.
|
||
|
||
Live preview (`preview_line_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/<token>`, `state` of `draft / sent / signed / expired / cancelled`.
|
||
- Signer can be client, spouse, parent, legal_guardian, power_of_attorney, public_trustee. Agent details (full address) captured when not the client.
|
||
- `consent_signed_by`: `applicant` (client signs themselves) or `agent` (anyone else). `send_page11_wizard` auto-sets this based on `signer_type`.
|
||
- Public security ACL: `base.group_public` has read-only access to `fusion.page11.sign.request` so the public sign page can resolve the token.
|
||
- `_generate_signed_pdf` uses `fusion.pdf.template` (from another module; the active template is named `adp_page_11` or `page 11`) to render a filled PDF, then writes the result to `x_fc_signed_pages_11_12` on the SO + creates an `ir.attachment`.
|
||
- `send_page11_wizard` opens the composer. Default expiry 7 days. Pre-fills signer from `partner_id.name` + `partner_id.email`. Signer relationship auto-fills from the signer_type label (spouse → "Spouse", etc.).
|
||
- Form view header buttons: Resend Email (sent/expired), Request New Signature (signed/cancelled, with confirm dialog), Cancel (draft/sent). Statusbar: draft → sent → signed.
|
||
- Cron `_cron_expire_requests` (2 AM daily) marks expired unsigned requests.
|
||
|
||
### 14.2 Page 12 (authorizer + vendor)
|
||
|
||
Tracked directly on the SO via two booleans + signer fields:
|
||
- `x_fc_page12_authorizer_signed` — OT signs after page 11 is received.
|
||
- `x_fc_page12_vendor_signed` + `x_fc_page12_vendor_signer_id` — designated vendor signer (`fusion_claims.designated_vendor_signer` setting) signs on the company's behalf.
|
||
|
||
### 14.3 SA Mobility + OW Discretionary signing
|
||
|
||
Two government-issued PDFs are filled by Python (using `pdfrw`):
|
||
|
||
| Form | Template | Wizard |
|
||
|---|---|---|
|
||
| **SA Mobility** form (ODSP SA division) | `static/src/pdf/sa_mobility_form_template.pdf` (482 KB) | `odsp_sa_mobility_wizard` (560 lines) |
|
||
| **OW Discretionary Benefits** form | `static/src/pdf/discretionary_benefits_form_template.pdf` (1.1 MB) | `odsp_discretionary_wizard` (395 lines) |
|
||
|
||
The SA Mobility wizard's `_build_field_mapping()` is the **field-name reference for the gov form 13007E**:
|
||
|
||
- Vendor section: `Text 1` (with space — gov form quirk), `Text2`, `Text3`, ..., `Text7`. Salesperson: `Text8`, `Text9`. Client: `Text10`, `Text11`.
|
||
- **Member ID is 9 separate text boxes** (`Text12`-`Text20`) — one per digit, left-justified via `.ljust(9)`.
|
||
- Relationship checkboxes: `Check Box16` (self), `17` (spouse), `18` (dependent).
|
||
- Device type checkboxes: `Check Box19` (manual_wheelchair) ... `Check Box24` (other).
|
||
- Parts table (up to **6 rows**): `Text30`-`Text59` in groups of 5 (qty, description, unit_price, taxes, amount). Total at `Text60`.
|
||
- Labour table (up to **5 rows**): `Text61`-`Text80` in groups of 4 (hours, rate, taxes, amount). Total at `Text81`.
|
||
- Additional Fees (up to **4 rows**): `Text82`-`Text97` in groups of 4 (description, rate, taxes, amount). Total at `Text98`.
|
||
- Estimated totals summary: `Text99` (parts), `Text100` (labour), `Text101` (fees), `Text102` (grand total).
|
||
- Page 2 Notes/Comments area: **`Text1`** (collides with vendor `Text 1` — different field, gov-form naming quirk).
|
||
|
||
The wizard pre-populates from the SO automatically: products with `default_code == 'LABOR'` → labour tab; everything else → parts tab.
|
||
|
||
**`_get_template_path()`** uses raw `os.path` operations to resolve the template file, NOT Odoo's `tools.misc.file_path()`. **Brittle if the module is loaded as a zip** — consider migrating to `file_path('fusion_claims/static/src/pdf/sa_mobility_form_template.pdf')` if that issue arises.
|
||
|
||
### 14.3.1 OW Discretionary Benefits wizard quirks
|
||
|
||
This wizard fills a different gov form (`discretionary_benefits_form_template.pdf`, 1.1 MB) with its own set of footguns:
|
||
|
||
- **Uses `PyPDF2`, not `pdfrw`** — because the gov PDF is AES-encrypted (no password, just "protected mode"). pdfrw cannot decrypt; PyPDF2 handles it via `reader.decrypt('')`. If pdfrw is the only PDF library available, the wizard will fail. Both are optional Python deps.
|
||
- Preserves `/AcroForm` from the original document and sets `/NeedAppearances` = True so the filled form renders correctly in Acrobat (without this, the values are stored but not visible).
|
||
- Splits text and checkbox fields into separate dicts. Text fields use PyPDF2's bulk `update_page_form_field_values`; **checkboxes are updated directly by mutating each annotation's `/V` and `/AS` to `/1` (checked) or `/Off` (unchecked)** via `NameObject`. PyPDF2 doesn't have a clean API for this.
|
||
- **The gov form has misleading field names** — they don't match physical layout. Hard-earned mapping:
|
||
| Field name (PDF) | Actually populates |
|
||
|---|---|
|
||
| `txt_First[0]` | Client Name |
|
||
| `txt_CITY[1]` | **Member ID** (not city — note the `[1]` index) |
|
||
| `txt_add[0]` | Address |
|
||
| `txt_CITY[0]` | City (the `[0]` index — different from `[1]`) |
|
||
| `txt_email[0]` | **Phone** (not email) |
|
||
| `txt_bphone[0]` | Alternate Phone |
|
||
| `txt_emp_phone[0]` | **Email** (not employer phone) |
|
||
| `txt_clientnumber[0]` | **Date** (not client number) |
|
||
| `CheckBox15[0]` | Medical Equipment |
|
||
| `CheckBox11[0]` | Dentures |
|
||
| `CheckBox11[1]` | Vision Care |
|
||
| `CheckBox13[0]` | Other |
|
||
| `TextField1[0]` | Description/details |
|
||
|
||
Don't normalize or rename when building the mapping — write to the names exactly as ODSP shipped them.
|
||
- 4 item types: `medical_equipment`, `vision_care`, `dentures`, `other`.
|
||
|
||
ODSP signing positions (signature/initial fields) are managed via Configuration > PDF Templates with a drag-and-drop visual editor — managed in `fusion.pdf.template` records (category=ODSP), not as static templates in `data/pdf_template_data.xml` (which is now an empty placeholder with a "templates retired" note).
|
||
|
||
`static/src/pdf/sa_mobility_page2_sample.pdf` (241 KB) is a reference sample showing what page 2 should look like when filled.
|
||
|
||
### 14.4 ODSP submission paths (`odsp_submit_to_odsp_wizard`)
|
||
|
||
Three actions on the wizard, all of which (a) render the standard `sale.action_report_saleorder` PDF, (b) attach `x_fc_odsp_authorizer_letter`, and (c) advance status `quotation`/`documents_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 `<style>` block; reads `primary` color var (set by caller) for `.fc-table th` background, totals row, etc. |
|
||
|
||
**Color convention** for all reports (2026-04): each report sets `primary` / `secondary` near the top from `company.primary_color` (default `#0066a1`) and `company.secondary_color` (default `#90be6d`), then references them inside the `<style>` block and inline `style=""` attributes via `<t t-out="primary"/>`.
|
||
|
||
### 19.2 Default Odoo report enhancements
|
||
|
||
`report_templates.xml` also **inherits the default `account.report_invoice_document` and `sale.report_saleorder_document`** to add a **SKU column** before Description AND strip `[internal_ref]` prefixes from line descriptions:
|
||
|
||
```python
|
||
# clean_desc logic
|
||
if '] ' in line.name:
|
||
clean_desc = line.name.split('] ', 1)[1]
|
||
else:
|
||
clean_desc = line.name
|
||
```
|
||
|
||
This makes the default Odoo invoice/SO reports show `[MXA-1618] GEOMATRIX SILVERBACK MAX BACKREST - ACTIVE` as separate `SKU: MXA-1618` + `Description: GEOMATRIX SILVERBACK MAX BACKREST - ACTIVE` columns instead of a single squashed line. Touches **default reports too**, not just the custom landscape/portrait ones — keep this in mind if the user reports default-report formatting differences.
|
||
|
||
Brand colours: every report opens with `<t t-set="primary" t-value="_co.primary_color or '#0066a1'"/>` and `<t t-set="secondary" t-value="_co.secondary_color or '#90be6d'"/>`. The fallback hexes preserve legacy rendering for databases that have never set company brand colours. Helper templates live in `report/report_templates.xml` (header + address boxes).
|
||
|
||
`report/report_saleorder_adp.xml` and `report/report_invoice_adp.xml` exist as empty stubs — do not delete (loaded by the manifest? — they're declared but currently empty).
|
||
|
||
## 20. Frontend assets
|
||
|
||
### 20.1 JS (`__manifest__.py:166-178`, registered into `web.assets_backend`)
|
||
|
||
| File | Purpose |
|
||
|---|---|
|
||
| `document_preview.js` | OWL component `DocumentPreviewDialog` — wraps Odoo's PDF.js viewer in a Dialog (xl / fullscreen toggle) |
|
||
| `preview_button_widget.js` | View widget `preview_button` — opens DocumentPreviewDialog from a button |
|
||
| `status_selection_filter.js` | Field `filtered_status_selection` — hides workflow-controlled statuses from the dropdown |
|
||
| `gallery_preview.js` | Patches `Many2ManyBinaryField` to use Odoo's native FileViewer in `.fc-gallery-content` sections (so clicking an attachment opens viewer instead of downloading) |
|
||
| `tax_totals_patch.js` | Defensively sets `totals.subtotals = []` when undefined — fixes "Invalid loop expression" crash on tax-less invoices |
|
||
| `google_address_autocomplete.js` | 1506-line address autocomplete widget |
|
||
| `calendar_store_hours.js` | Patches `CalendarRenderer` to clamp `fusion.technician.task` calendar to 8 AM–7 PM |
|
||
| `attachment_image_compress.js` | Patches `FileUploader.onFileChange` — compresses images > 500 KB to ≤1280px / JPEG 0.80 via Canvas BEFORE base64 conversion. Fixes iPhone Safari crash on 4+ photo upload |
|
||
| `debug_required_fields.js` | Patches `Record._displayInvalidFieldNotification` to log missing field labels + technical names to console |
|
||
|
||
### 20.2 SCSS
|
||
|
||
`static/src/scss/fusion_claims.scss` (771 lines): status button utility classes (`fc-btn-status-good` / `-bad` / `-neutral`), ADP/client portion column tints (`fc-adp-portion` / `fc-client-portion`), card hover effects, gallery section styling.
|
||
|
||
> **Dark mode caveat**: the existing SCSS uses `html.dark` / `.o_dark` selectors. The project-wide CLAUDE.md states this approach does **not** fire reliably in Odoo 19 — new SCSS in this module should branch on `$o-webclient-color-scheme` at compile time and be registered in both `web.assets_backend` and `web.assets_web_dark`.
|
||
|
||
### 20.3 OWL XML templates
|
||
|
||
`static/src/xml/document_preview.xml` (204 lines) — markup for `DocumentPreviewDialog`, `PreviewButtonWidget`.
|
||
`static/src/xml/fusion_task_map_view.xml` (250 lines) — referenced from `static/src/js/fusion_task_map_view.js` (1197 lines, not bundled by THIS module — declared by `fusion_tasks`).
|
||
|
||
### 20.4 SCSS detail (771 lines)
|
||
|
||
The stylesheet covers more than just status pills — five distinct sections:
|
||
|
||
| Section | What it does |
|
||
|---|---|
|
||
| Status button utilities | `.fc-btn-status-good` / `-bad` / `-neutral` — Bootstrap-compatible tinted action buttons with hover states |
|
||
| Status-pill dark mode | Three different dark-mode strategies (`html.dark`, `.o_dark`, AND `@media (prefers-color-scheme: dark)`) — see gotcha #54 below |
|
||
| **SOL list column widths** | `.o_field_one2many[name="order_line"] .o_list_table { table-layout: fixed }` with explicit pixel widths for every column (serial, qty, UoM, price, tax, discount, subtotal, ADP/Client portion, sale_margin columns). **Fragile** — required because Odoo 19 ignores the XML `width` attribute on list fields. If you rename a SOL field, you must update the matching `th[data-name="..."]` selector here. |
|
||
| Document tiles | `.fc-document-tiles` / `.fc-document-tile` — 220px-wide cards with PDF/XML icons, hover upload overlay, required-field star indicator, green border in `filled` state, yellow border for required-but-empty |
|
||
| Approval photos gallery | `.fc-gallery-section` / `.fc-gallery-content` — 80×80px thumbnails on the Many2Many binary widget. Hover scales 1.05× with blue border. Delete button hidden until hover. |
|
||
| LTC kanban data-attr styling | Uses CSS `:has(main[data-stage="info"])` to color-code outer kanban cards from inner `<main>` data attributes. Stages: `info`, `warning`, `success`, `danger`, `secondary`. Plus `data-priority="1"` (warm amber bottom accent) and `data-emergency="1"` (override red border). |
|
||
| Google Places fix | `.pac-container { z-index: 100000 }` — required so the autocomplete dropdown floats above Odoo modals |
|
||
| XML viewer styling | VS Code dark theme inspired colors (`.xml-tag`, `.xml-attr`, `.xml-value`) for XML preview |
|
||
| `.adp_file_preview` | Monospace code block for displaying raw ADP export `.txt` file content in the export record form |
|
||
|
||
### 20.5 Account move form inheritance (5 priorities)
|
||
|
||
`views/account_move_views.xml` inherits `account.view_move_form` five times with different priorities:
|
||
|
||
| Priority | id | What it adds |
|
||
|---|---|---|
|
||
| 49 | `view_move_form_fusion_claims_billing_status` | ADP billing-status statusbar in header (`waiting → submitted → payment_issued`, clickable). Plus "Send to Case Worker" button for MOD invoices |
|
||
| 50 | `view_move_form_fusion_claims_header` | Portion badge (floats top-right of title), invoice type field, authorizer-required field, authorizer field, client type field — all after `journal_div`, all with visibility expressions tied to `move_type` and `x_fc_show_*` flags |
|
||
| 51 | `view_move_form_fusion_claims_verification_alert` | Yellow alert banner above sheet when `x_fc_needs_device_verification` is True |
|
||
| 52 | `view_move_form_fusion_claims_button` | "Verify Device Approval" + "Export ADP" buttons in header. Export only visible for posted ADP-portion invoices. |
|
||
| 55 | `view_move_form_fusion_claims_adp_tab` | ADP Case Details notebook tab (claim info + dates + export status) |
|
||
| 60 | `view_move_form_fusion_claims_tab` | ADP Summary notebook tab (deduction alert + per-line table) |
|
||
|
||
## 21. Menus (app structure)
|
||
|
||
Root menu: **ADP Claims** (id `menu_adp_claims_root`).
|
||
|
||
```
|
||
ADP Claims
|
||
├── All Orders
|
||
│ ├── All Sales Orders
|
||
│ └── All Invoices
|
||
├── ADP
|
||
│ ├── All ADP Orders
|
||
│ ├── Billing
|
||
│ │ ├── ADP Invoices
|
||
│ │ ├── Client Invoices
|
||
│ │ ├── Ready for Billing
|
||
│ │ ├── Billed to ADP
|
||
│ │ └── Claim Submission Files
|
||
│ ├── Pre-Submission
|
||
│ │ ├── Quotation Stage / Assessment Scheduled / Waiting for Application
|
||
│ │ ├── Application Received / Ready for Submission
|
||
│ ├── Application Review
|
||
│ │ ├── Application Submitted / Accepted / Approved / Rejected / Needs Correction
|
||
│ ├── Fulfillment
|
||
│ │ ├── Ready for Delivery / Case Closed
|
||
│ └── Special Statuses
|
||
│ └── On Hold / Withdrawn / Denied / Cancelled / Expired
|
||
├── ODSP
|
||
│ ├── All ODSP Cases / ODSP Invoices
|
||
│ ├── ODSP Standard (8 stage sub-menus + Special Statuses)
|
||
│ ├── SA Mobility (8 stage sub-menus + Special Statuses)
|
||
│ └── Ontario Works (stage sub-menus)
|
||
├── MOD / WSIB / Insurance / MDC / Hardship (sub-menus per funder)
|
||
├── Dashboard → fusion.claims.dashboard (4 configurable panels)
|
||
├── Client Profiles → fusion.client.profile
|
||
├── Application Data → fusion.adp.application.data
|
||
├── Submission History → fusion.submission.history
|
||
├── ADP Device Codes → fusion.adp.device.code (Mobility Manual)
|
||
├── Page 11 Signing → fusion.page11.sign.request
|
||
├── Technician Tasks → fusion.technician.task (extends fusion_tasks menu)
|
||
└── Configuration
|
||
├── Settings → res.config.settings
|
||
├── Field Mapping → fusion_claims.field_mapping_config wizard
|
||
├── Device Code Import → fusion_claims.device.import.wizard
|
||
├── XML Import → fusion.xml.import.wizard
|
||
└── ADP Import → fusion_claims.adp.import.wizard
|
||
```
|
||
|
||
Dashboard model: `fusion.claims.dashboard` (TransientModel, `models/dashboard.py`) — 4 configurable panels each showing top 50 cases for any of 8 case types (ADP, ODSP, MOD, Hardship, ACSD, Muscular Dystrophy, Insurance, WSIB), rendered as HTML tables with direct links to the SO.
|
||
|
||
## 21.5 Case-close audit gate
|
||
|
||
`fusion_claims.case_close_verification_wizard` (`wizard/case_close_verification_wizard.py`) is mandatory for the `case_closed` transition. It verifies four things:
|
||
|
||
| Check | Source |
|
||
|---|---|
|
||
| Signed Pages 11 & 12 | `x_fc_has_signed_pages_11_12` (bundled flag OR file OR signed remote request) |
|
||
| Final Application | `x_fc_final_submitted_application` set |
|
||
| Proof of Delivery | `x_fc_proof_of_delivery` set |
|
||
| Vendor Bills | At least one record in `x_fc_vendor_bill_ids` (Many2many to vendor `account.move`) |
|
||
|
||
The wizard offers **two paths**:
|
||
- `action_close_case` — strict, raises `UserError` if any item is missing.
|
||
- `action_close_anyway` — escape hatch, closes the case AND posts a yellow warning card to chatter listing what was missing (for audit).
|
||
|
||
`x_fc_vendor_bill_ids` is a manual Many2many — the user links the vendor bills that supplied the products to this case via the ADP Order Trail tab. There's no automatic linkage from purchase orders.
|
||
|
||
## 21.6 Contact types (res.partner)
|
||
|
||
`res.partner.x_fc_contact_type` has **22 values** — used to drive conditional form rendering, smart buttons, and search filters:
|
||
|
||
```
|
||
adp_customer, adp_odsp_customer, odsp_customer, mod_customer,
|
||
private_customer, wsib_customer, acsd_customer, private_insurance,
|
||
adp_agent, odsp_agent, muscular_dystrophy,
|
||
occupational_therapist, physiotherapist, accessibility_specialist,
|
||
vendor, funding_agency, government_agency, company_contact,
|
||
long_term_care_home, retirement_home, odsp_office, other
|
||
```
|
||
|
||
Form view (`views/res_partner_views.xml`):
|
||
- Contact Type field above the address.
|
||
- **Smart button** "ADP Applications" — visible only when `x_fc_adp_application_count > 0`. Opens applications filtered by either the authorizer's ADP registration number or by name match (`_get_authorizer_application_domain`).
|
||
- `x_fc_authorizer_number` ("ADP Reg. Number" e.g. `3000001234`) — visible only for `occupational_therapist`, `physiotherapist`, `adp_agent`. Used by the XML parser to auto-link the authorizer on imported applications.
|
||
- **ODSP notebook tab** — visible only for `odsp_customer`, `adp_odsp_customer`, `odsp_agent`, `odsp_office`. Captures `x_fc_odsp_member_id`, `x_fc_case_worker_id`, `x_fc_date_of_birth`, `x_fc_healthcard_number`.
|
||
- Search filters: "ODSP Customers", "ODSP Offices".
|
||
|
||
## 22. Security model
|
||
|
||
Two groups + one special override group (`security/security.xml`):
|
||
|
||
| Group | Implies | Purpose |
|
||
|---|---|---|
|
||
| `group_fusion_claims_user` | `base.group_user` + `sales_team.group_sale_salesman` | Standard user |
|
||
| `group_fusion_claims_manager` | user + `sales_team.group_sale_manager`, default for admin/root | Administrator |
|
||
| `group_document_lock_override` | (none — manually assigned) | Allows editing locked documents on legacy cases when `fusion_claims.allow_document_lock_override = True`. |
|
||
|
||
Module category `module_category_fusion_claims` + privilege `res_groups_privilege_fusion_claims` group everything under a **FUSION CLAIMS** section in user settings (Odoo 19 pattern).
|
||
|
||
Manager-only fields (`groups='fusion_claims.group_fusion_claims_manager'`):
|
||
- `fc_twilio_account_sid`
|
||
- `fc_twilio_auth_token`
|
||
- (Sensitive credentials)
|
||
|
||
Public model access: `fusion.page11.sign.request` grants `base.group_public` read-only access for the `/page11/sign/<token>` endpoint to resolve.
|
||
|
||
`security/ir.model.access.csv` has 65+ ACLs — every wizard model has user+manager rules, the device codes table is sales-team writable / base-user readable.
|
||
|
||
## 23. Configuration parameters
|
||
|
||
Defaults in `data/ir_config_parameter_data.xml` (all `noupdate="1"` — never overwritten on upgrade). Settings UI in `models/res_config_settings.py` + `views/res_config_settings_views.xml`.
|
||
|
||
### Billing
|
||
- `fusion_claims.vendor_code` — ADP vendor ID for exports.
|
||
- `fusion_claims.adp_posting_base_date` — `2026-01-23`.
|
||
- `fusion_claims.adp_posting_frequency_days` — `14`.
|
||
- `fusion_claims.adp_approval_expiry_months` — `12`.
|
||
- `fusion_claims.adp_billing_reminder_user_id` — single user (stored manually).
|
||
- `fusion_claims.adp_correction_reminder_user_ids` — comma-separated user IDs.
|
||
|
||
### Email
|
||
- `fusion_claims.enable_email_notifications` — `True`.
|
||
- `fusion_claims.application_reminder_days` — `4`.
|
||
- `fusion_claims.application_reminder_2_days` — `4`.
|
||
- `fusion_claims.adp_hold_reminder_interval_days` — `30`.
|
||
- `fusion_claims.adp_hold_final_warning_days_before_expiry` — `30`.
|
||
|
||
### AI
|
||
- `fusion_claims.ai_api_key` — OpenAI key.
|
||
- `fusion_claims.ai_model` — `gpt-4o-mini` / `gpt-4o` / `gpt-4.1-mini` / `gpt-4.1`.
|
||
- `fusion_claims.auto_parse_xml` — `True`.
|
||
|
||
### Twilio
|
||
- `fusion_claims.twilio_enabled` — `False`.
|
||
- `fusion_claims.twilio_account_sid` / `_auth_token` — manager-only.
|
||
- `fusion_claims.twilio_phone_number`.
|
||
|
||
### MOD
|
||
- `fusion_claims.mod_default_email` — `hvmp@marchofdimes.ca`.
|
||
- `fusion_claims.mod_vendor_code`.
|
||
- `fusion_claims.mod_followup_interval_days` — `14`.
|
||
- `fusion_claims.mod_followup_escalation_days` — `3`.
|
||
- `fusion_claims.mod_followup_max_per_month` — `2`.
|
||
- `fusion_claims.mod_followup_window_days` — `30`.
|
||
- `fusion_claims.mod_followup_max_per_cron_run` — `10`.
|
||
|
||
### ODSP
|
||
- `fusion_claims.sa_mobility_email` — `samobility@ontario.ca`.
|
||
- `fusion_claims.sa_mobility_phone` — `1-888-222-5099`.
|
||
- `fusion_claims.odsp_default_office_id`.
|
||
|
||
### Workflow
|
||
- `fusion_claims.allow_sale_type_override` — bypasses sale-type lock.
|
||
- `fusion_claims.allow_document_lock_override` — required for the Document Lock Override group to take effect.
|
||
- `fusion_claims.designated_vendor_signer` — user who signs Page 12 on behalf of the company.
|
||
- `fusion_claims.store_open_hour` / `store_close_hour` — defaults `9.0` / `18.0`.
|
||
|
||
### Portal branding
|
||
- `fusion_claims.portal_gradient_preset` — `green_teal` / `blue_purple` / `orange_red` / `dark_slate` / `custom`.
|
||
- `fusion_claims.portal_gradient_start` / `_mid` / `_end` — custom hex colours.
|
||
|
||
### Field mapping (~25 keys)
|
||
- `fusion_claims.field_sale_type`, `field_so_client_type`, `field_so_authorizer`, `field_so_claim_number`, `field_so_client_ref_1/2`, `field_so_delivery_date`, `field_so_adp_status`, `field_so_service_start/end`, `field_so_primary_serial`
|
||
- `fusion_claims.field_invoice_type`, `field_inv_client_type`, `field_inv_authorizer`, `field_inv_claim_number`, `field_inv_client_ref_1/2`, `field_inv_delivery_date`, `field_inv_service_start/end`, `field_inv_primary_serial`
|
||
- `fusion_claims.field_sol_serial`, `field_sol_placement`
|
||
- `fusion_claims.field_aml_serial`, `field_aml_placement`
|
||
- `fusion_claims.field_product_code`, `field_product_adp_price`
|
||
|
||
## 24. Naming conventions
|
||
|
||
Follows the repo-wide rule: `x_fc_*` for new fields, `x_studio_*` for legacy Studio-created fields (kept for migration).
|
||
|
||
| Pattern | Used for |
|
||
|---|---|
|
||
| `x_fc_is_*_sale`, `x_fc_show_*_fields` | Computed booleans deriving from `x_fc_sale_type` |
|
||
| `x_fc_*_status` | State machine fields (per funder) |
|
||
| `x_fc_*_status_locked` | Computed locks derived from current status |
|
||
| `x_fc_previous_status_before_hold/withdrawal/cancel/delivery` | Audit-trail strings used to restore status after a special branch |
|
||
| `x_fc_*_date`, `x_fc_*_datetime` | Workflow event timestamps |
|
||
| `x_fc_trail_has_*`, `x_fc_*_trail_*` | Computed boolean audit trail flags (drawing received, POD sent, etc.) |
|
||
| `x_fc_source_sale_order_id`, `x_fc_adp_invoice_id`, `x_fc_client_invoice_id` | Cross-document linking |
|
||
| `x_fc_*_filename` | Required companion field for every `Binary(attachment=True)` field |
|
||
| `fusion_claims.field_*` | ICP keys for the legacy field mapping system |
|
||
|
||
Sale type `selection` values are kebab-snake (`adp_odsp`, `march_of_dimes`, `muscular_dystrophy`, `direct_private`) — match them exactly when domain-filtering or comparing strings.
|
||
|
||
All user-facing text is **Canadian English** (per repo CLAUDE.md). All monetary values are CAD with the `$` sign + `Monetary` field + explicit `currency_id`.
|
||
|
||
## 25. Common gotchas
|
||
|
||
1. **Never write `x_fc_adp_application_status` (or any controlled status) directly.** Use the corresponding `action_*` method on `sale.order` so emails, chatter, activity scheduling, and history records fire correctly. The `filtered_status_selection` widget already hides controlled statuses from the dropdown; only pass `with_context(skip_status_validation=True)` for legitimate framework calls (sync hooks, cron, test fixtures).
|
||
|
||
2. **Context skip flags** are everywhere — using or omitting them wrongly causes infinite recursion or silent data loss. The vocabulary:
|
||
- `skip_sync` — don't bounce values back through SO↔invoice sync.
|
||
- `skip_status_emails` — silence outgoing transition emails.
|
||
- `skip_status_validation` — bypass status-machine guards.
|
||
- `skip_document_chatter` — don't re-post the "document uploaded" chatter line.
|
||
- `skip_page11_check` — bypass the Page 11 signature requirement check.
|
||
- `skip_payment_status_update` — invoice write skips auto-billing-status flip.
|
||
- `mark_ready_for_delivery`, `mark_odsp_ready_for_delivery` — used on technician task create to advance the SO.
|
||
|
||
3. **`x_fc_sale_type` is locked** once any funder workflow leaves quotation. Setting `fusion_claims.allow_sale_type_override = True` is the only escape hatch — don't add custom bypasses.
|
||
|
||
4. **Client invoice can be created BEFORE device verification.** ADP invoice cannot. Modification-reason cases (`mod_non_adp`, `mod_adp`) block client invoice creation until ADP approval — this is intentional and surfaces as a `danger`-styled sticky notification.
|
||
|
||
5. **Non-ADP funded product codes route to client 100%** regardless of sale type. The whitelist (prefix match, case-insensitive): `NON-ADP`, `NON-FUNDED`, `UNFUNDED`, `NOT-FUNDED`, `ACS`, `ODS`, `OWP`. If a product needs to be ADP-funded with a code that prefix-matches one of these, rename the code.
|
||
|
||
6. **ADP price source order matters.** When invoking calculations, always go through `product.product.get_adp_price()` / `.get_adp_device_code()`. Reading the field directly skips both the device-code database lookup AND the legacy field-mapping layer — silently produces wrong totals in legacy databases.
|
||
|
||
7. **Split invoice sync is two-way.** Editing a field on one invoice (`x_fc_claim_number`, dates, authorizer, client_type, primary_serial) can rewrite the SO **and** the sibling invoice. Set `is_manually_modified = True` on an invoice to opt it out of SO→invoice direction; serial numbers always sync via the line-by-line helper.
|
||
|
||
8. **Page 11 signing writes both an attachment and `x_fc_signed_pages_11_12`.** Regenerating a signed PDF doesn't delete the previous attachment — the old version stays in chatter. Use the wizard's cancel/resend flow rather than direct writes.
|
||
|
||
9. **Field-mapping getters are not optional.** If you read `record.x_fc_sale_type` directly in code you ship to production, you'll skip the mapping layer and break legacy databases. Use `record._get_sale_type()`.
|
||
|
||
10. **Post-init hook idempotence** — `_load_adp_device_codes` runs on every upgrade. The `UPDATE` SQL in `_link_products_to_device_codes` is guarded by `IS NULL` checks; preserve those when editing or you'll clobber manual product↔device-code remappings on every upgrade.
|
||
|
||
11. **MOD follow-up cap is shared state.** Don't create `fusion.activity` records of type `mod_followup` directly — call `_mod_followup_cap_state` first to check the rolling 30-day window. The cron defends against burst sends, but ad-hoc code can blow past the cap.
|
||
|
||
12. **`fusion_pdf_preview` migration is incomplete in this module.** Per repo CLAUDE.md, attachments opened by custom buttons should route through `att.action_fusion_preview(title='...')` instead of `ir.actions.act_url` with `target=new` / `download=true`. When you touch a callsite here, migrate it. Existing report-style downloads are already intercepted by the JS layer.
|
||
|
||
13. **`tax_totals_patch.js` is load-bearing.** Removing it crashes the JS view on any invoice without tax configuration. If you ship a fix upstream, audit the patch's removal carefully.
|
||
|
||
14. **`action_invoice_sent` override on `account.move`** auto-selects the `email_template_adp_invoice` template for ADP invoices. Removing this override without migrating the email-template selection breaks the ADP invoice send UX.
|
||
|
||
15. **Sale order `display_name` is overridden** to `"<name> - <partner.name>"` (`models/sale_order.py:22-28`). Don't rely on `display_name` being just the order number — use `name` if that's what you need.
|
||
|
||
16. **Sale order display name search** (`_rec_names_search`) matches both `name` and `partner_id.name` — useful for `Many2one` autocomplete; gotcha if you're doing a Domain on `display_name`.
|
||
|
||
17. **The auto-payment ODSP advancement** runs in `account.move._compute_payment_state` (override). A paid invoice on a linked ODSP order at status `pod_submitted` / `submitted_to_ow` / `payment_received` auto-advances to `payment_received` and (Ontario Works only) schedules a delivery activity.
|
||
|
||
18. **`fusion_claims.scss` uses `html.dark` / `.o_dark`** — per repo CLAUDE.md, this doesn't reliably fire in Odoo 19. New SCSS should branch on `$o-webclient-color-scheme` at compile time and be registered in both `web.assets_backend` AND `web.assets_web_dark`.
|
||
|
||
19. **The legacy `x_fc_adp_status` field** (7-state, simpler) is kept for backward compatibility. Read from `x_fc_adp_application_status` (22-state, comprehensive) instead. Don't write both.
|
||
|
||
20. **`fusion_claims.config` is `TransientModel`** — `models/fusion_central_config.py`. Don't expect `id`-stable records. The model exists purely to host action methods callable from the settings UI.
|
||
|
||
21. **Pages 11 & 12 have THREE valid intake states** — don't check `x_fc_signed_pages_11_12` directly to gate workflow steps. Use the computed `x_fc_has_signed_pages_11_12` which returns True for: bundled flag (`x_fc_pages_11_12_in_original=True`), separate file uploaded, OR `fusion.page11.sign.request` in state `signed`. Same for `x_fc_trail_has_signed_pages`. Check the existing tests in `tests/test_signed_pages_gate.py` before adding new gates.
|
||
|
||
22. **`x_fc_case_locked` is global** — when True, the `write` override blocks every `x_fc_*` write except the lock itself + Odoo plumbing (`message_main_attachment_id`, `message_follower_ids`, `activity_ids`). This is different from per-document status locks. Context bypass: `skip_all_validations=True`.
|
||
|
||
23. **Document status locks vs case lock vs sale type lock** — three separate mechanisms with different rules:
|
||
- **Sale type lock** — blocks `x_fc_sale_type` writes after any non-quotation funder status. Bypass: `fusion_claims.allow_sale_type_override = True` OR (`fusion_claims.allow_document_lock_override = True` AND user in `group_document_lock_override`) OR context `skip_sale_type_lock=True`.
|
||
- **Document status locks** — blocks specific document fields based on workflow status (see §4.5). Bypass: (`fusion_claims.allow_document_lock_override = True` AND user in `group_document_lock_override`) OR context `skip_document_lock_validation=True`.
|
||
- **Case lock** — blocks all `x_fc_*` writes when `x_fc_case_locked=True`. Bypass: context `skip_all_validations=True`.
|
||
|
||
24. **Document audit trail preservation in chatter** — when you write to a document binary field (`x_fc_original_application` etc.) on `sale.order`, the old binary value is automatically copied into a chatter post before being overwritten. Use `with_context(skip_document_chatter=True)` for legitimate re-saves (e.g., re-rendering Page 11 PDF with the same content).
|
||
|
||
25. **Replacement <5 year warning** — `x_fc_under_5_years` flag (computed from `x_fc_previous_funding_date`) drives a chatter warning when creating a client invoice for `replace_status` / `replace_size` / `replace_worn` reasons. Don't suppress the warning — it's a hint to verify the approval letter for ADP deductions.
|
||
|
||
26. **`reason_for_application` previous-funding gate** — `ready_for_submission_wizard` requires `previous_funding_date` for every reason except `first_access` and `mod_non_adp`. Don't skip the check on programmatic transitions.
|
||
|
||
27. **`account.payment.x_fc_card_last_four` is exactly 4 digits** — `account_payment_register` validates `.isdigit() and len()==4`. Don't store dashed strings or full PANs. The "is card payment" detection prefers `payment_method_line.x_fc_requires_card_digits` flag over keyword matching — set the flag on the journal form, don't rely on method-name heuristics.
|
||
|
||
28. **`sale.advance.payment.inv` adds ADP options** — the standard "Create Invoice" wizard now has `adp_client` (25%) and `adp_portion` (75%/100%) selections that route to `_create_adp_split_invoice`. `adp_client` raises if the client_type is not REG; both raise if the order is not an ADP sale. Use these instead of bypassing to direct `_create_invoices` for split scenarios.
|
||
|
||
29. **PDF magic-byte validation** — `application_received_wizard._validate_pdf_bytes` checks that the base64-decoded payload starts with `%PDF-`. Test fixtures use `b'%PDF-1.4\n%fake pdf for tests'`. If you add a new PDF upload field elsewhere, copy this defence — the filename `.pdf` constraint alone is not enough.
|
||
|
||
30. **`web_save` debug override** — `sale.order.web_save` has a `_logger.warning` shim active in production to diagnose "Missing required fields" errors on legacy orders. Don't remove it without coordinating with whoever's tracking the issue; if you do, also remove the matching `_logger.error` in the except branch.
|
||
|
||
31. **`ready_submission` gate uses raw `x_fc_signed_pages_11_12`, not the computed `x_fc_has_signed_pages_11_12`** (`models/sale_order.py:7706-7707`). This means a direct write (without going through `application_received_wizard` + `ready_for_submission_wizard`) will reject bundled-mode and remote-signed orders even though they're functionally complete. The wizards bypass this by ensuring `x_fc_signed_pages_11_12` is populated for separate/remote modes — but bundled-mode orders rely on the wizard's own gate (which uses the computed). If you call `write()` directly with `x_fc_adp_application_status='ready_submission'`, you must also pass `with_context(skip_status_validation=True)` for bundled-mode orders to succeed.
|
||
|
||
32. **MOD partial PCA invoices reuse `x_fc_adp_invoice_portion='adp'`** for the MOD invoice — disambiguate via `x_fc_invoice_type` (`'march_of_dimes'` vs `'adp'`). Searching only by `x_fc_adp_invoice_portion='adp'` will catch MOD invoices too.
|
||
|
||
33. **ADP invoice customer is NOT the SO partner** — `_create_adp_split_invoice` switches `partner_id` to the ADP partner (searched by name) and moves the original client to `partner_shipping_id`. Aggregations grouped by `partner_id` on `account.move` will lump all ADP invoices together under the ADP partner — group by `x_fc_source_sale_order_id.partner_id` for client-level aggregates.
|
||
|
||
34. **Price-mismatch auto-correction is silent for products with `x_fc_adp_device_code_id`** — those products are managed via the Many2one and the write override on `fusion.adp.device.code` pushes price changes to them. For products WITHOUT the Many2one, `_create_adp_split_invoice` will silently overwrite their `x_fc_adp_price` if the device-code DB says otherwise. The chatter warning is the only signal.
|
||
|
||
35. **Stage 2 device approval syncs to existing invoices** — `_sync_approval_to_invoices` rewrites line `price_unit` on existing client and ADP invoices to match the new approval state. If you've manually adjusted invoice line prices, they'll be overwritten. Use `is_manually_modified=True` on the invoice if you need to lock it.
|
||
|
||
36. **`mark_as_approved` mode attachment garbage collection** — many2many_binary to a TransientModel is GC'd when the wizard closes. `device_approval_wizard.action_confirm_approval` copies the bytes into new persistent `ir.attachment` records. Any wizard you write that takes many2many_binary on a transient model must do the same — DO NOT pass the original attachment IDs into a non-transient many2many; copy first.
|
||
|
||
37. **MOD `quote_submitted` / `funding_approved` auto-stamp dates** — the write override auto-fills `x_fc_case_submitted` / `x_fc_case_approved` with today if blank. Don't rely on those dates as "user-entered" — they may be auto-stamped by status transitions.
|
||
|
||
38. **Soft `fusion_faxes` dependency** — `odsp_submit_to_odsp_wizard.action_send_fax` and `action_send_fax_and_email` will fail at click-time if `fusion_faxes` is not installed. Add it to the manifest's depends if you're moving to a database without it, or guard the actions explicitly in views.
|
||
|
||
39. **`action_adp_reopen_expired` is a back-compat shim** — it just calls `action_adp_duplicate_for_reassessment`. Per 2026-04 policy, expired cases CANNOT be self-renewed (authorizer must reassess after 12 months). The shim is kept so old Studio views with the button still resolve the method name.
|
||
|
||
40. **The 3-month assessment expiry blocks resume-from-hold** — `x_fc_assessment_expired = True` when `(today - x_fc_assessment_end_date).days > 90`. Resuming from `on_hold` to any other status will hit this gate. The OT must redo the assessment, which (in practice) means using `assessment_completed_wizard` in override mode to record the new assessment date.
|
||
|
||
41. **ADP export verification BLOCKS, not warns** — when stored line portions don't match the recomputed values from the device-code DB (tolerance `$0.01 × qty`), `adp_export_wizard.action_export` raises a `UserError` listing every mismatch. The export does NOT complete with warnings — it hard-stops. Fix the invoice calculations (`account.move.line.action_recalculate_portions`) or update the device codes DB before re-trying.
|
||
|
||
42. **ADP export expands `qty` to per-unit rows** — a line with `qty=3` exports 3 rows, each with `qty=1` and `unit_portion = stored_portion / qty`. If you build downstream tooling against the CSV, expect per-unit rows, not per-line rows.
|
||
|
||
43. **ADP rejects renamed export files** — the filename is `{vendor_code}_{YYYY-MM-DD}.txt`. If you re-export on the same day (same filename), the wizard warns but proceeds; you must manually rename outside Odoo before submitting to ADP, otherwise ADP rejects the submission.
|
||
|
||
44. **`_sync_approval_to_invoices` re-posts posted invoices** — Stage 2 approval flips run `invoice.button_draft() → write → action_post()` on posted invoices. This can re-fire post-actions, invalidate sequence assumptions, and is destructive if the invoice has been exported. Set `is_manually_modified=True` on the invoice to opt it out.
|
||
|
||
45. **Three footer voices** — emails carry one of three footer lines depending on which wrapper builds them: `_email_build` (ADP), `_mod_email_build` (Accessibility Case), `_odsp_email_build` (ODSP). Use the right wrapper or your funder context disappears from the email.
|
||
|
||
46. **MOD activity dedup** — every MOD cron checks for an existing open `mail.activity` of type `mail_activity_type_mod_followup` on the order before creating a new one. Don't bypass this by creating activities directly — use `activity_schedule` or the cron's pattern, otherwise you'll get duplicate activities every day.
|
||
|
||
47. **`automated=True` on `mail.activity.create`** — suppresses Odoo's default "activity assigned" email notification. Three MOD crons use this so the assignee sees the activity on their dashboard but doesn't get spammed in their inbox. If you create activities programmatically and the user complains about inbox noise, add this.
|
||
|
||
48. **`_schedule_or_renew_adp_activity` updates instead of duplicating** — the pattern for ADP billing/correction reminders. Finds an existing activity of the same type for the same user and updates its date/summary/note. Use this helper, not `activity_schedule` directly, for repeating reminders.
|
||
|
||
49. **Twilio SMS hard-codes "Westin Healthcare"** — the `assessment_scheduled` message template (`models/sale_order.py:9825`) has the company name baked into a string literal, not pulled from `self.company_id.name`. Multi-tenant deployments will read wrong here — fix before going past a single Westin install.
|
||
|
||
50. **Per-MOD-method recipient rules vary** — `contract_received` is client-only (no authorizer); `invoice_submitted` is MOD contact only; `pod_submitted` includes client + authorizer + MOD contact. Don't assume "all MOD emails CC the authorizer" — see the table in §16.5.
|
||
|
||
51. **MOD funding denied wizard requires a category** — 5 enum values (`income_too_high`, `residency`, `project_scope`, `missing_docs`, `funding_depleted`, plus `other`). The category label is prepended to the free-text reason as `'[{label}] {text}'` and stored in `x_fc_mod_funding_denial_reason`. Don't write the field directly — go through the wizard so the format is consistent.
|
||
|
||
52. **MOD resubmit can clear documents** — `mod_resubmit_wizard.clear_old_documents` wipes `x_fc_mod_drawing` and `x_fc_mod_proposal_doc` after posting copies to chatter. If you've built tooling that assumes those fields stay populated through the lifecycle, document the resubmission cycle explicitly.
|
||
|
||
53. **`odsp_pre_approved_wizard` routes by division** — same wizard, two different target fields depending on `x_fc_odsp_division`: `sa_mobility` → `x_fc_sa_approval_form`; `standard` → `x_fc_odsp_approval_document`. Ontario Works (`x_fc_odsp_division == 'ontario_works'`) is NOT handled here — OW has its own discretionary flow.
|
||
|
||
54. **SCSS uses THREE dark-mode strategies** simultaneously: `html.dark`, `.o_dark`, AND `@media (prefers-color-scheme: dark)`. Per repo CLAUDE.md, NONE of these fire reliably in Odoo 19 — new SCSS should branch on `$o-webclient-color-scheme` at compile time and be registered in both `web.assets_backend` AND `web.assets_web_dark`. The existing patterns work in some browsers and fail in others; treat them as legacy.
|
||
|
||
55. **SOL list column widths are pinned in SCSS** — `.o_field_one2many[name="order_line"]` forces `table-layout: fixed` with explicit pixel widths for every `th[data-name="..."]` selector. Odoo 19 ignores the XML `width` attribute on list fields, so this is the only way to control column widths. **If you rename a SOL field, update the matching SCSS selector** — otherwise the column collapses to whatever the browser auto-sizes.
|
||
|
||
56. **Default Odoo SO + invoice reports are also modified** — `report_templates.xml` inherits `account.report_invoice_document` and `sale.report_saleorder_document` to add an SKU column AND strip `[internal_ref]` prefixes. This affects reports you may not realize this module touches. If users report formatting differences on the standard invoice/SO PDFs, this is the cause.
|
||
|
||
57. **Acceptance reminder cron has 14-day backlog guard** — `fusion_claims.acceptance_reminder_max_age_days` (default 14) excludes cases submitted > 14 days ago. First cron run after a long deploy outage won't email every old stuck `submitted` case. Per-run cap (`acceptance_reminder_max_per_cron_run`, default 10) further spreads large backlogs. Both can be tuned via settings if you need to flush a backlog quickly.
|
||
|
||
58. **Acceptance reminder skips weekends with 2-day lower bound** — hard-coded `cutoff_date = today - 2 days`. Don't expect reminders the day after submission; the cron treats "1 business day" as "2 calendar days" so Friday submissions don't trigger Saturday emails.
|
||
|
||
59. **Acceptance reminder is two-tier** — `≤3 days` since submission → office only; `>3 days` → office + sales rep. The one-shot `x_fc_acceptance_reminder_sent` flag means each cycle gets AT MOST ONE email; the flag resets on resubmission (see §4.9).
|
||
|
||
60. **`_send_approval_email` attaches an "Approved Items" PDF** — generated on-the-fly via `_generate_approved_items_pdf` (calls `action_report_approved_items` QWeb report). If you suppress this for performance reasons (e.g., for very large orders), the email body still has `_build_approved_items_html` inline as `extra_html` — so the user sees the table in the email even if the PDF generation fails.
|
||
|
||
61. **`_send_application_reminder_2_email` mentions 90-day assessment validity** — content callout to the OT that the assessment may need to be redone if too much time passes. Tied to the `x_fc_assessment_expired` computed field (>90 days → True).
|
||
|
||
62. **Report color convention** — every report sets `primary` and `secondary` from `company.primary_color` (default `#0066a1`) and `company.secondary_color` (default `#90be6d`) at the top, then references them via `<t t-out="primary"/>`. If you ship a new report, follow this pattern so it picks up the company's brand colors.
|
||
|
||
63. **Ontario Works auto-close uses `delivered`, not `payment_received`** — because for OW, payment comes BEFORE delivery (the wholesaler pays the vendor, then the vendor delivers to the client). So `_cron_auto_close_odsp_paid_cases` closes 7 days after `delivered` for OW; 7 days after `payment_received` for SA Mobility and ODSP Standard. **Important to remember when building OW reports/KPIs.**
|
||
|
||
64. **Default-report SKU split logic** — `[internal_ref]` prefix is split on the first occurrence of `'] '` (close-bracket-space) — not just `']'`. A product name like `[MXA-1618]GEOMATRIX...` (no space) would NOT be split correctly. If users complain about descriptions still containing brackets, check the product name format.
|
||
|
||
65. **`_send_application_reminder_email` sets a one-shot flag** — `x_fc_application_reminder_sent=True` after the first send. The flag does NOT reset on resubmission (unlike the acceptance reminder flag). If you need a fresh reminder cycle after a workflow reset, clear the flag manually via `with_context(skip_all_validations=True).write({...})`.
|
||
|
||
66. **The 2026-04 authorizer email policy excludes the OT from `accepted` and `ready_for_delivery` emails** — by design (see §16.4.1). Don't add the authorizer back into those without checking the audit policy first; the rule is that the OT is only in the loop for actionable states.
|
||
|
||
67. **`_send_ready_for_delivery_email` adds delivery address from `partner_shipping_id.contact_address`** — comma-joined, newlines replaced. Falls back to `partner_id` if shipping address is blank. Technicians are CC'd; the email also includes `Scheduled` datetime from the wizard.
|
||
|
||
68. **`_send_withdrawal_email` has three different intents** — `cancel` (red, permanent), `resubmit` (amber, back to ready_submission), and (None) (amber, plain). `action_adp_withdraw` defaults to `cancel` but `status_change_reason_wizard` exposes both.
|
||
|
||
69. **`_sync_fields_to_invoices` is defensive about Studio fields** — checks `if 'x_fc_*' in invoice._fields` before writing each key. Don't remove these checks even though they look redundant; legacy databases without certain studio fields rely on them.
|
||
|
||
70. **`_sync_serial_numbers_to_invoices` does NOT use header fallback** — each SO line syncs its OWN serial to its linked invoice lines via `sale_line_ids` link. If the SOL is blank, the AML is left alone. Don't assume there's a header-to-line fallback; serials are per-line throughout.
|
||
|
||
71. **`adp_export_record._get_posting_period_for_file`** uses **strict `<=` comparison** — a file dated EXACTLY on the posting date falls into THAT posting; a file dated one day later falls into the NEXT posting. Important for `adp_import_wizard` historical imports.
|
||
|
||
72. **`migrate_from_documents` is run from a settings button**, not auto. The settings UI in `res_config_settings` has an `action_migrate_adp_export_files` button. The migration is **one-time** by design (and idempotent if rerun) — but the source documents get archived (`active=False`), so a rerun won't find them.
|
||
|
||
73. **SA Mobility gov form has a `Text1` field that collides with `Text 1` (with space)** — they're different fields on different pages. Don't normalize whitespace when building the mapping or you'll write to the wrong field.
|
||
|
||
74. **`odsp_sa_mobility_wizard._get_template_path()` uses raw `os.path`** instead of Odoo's `tools.misc.file_path`. If the module is ever deployed as a zip (rare in Odoo deployments but possible), this will fail. Migrate to `file_path('fusion_claims/static/src/pdf/sa_mobility_form_template.pdf')` if you ship this for multi-tenant.
|
||
|
||
75. **PDF template field positions for ODSP signing live in `fusion.pdf.template` (category=odsp)** — managed via a drag-and-drop editor that lives in `fusion_authorizer_portal`. The OWL editor reads field positions per-page; `_apply_pod_signature_to_approval_form` consumes them. If the gov SA form layout changes, edit the template via the visual editor, not by changing Python coordinates.
|
||
|
||
76. **SA Mobility wizard limits rows**: 6 parts, 5 labour, 4 fees. The gov PDF only has that many slots. If the SO has more lines, the rest are silently dropped from the form fill (but still appear in the invoice). The wizard truncates via slicing in `default_get`.
|
||
|
||
77. **`migrate_from_documents` archives source documents on success** (`active=False`). This means a second run won't re-migrate. If you need to re-migrate intentionally, unarchive the documents first via the Documents app.
|
||
|
||
78. **OW Discretionary uses PyPDF2, NOT pdfrw** — because the gov form is AES-encrypted. The wizard handles decryption (empty password) and AcroForm preservation. **Both `pdfrw` AND `PyPDF2` are de-facto required Python deps** for ODSP workflows: pdfrw for SA Mobility, PyPDF2 for OW Discretionary. Only `pdfrw` and `pdf2image`/`PIL` are declared in the manifest; PyPDF2 is implicit (used as a transitive dep via PdfFileReader from odoo.tools.pdf in some places).
|
||
|
||
79. **OW Discretionary form field names DON'T match their labels** — `txt_email[0]` is the Phone field, `txt_emp_phone[0]` is the Email field, `txt_CITY[1]` is the Member ID. **Don't normalize these names** when writing the mapping or you'll write to the wrong field. The mapping in `_build_field_mapping` is the canonical reference — keep it in sync with the gov form, not with semantic intuition.
|
||
|
||
80. **OW Discretionary checkbox annotations are mutated directly** — `annot[NameObject('/V')] = NameObject('/1')` for checked, `NameObject('/Off')` for unchecked. PyPDF2 has no high-level checkbox API; if you add new checkboxes to the form, update the mapping AND the annotation-walking loop.
|
||
|
||
81. **send_to_mod_wizard subjects use HVMP reference when available** — `f'{prefix} - {ref} - {client_name}'` for cases with `x_fc_case_reference`, otherwise `f'{prefix} - {client_name} - {order.name}'`. Don't write code that hard-codes the order name into the subject — preserve the reference-first pattern.
|
||
|
||
82. **`_get_field_att` mutates filenames in-place** — finds the existing Odoo auto-generated `ir.attachment` for a binary field and renames it to the "pro" format (`Drawing - John_Doe - S29958.pdf`). Don't bypass this and create a duplicate attachment; the helper preserves Odoo's binary-field linkage.
|
||
|
||
83. **`send_to_mod_wizard` requires DIFFERENT files per mode** — drawing mode needs `drawing_file`; completion mode needs BOTH `completion_photos_file` AND `pod_file`. The wizard raises `UserError` if missing — don't try to bypass by passing `mod_wizard_mode='completion'` programmatically without uploading both.
|
||
|
||
## 26. Local development
|
||
|
||
Per the per-user memory and current local setup (overrides the repo-root README defaults):
|
||
|
||
```bash
|
||
# Container & DB
|
||
docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --stop-after-init
|
||
|
||
# Tests (filter by the module tag)
|
||
docker exec odoo-modsdev-app odoo -d modsdev -u fusion_claims --test-enable --test-tags fusion_claims --stop-after-init
|
||
|
||
# URL
|
||
http://localhost:8069
|
||
```
|
||
|
||
### 26.1 Existing tests (`tests/`, tagged `'-at_install', 'post_install', 'fusion_claims'`)
|
||
|
||
- **`tests/test_signed_pages_gate.py`** (108 lines, 11 test methods) — covers `x_fc_has_signed_pages_11_12` and `x_fc_trail_has_signed_pages` across all 3 intake modes (bundled flag, separate file, remote sent vs. remote signed). Also covers `ready_for_submission_wizard` accepting bundled-only flag (no separate file) and `case_close_verification_wizard.has_signed_pages` accepting the bundled flag.
|
||
- **`tests/test_application_received_wizard.py`** (191 lines, 17 test methods) — full coverage of `application_received_wizard`: bundled/separate/remote intake modes, PDF magic-byte validation (rejects fake `.pdf` files), status gate (only from `assessment_completed` / `waiting_for_application`), default_get mode selection logic, mode-specific chatter messages.
|
||
|
||
When adding new workflow features, follow the existing pattern — tag tests with `'-at_install', 'post_install', 'fusion_claims'` so they're easy to filter and don't block module install if the test suite breaks.
|
||
|
||
### 26.2 Scripts (`scripts/` — not part of the addon, run via odoo-shell)
|
||
|
||
- **`scripts/import_adp_mobility_manual.py`** (6.4 KB) — standalone CLI for importing the ADP Mobility Manual JSON into the running database. Use when the packaged file gets out of date and you need to push an updated manual without bumping module version.
|
||
- **`scripts/import_demo_pool.py`** (9.9 KB) — loads a curated demo product pool (for staging / training environments).
|
||
- **`scripts/cleanup_demo_pool.py`** (7 KB) — reverses the demo pool import (removes demo products without touching production data).
|
||
|
||
### 26.3 Orphan / unused files (be aware before refactoring)
|
||
|
||
These files exist in the source tree but are **not loaded** by the manifest — don't assume they're active code:
|
||
|
||
| File | Notes |
|
||
|---|---|
|
||
| `views/client_chat_views.xml` (87 lines) | Legacy chat UI — replaced by native `ai.agent` chat |
|
||
| `static/src/css/fusion_task_map_view.scss` | Belongs to `fusion_tasks` — bundled by THAT module, not by this one |
|
||
| `static/src/js/fusion_task_map_view.js` (1197 lines) | Same — bundled by `fusion_tasks` |
|
||
| `static/src/xml/fusion_task_map_view.xml` (250 lines) | Same — bundled by `fusion_tasks` |
|
||
| `report/report_saleorder_adp.xml`, `report/report_invoice_adp.xml` | Empty stubs |
|
||
| `data/stock_location_data.xml` | Empty `<data/>` placeholder kept for upgrade-safety |
|
||
| `wizard/temp_serial_migration_views.xml` | No corresponding Python — historical migration aid |
|
||
|
||
If you're removing dead code, these are safe candidates; if you're adding features, double-check the manifest before editing one of these expecting it to be live.
|
||
|
||
## 27. Key files (bird's-eye)
|
||
|
||
```
|
||
models/sale_order.py 10,631 lines — workflow spine (all 8 funder lifecycles)
|
||
models/account_move.py 1,219 lines — split invoicing + billing lifecycle
|
||
models/account_move_line.py 247 lines — invoice line portion compute (mirror of SOL)
|
||
models/sale_order_line.py 418 lines — primary calculation logic
|
||
models/fusion_adp_device_code.py 428 lines — Mobility Manual + JSON/CSV import
|
||
models/dashboard.py 162 lines — 4-panel home page
|
||
models/adp_application_data.py 670 lines — every XML field (sections 1, 2, 2a-2d)
|
||
models/xml_parser.py 772 lines — XML→JSON→model with round-trip fidelity
|
||
models/client_profile.py 298 lines — client master with AI summary/risk flags
|
||
models/client_chat.py 350 lines — legacy OpenAI chat (gpt-4o-mini default)
|
||
models/ai_agent_ext.py 164 lines — native ai.agent tools
|
||
models/page11_sign_request.py 389 lines — public-token signing for Page 11
|
||
models/technician_task.py 674 lines — sale_order_id/purchase_order_id + rental inspection
|
||
models/submission_history.py 237 lines — per-submission audit trail
|
||
models/res_config_settings.py 679 lines — settings UI + ICP field mapping
|
||
models/adp_posting_schedule.py 262 lines — mixin for posting-cycle date maths
|
||
models/adp_export_record.py 449 lines — exported file archive
|
||
models/fusion_central_config.py 126 lines — Detect Existing Fields action
|
||
models/product_template.py 151 lines — x_fc_is_adp_product, device-code link
|
||
models/product_product.py 189 lines — get_adp_price / get_adp_device_code / is_non_adp_funded
|
||
models/res_company.py 124 lines — company-level MOD form storage + office CC list
|
||
models/res_partner.py 129 lines — contact_type, ODSP/authorizer fields
|
||
|
||
wizard/status_change_reason_wizard.py 543 lines — guards reject/deny/withdraw/hold/cancel
|
||
wizard/device_approval_wizard.py 724 lines — Stage 2 verification
|
||
wizard/submission_verification_wizard.py 397 lines — Stage 1 verification
|
||
wizard/adp_export_wizard.py 452 lines — TXT generator + verifier
|
||
wizard/odsp_sa_mobility_wizard.py 560 lines — fills gov SA form via pdfrw
|
||
wizard/send_to_mod_wizard.py 335 lines — drawing/quotation/POD MOD composer
|
||
wizard/odsp_discretionary_wizard.py 395 lines — OW Discretionary Benefits
|
||
wizard/field_mapping_config_wizard.py 460 lines — legacy Studio field mapper UI
|
||
wizard/mod_pca_received_wizard.py 304 lines — PCA upload + lifecycle advancement
|
||
wizard/application_received_wizard.py 304 lines — capture application from authorizer
|
||
|
||
wizard/application_received_wizard.py 304 lines — 3-mode intake (bundled/separate/remote) + PDF magic-byte check
|
||
wizard/ready_for_submission_wizard.py 196 lines — required fields gate + previous-funding rule + reason_for_application
|
||
wizard/ready_for_delivery_wizard.py 219 lines — assigns techs + auto-creates fusion.technician.task with pod_required=True
|
||
wizard/case_close_verification_wizard.py 211 lines — 4-check audit gate + strict/anyway paths
|
||
wizard/account_payment_register.py 79 lines — card last-4 digits validation
|
||
wizard/sale_advance_payment_inv.py 125 lines — adds adp_client (25%) + adp_portion (75%/100%) options
|
||
wizard/send_page11_wizard.py 92 lines — public-token signing composer
|
||
wizard/xml_import_wizard.py 107 lines — bulk XML import via fusion.xml.parser
|
||
wizard/odsp_discretionary_wizard.py 395 lines — OW Discretionary Benefits form
|
||
wizard/mod_pca_received_wizard.py 304 lines — PCA upload + lifecycle advancement
|
||
|
||
data/device_codes/adp_mobility_manual.json 528 KB — packaged Mobility Manual snapshot (~hundreds of records)
|
||
data/ai_agent_data.xml — native AI agent + 3 tools
|
||
data/ir_cron_data.xml — 13 cron jobs
|
||
data/mail_template_data.xml — 3 ADP mail.templates
|
||
data/mail_activity_type_data.xml — 4 activity types
|
||
data/ir_config_parameter_data.xml — initial settings (noupdate=1)
|
||
data/product_labor_data.xml — pre-seeded LABOR product
|
||
data/pdf_template_data.xml — empty placeholder (SA templates retired, see views/pdf_template_inherit_views.xml)
|
||
data/stock_location_data.xml — empty placeholder
|
||
|
||
security/security.xml — groups + privilege + module category
|
||
security/ir.model.access.csv — 66 ACL rows (one for every wizard/model)
|
||
|
||
static/src/scss/fusion_claims.scss — 771 lines, status pill colours
|
||
static/src/js/document_preview.js — OWL PDF viewer dialog
|
||
static/src/js/preview_button_widget.js — view widget that opens the dialog
|
||
static/src/js/status_selection_filter.js — filtered_status_selection field (hides controlled statuses)
|
||
static/src/js/gallery_preview.js — patches Many2ManyBinaryField for FileViewer
|
||
static/src/js/tax_totals_patch.js — defensive subtotals=[] on tax-less invoices
|
||
static/src/js/google_address_autocomplete.js 1506 lines — address autocomplete widget
|
||
static/src/js/calendar_store_hours.js — clamps technician task calendar to 8 AM-7 PM
|
||
static/src/js/attachment_image_compress.js 192 lines — mobile crash fix via Canvas image compression
|
||
static/src/js/debug_required_fields.js — logs missing required field labels
|
||
static/src/xml/document_preview.xml — OWL template for preview dialog
|
||
|
||
static/src/pdf/sa_mobility_form_template.pdf 482 KB — government SA Mobility form (filled by pdfrw)
|
||
static/src/pdf/sa_mobility_page2_sample.pdf 241 KB — reference sample
|
||
static/src/pdf/discretionary_benefits_form_template.pdf 1.1 MB — government OW Discretionary Benefits form (filled by pdfrw)
|
||
|
||
report/sale_report_portrait.xml — ADP-aware quotation/order portrait
|
||
report/sale_report_landscape.xml — ADP-aware quotation/order landscape
|
||
report/invoice_report_portrait.xml — invoice portrait
|
||
report/invoice_report_landscape.xml — invoice landscape (no menu binding)
|
||
report/report_proof_of_delivery.xml — ADP POD
|
||
report/report_mod_quotation.xml — MOD quote
|
||
report/report_mod_invoice.xml — MOD invoice
|
||
report/report_templates.xml — shared header + address-boxes templates
|
||
|
||
views/sale_order_views.xml 2768 lines — main SO form (tabs, buttons, conditional sections per funder)
|
||
views/adp_claims_views.xml 2013 lines — root menu tree (5 levels deep) + per-stage actions
|
||
views/client_profile_views.xml 723 lines — profile form + AI chat + claim history
|
||
views/res_config_settings_views.xml 563 lines — settings form
|
||
views/account_move_views.xml 454 lines — invoice form + sync buttons + ADP billing fields
|
||
views/dashboard_views.xml — 9 gradient cards + 4 configurable panels (2x2 grid)
|
||
views/page11_sign_request_views.xml — list + form with Resend/Cancel/New Signature header buttons
|
||
views/product_template_adp_views.xml — ADP Product toggle + ADP Information section
|
||
views/res_partner_views.xml — Contact Type (22 values) + ODSP tab + ADP Reg. Number for OTs
|
||
views/technician_task_views.xml — adds SO/PO stat buttons + Rental Inspection tab (pickup only)
|
||
views/account_journal_views.xml — "Req. Card #" column on inbound/outbound payment methods
|
||
|
||
tests/test_application_received_wizard.py 191 lines — 17 tests for 3-mode intake
|
||
tests/test_signed_pages_gate.py 108 lines — 11 tests for x_fc_has_signed_pages_11_12
|
||
|
||
scripts/import_adp_mobility_manual.py — standalone Mobility Manual importer (run via odoo-shell)
|
||
scripts/import_demo_pool.py — demo product pool loader
|
||
scripts/cleanup_demo_pool.py — reverses the demo pool import
|
||
```
|
||
|
||
## 27.3 Small status-transition wizards (one-shot wizards on a single button)
|
||
|
||
These wizards are minimal — most are ~50-100 lines — but they encode crucial business rules:
|
||
|
||
| Wizard | Purpose | Allowed FROM | Notes |
|
||
|---|---|---|---|
|
||
| `mod_awaiting_funding_wizard` | Records the application submission date | (any non-funded state, called via `action_mod_awaiting_funding`) | Just captures the date + optional notes, writes status `awaiting_funding`, stamps `x_fc_mod_application_submitted_date`. |
|
||
| `mod_funding_denied_wizard` | Captures **denial category + free-text reason** | `awaiting_funding`, `quote_submitted`, `handoff_to_client` | 5 categories: `income_too_high`, `residency`, `project_scope`, `missing_docs`, `funding_depleted`, `other`. Stored in `x_fc_mod_funding_denial_reason` as `'[{Category Label}] {details}'` — used by the denial email body. |
|
||
| `mod_resubmit_wizard` | Revise + resubmit a denied MOD case | `funding_denied` ONLY | Required `revision_notes`. Optional `clear_old_documents` toggle: when True, clears `x_fc_mod_drawing` and `x_fc_mod_proposal_doc` (after posting the originals to chatter for preservation). Always advances status → `processing_drawings`. |
|
||
| `mod_submission_confirmed_wizard` | Office confirms client/authorizer actually submitted to MOD | `handoff_to_client` ONLY | 5 confirmation sources: `phone_call`, `email`, `client_portal`, `authorizer`, `other`. Advances status → `awaiting_funding`, stamps `x_fc_mod_application_submitted_date`. Pre-fills the `submitted_by_label` from `x_fc_mod_submitted_by`. |
|
||
| `odsp_pre_approved_wizard` | Upload the ODSP approval PDF | (called via `action_odsp_pre_approved`) | Single binary field. **Routes the file by division**: `sa_mobility` → `x_fc_sa_approval_form`; `standard` → `x_fc_odsp_approval_document`. Always creates a persistent `ir.attachment` and advances ODSP status via `_odsp_advance_status('pre_approved', ...)`. |
|
||
| `account_payment_register` (override) | Capture last-4 card digits at payment time | always — extends the standard wizard | See §7.4. Returns the same action; just adds fields + validation. |
|
||
| `sale_advance_payment_inv` (override) | Adds `adp_client` / `adp_portion` options to Create Invoice | always — extends the standard wizard | See §7.3. |
|
||
| `schedule_assessment_wizard` | Create `calendar.event` + advance to `assessment_scheduled` | `quotation` ONLY | Pre-fills location from `partner_id` address; optional 1-day email alarm. |
|
||
| `assessment_completed_wizard` | Advance to `assessment_completed`, optional scheduling override | `quotation` (override mode) or `assessment_scheduled` | See §4.3.1. `override_reason` required from quotation; validates `completion_date >= assessment_start_date`. |
|
||
| `application_received_wizard` | 3-mode intake of Pages 11/12 | `assessment_completed` or `waiting_for_application` | See §4.4. |
|
||
| `ready_for_submission_wizard` | Required-field gate before submission | `application_received` ONLY | See §4.9 required-field matrix. |
|
||
| `submission_verification_wizard` | Stage 1 verification — what device types are being submitted | (before `submitted`) | See §4.3. Dual-purpose with `submit_application=True` context (writes status + final PDF + XML). |
|
||
| `device_approval_wizard` | Stage 2 verification — what ADP approved | (after ADP responds) | See §4.3 + §7.10. `mark_as_approved=True` context advances status. |
|
||
| `ready_to_bill_wizard` | POD upload + delivery date capture | `approved` or `approved_deduction` ONLY | **Blocks if `x_fc_device_verification_complete=False`**. POD must be `.pdf`. Creates a separate `ir.attachment` for the POD and posts it in chatter. |
|
||
| `ready_for_delivery_wizard` | Assigns technicians + creates delivery task | (any non-terminal post-approval state) | See §13. Auto-creates `fusion.technician.task` with `pod_required=True`, lead + additional technicians on same task. |
|
||
| `case_close_verification_wizard` | 4-check audit gate before case closure | (called via "Close Case" button) | See §21.5. Strict (`action_close_case`) or anyway (`action_close_anyway`) paths. |
|
||
| `send_page11_wizard` | Compose a Page 11 remote-signing request | (any active state) | See §14.1. Default 7-day expiry. |
|
||
| `mod_submission_path_wizard` | Choose who submits to MOD | (any pre-handoff MOD state) | 3 paths: internal/client/authorizer. Auto-fires `_send_mod_vod_request_email` when `internal` selected for the first time. |
|
||
| `mod_funding_approved_wizard` | Records case worker + HVMP ref on approval | (any pre-approval MOD state) | Bare write; stamps `x_fc_case_approved`. |
|
||
| `mod_pca_received_wizard` | PCA upload + full/partial split invoice creation | (called via `action_mod_contract_received`) | See §7.12. Creates 1 invoice (full) or 2 invoices (partial). |
|
||
| `send_to_mod_wizard` | Multi-mode MOD email composer | (multiple — context-driven `mod_wizard_mode`) | See §5.4. Modes: drawing/quotation/completion. |
|
||
| `odsp_sa_mobility_wizard` | Generate filled SA Mobility government PDF | (called from SA Mobility flow) | See §14.3. Uses `pdfrw` to fill `static/src/pdf/sa_mobility_form_template.pdf`. Has 3 transient line types: part lines, labour lines, fee lines (each with own tax calc). |
|
||
| `odsp_discretionary_wizard` | Generate filled OW Discretionary Benefits PDF | (called from Ontario Works flow) | Uses `pdfrw` to fill `static/src/pdf/discretionary_benefits_form_template.pdf`. |
|
||
| `odsp_submit_to_odsp_wizard` | Send quotation + authorizer letter to ODSP office | (any pre-`submitted_to_odsp` ODSP state) | See §14.4. Email / Fax / Email+Fax paths. |
|
||
| `odsp_ready_delivery_wizard` | Configure signature page + open delivery task | (after ODSP pre-approval) | See §14.5. PDF preview with colored markers per field. |
|
||
| `status_change_reason_wizard` | Capture reason for status changes that need one | (controlled statuses — rejected/denied/withdrawn/on_hold/cancelled/needs_correction) | Single wizard with branching by `new_status`. Different selection fields for rejection vs denial reasons. |
|
||
| `field_mapping_config_wizard` | Visual editor for the field-mapping ICP layer | (manual access from Settings) | Uses `DEFAULT_FIELD_MAPPINGS` constant — 25+ mapping definitions. Each line tracks `field_name`, `default_fc_field`, `config_param_key`, plus a **computed `field_exists`** flag that checks `ir.model.fields` to validate the user's pick. Five action buttons: `action_save`, `action_save_and_close`, `action_reset_defaults` (back to `x_fc_*`), `action_auto_detect` (uses `AUTO_DETECT_PATTERNS` keyword dict to match non-`x_fc_*` Studio fields), `action_close`. |
|
||
|
||
## 27.4 `AUTO_DETECT_PATTERNS` (field-mapping auto-detect)
|
||
|
||
`wizard/field_mapping_config_wizard.py:194-227` defines the keyword dictionary used by `action_auto_detect`. Each mapping is `config_param_key → list of substring patterns`. The matcher iterates manual custom fields (not `x_fc_*` prefixed), lowercase-checks each pattern against the field name, and picks the first match.
|
||
|
||
Synonyms per field (sample):
|
||
|
||
```python
|
||
'fusion_claims.field_sale_type': ['sale_type', 'saletype', 'type_of_sale', 'order_type']
|
||
'fusion_claims.field_so_claim_number': ['claim_number', 'claimnumber', 'claim_no', 'adp_claim', 'claim_num']
|
||
'fusion_claims.field_so_client_ref_1': ['client_ref_1', 'clientref1', 'reference_1', 'client_reference_1', 'ref1', 'ref_1']
|
||
'fusion_claims.field_so_delivery_date': ['delivery_date', 'deliverydate', 'adp_delivery', 'deliver_date', 'date_delivery']
|
||
'fusion_claims.field_sol_serial': ['serial_number', 'serial', 'sn', 'serialno']
|
||
'fusion_claims.field_product_code': ['adp_code', 'adp_device', 'device_code', 'adp_sku', 'product_code']
|
||
```
|
||
|
||
Add your own pattern to this dict if you find a Studio installation that uses an unrecognized naming convention. Keep the substring-match semantics in mind — `'sn'` matches `'business_no'` (oops), so order matters; more-specific patterns should come first if there's a conflict risk.
|
||
|
||
## 27.5 Import wizards (one-shot imports outside the normal workflow)
|
||
|
||
| Wizard | Purpose | Notes |
|
||
|---|---|---|
|
||
| `xml_import_wizard` | Bulk-import ADP XML files → `fusion.client.profile` + `fusion.adp.application.data` | Per-file try/except, builds a result_message with created/updated/errors. Manager-only. |
|
||
| `device_import_wizard` | Import device codes from JSON or CSV → `fusion.adp.device.code` | Auto-detects file type from extension. Handles UTF-8 BOM (`utf-8-sig`), Latin-1 fallback. CSV column detection tries multiple price column names ("ADP Price", "Approved Price" with various spacings) + partial match on any column containing "price". |
|
||
| `adp_import_wizard` | Import historical ADP export `.txt` files (or ZIP) → `fusion_claims.adp.export.record` | Idempotent (skips by filename). ZIP support scans subfolders, ignores `__macosx`, dedupes by base name. Auto-parses vendor code + file date from filename, computes posting period. |
|
||
| `field_mapping_config_wizard` | Visual editor for the field-mapping ICP layer (~25 mappings) | Uses a `DEFAULT_FIELD_MAPPINGS` constant — list of `{model_name, label, default_fc_field, config_param_key}` dicts. Per-row, lets the user pick which actual field on the model the mapping should point at. |
|
||
|
||
## 28. Quick reference: things that surprise newcomers
|
||
|
||
- **Sale type values use kebab-snake**: `adp_odsp`, `march_of_dimes`, `muscular_dystrophy`, `direct_private`, `march_of_dimes` (NOT `mod`, NOT `adp/odsp`).
|
||
- **`x_fc_is_adp_sale` is True for both `adp` AND `adp_odsp`** (the `_compute_is_adp_sale` checks for `'adp' in sale_type`).
|
||
- **`x_fc_sale_type_locked`** is True for ANY funder workflow's non-quotation status — not just ADP's.
|
||
- **The `display_name` of a SO is overridden** to `"<name> - <partner.name>"` (`models/sale_order.py:22-28`).
|
||
- **`is_manually_modified` opts an invoice out of SO→invoice sync** but NOT out of invoice→SO sync. Set it before editing if you want decoupled fields.
|
||
- **The `pdfrw` Python package is optional** — if missing, only the SA Mobility and OW Discretionary wizards lose their PDF filling capability. The module still installs.
|
||
- **`fusion.page11.sign.request` access is granted to `base.group_public`** — needed so the public sign URL `/page11/sign/<token>` can resolve the request server-side. Don't tighten this without changing the public controller.
|
||
- **Many2many serial sync** (`_sync_line_fields_to_sale_order`) syncs across sibling invoices linked to the same `sale.order.line` — editing a serial on the ADP invoice rewrites it on the client invoice and the SO line. Avoid blocking this with `skip_sync` unless you really want decoupled serials.
|
||
- **The kanban kanban ordering is enforced via `_read_group` override** that sorts by `_STATUS_ORDER` — don't sort by `x_fc_adp_application_status` directly in custom views; sort by `x_fc_status_sequence` (computed, indexed).
|
||
- **MOD has its own invoice template** (`fusion_claims.action_report_mod_invoice`) — `account.move.action_mod_send_invoice` renders + attaches it before emailing the case worker; don't use the ADP landscape template for MOD invoices.
|
||
|
||
---
|
||
|
||
## 29. Cross-module integration
|
||
|
||
This module is the **lower-level engine**. Two sibling modules layer on top of it and one sits below as infrastructure. Understanding the integration matrix is critical when changing field names, signatures of helpers, or model schemas.
|
||
|
||
### 29.1 `fusion_tasks` (infrastructure layer — declared in our manifest)
|
||
|
||
| Lives in fusion_tasks | Used by fusion_claims as |
|
||
|---|---|
|
||
| `fusion.email.builder.mixin` | Mixed into `sale.order`, `account.move`, `fusion.technician.task`, `fusion.page11.sign.request` via `_inherit`. Provides `_email_build(title, summary, sections, note, ...)` — every `_send_*_email` method in the module routes through it. |
|
||
| `fusion.technician.task` (base, 3,208 lines) | Inherited (`_inherit`, NOT `_inherits`) by `models/technician_task.py` to add `sale_order_id` + `purchase_order_id` + rental inspection fields. fusion_tasks owns the calendar, GPS tracking, scheduling, push notifications, cross-instance sync. |
|
||
| `fusion.task.sync.config` (cross-instance sync) | The Westin↔Mobility task-sync feature. Each instance has a `fusion_claims.sync_instance_id` ICP (e.g. `westin`). Tasks marked with `x_fc_sync_source` are read-only "shadow" tasks from the other instance. Match between instances by `x_fc_tech_sync_id` on `res.users`. JSON-RPC API key auth. Terminal statuses (`completed`, `cancelled`) stop sync. |
|
||
| `fusion.technician.location` | Per-technician GPS history for the admin map. |
|
||
| `fusion.push.subscription` | Web Push subscriptions for technician phones. |
|
||
| `x_fc_is_field_staff` on `res.users` | Filter for the `technician_id` domain on tasks. |
|
||
| `x_fc_tech_sync_id` on `res.users` | Cross-instance technician matching. |
|
||
| `group_field_technician` | Auto-populated with all internal users on install (`_fusion_tasks_post_init`). |
|
||
| ICP defaults set by post-init | `fusion_claims.google_maps_api_key`, `store_open_hour` (9.0), `store_close_hour` (18.0), `push_enabled`, `push_advance_minutes` (30), `sync_instance_id`, `technician_start_address` |
|
||
| `res.company` settings | `x_fc_mod_followup_assignee_mode`, `x_fc_mod_followup_office_contact_id` — owned by fusion_tasks but referenced by fusion_claims MOD follow-up crons (see memory `[[project_fusion_tasks_sync]]`). |
|
||
| `_email_is_enabled` etc. | Helpers on `sale.order` come from the mixin chain. |
|
||
| MEMORY: `[[project_fusion_tasks_sync]]` (Westin/Mobility task sync) | When "tasks not syncing", check `res.users.x_fc_tech_sync_id` first — silent failure when missing or duplicated. |
|
||
|
||
**`fusion.technician.task` lifecycle hooks** that fusion_claims overrides:
|
||
|
||
- `_create_vals_fill` — pre-fill address from SO/PO during create.
|
||
- `_on_create_post_actions` — chatter notice + optional SO `ready_delivery` advancement.
|
||
- `_check_completion_requirements` — rental pickup must have inspection done.
|
||
- `_on_complete_extra` — ODSP `ready_delivery` → `delivered`; rental security-deposit refund / damage activity.
|
||
- `_on_cancel_extra` — delivery cancellation reverts SO to `x_fc_status_before_delivery`.
|
||
|
||
The whole technician task → sale order coupling lives in `fusion_claims/models/technician_task.py:674` — and the calendar / map / scheduling logic stays in the base `fusion.technician.task` model in fusion_tasks.
|
||
|
||
### 29.2 `fusion_authorizer_portal` (portal layer — undeclared but co-installed)
|
||
|
||
fusion_authorizer_portal manifest declares `fusion_claims` + `fusion_tasks` + `fusion_loaners_management` as hard deps. fusion_claims uses APIs that only exist when fusion_authorizer_portal is installed — see the dependency note at the top of §2.
|
||
|
||
| Provided by fusion_authorizer_portal | Used by fusion_claims |
|
||
|---|---|
|
||
| `PDFTemplateFiller` class (`utils/pdf_filler.py`) | `sale.order._apply_pod_signature_to_approval_form` imports it. Same pattern as Odoo Enterprise Sign module — overlays text/checkmarks/signatures via reportlab Canvas + `mergePage()`. |
|
||
| `fusion.pdf.template` model + `fusion.pdf.template.field` + `fusion.pdf.template.preview` | Drag-and-drop visual editor for placing fields on PDF preview images. Categories: `adp`, `mod`, `odsp`, `hardship`, `other`. fusion_claims searches for `(category='odsp', state='active')` for SA Mobility / OW signature overlays. The Page 11 wizard searches for `name ilike 'adp_page_11'` or `'page 11'`. |
|
||
| `fusion.assessment` model (1,636 lines) | The OT assessment form, captures all wheelchair specs + signatures. `page11_sign_request._generate_signed_pdf` reads `fusion.assessment` records linked by `sale_order_id` to populate signing context (client name, address, type, signatures). |
|
||
| `fusion.accessibility.assessment` model (966 lines) | For MOD/accessibility cases — separate from ADP OT assessment. |
|
||
| `fusion.adp.document` (183 lines) | Document tracking on portal cases. |
|
||
| `fusion.authorizer.comment` (85 lines) | Authorizer/sales-rep comments shown on case pages. |
|
||
| `/page11/sign/<token>` public route | Handles the URL that `fusion.page11.sign.request._send_signing_email` sends to clients/agents. 3 routes: form display, submit (writes signature_data + agent fields + state='signed', triggers `_generate_signed_pdf` + `_update_sale_order`), download. Public auth — depends on `base.group_public` ACL on `fusion.page11.sign.request`. |
|
||
| `/my/authorizer/*` routes | OT portal — list cases, view case, upload documents, add comments, download signed pages. |
|
||
| `/my/sales/*` routes | Sales rep portal — list cases, view case, add comments. |
|
||
| `/my/technician/*` routes | Technician portal — task list, action buttons (start/complete/cancel), tomorrow's schedule, location logging, push subscribe, delivery POD signing. |
|
||
| `/my/assessment/*` routes | Assessment creation/edit, signature capture, "express" mode (quick wheelchair specs without full form), complete + auto-create-SO. |
|
||
| `/my/pod/*` routes | Client-facing POD signing on delivery. |
|
||
| `/my/accessibility/*` routes | MOD-style accessibility forms (stairlift straight/curved, VPL, ramp, bathroom). |
|
||
| `/my/timezone/detect` | JS-driven timezone detection for portal users. |
|
||
|
||
**Why this matters for fusion_claims development:**
|
||
|
||
- Renaming a field on `sale.order` likely affects portal templates (`portal_templates.xml`, `portal_assessment_express.xml`, `portal_accessibility_*.xml`) that reference it via QWeb.
|
||
- Adding a new `x_fc_adp_application_status` value may need a portal-side handler in `portal_main.py` to render the new state.
|
||
- The `fusion.pdf.template` schema (page-positioned fields) is the ground truth for ODSP signature placement — DON'T hard-code coordinates in fusion_claims when you could create a template field instead.
|
||
- The `_reactivate_views` post-init hook on fusion_authorizer_portal exists specifically because the inheritance from this module's views is fragile — if you rename a field referenced by an xpath in fusion_authorizer_portal, that view goes dead and stays dead.
|
||
|
||
### 29.3 Other co-installed Nexa modules
|
||
|
||
| Module | What it provides | Used by fusion_claims |
|
||
|---|---|---|
|
||
| `fusion_ringcentral` | RingCentral softphone, click-to-dial widget, fax composer | Click-to-dial works on any phone field — no direct API calls from this module |
|
||
| `fusion_faxes` | `fusion_faxes.send.fax.wizard` + `partner.x_ff_fax_number` | Hard-soft-dep: `odsp_submit_to_odsp_wizard` calls the fax wizard for ODSP submissions |
|
||
| `fusion_loaners_management` | Loaner equipment lending | fusion_authorizer_portal depends on this; fusion_claims doesn't touch it directly |
|
||
| `fusion_pdf_preview` | PDF preview client action + report intercept | Project CLAUDE.md says prefer this over `act_url`+`target=new` for attachments. fusion_claims still has legacy attachment buttons using the old pattern — see gotcha #12 |
|
||
|
||
## 30. Per-funder workflow state machines
|
||
|
||
For every funder, the same four artifacts are documented: **status enum**, **action methods or write-driven transitions**, **emails fired per transition**, **wizards that gate the transitions**. The ADP workflow (§4) is the most elaborate; the table below cross-references the others.
|
||
|
||
### 30.1 ADP (`x_fc_adp_application_status`, 22 states)
|
||
|
||
See §4 for the full spec. Status order via `_STATUS_ORDER` dict. Kanban groups via `_expand_adp_application_statuses` keep core states always visible. Status sequence (kanban left-to-right):
|
||
|
||
```
|
||
quotation → assessment_scheduled → assessment_completed → waiting_for_application →
|
||
application_received → ready_submission → submitted → accepted → resubmitted →
|
||
needs_correction → approved/approved_deduction → ready_delivery → ready_bill → billed →
|
||
case_closed. Special: on_hold, rejected, denied, withdrawn, cancelled, expired.
|
||
```
|
||
|
||
| Transition | Wizard | Email | Side effects |
|
||
|---|---|---|---|
|
||
| `quotation → assessment_scheduled` | `schedule_assessment_wizard` | `_send_assessment_scheduled_email` | Creates `calendar.event` with 1-day alarm |
|
||
| `assessment_scheduled → assessment_completed` | `assessment_completed_wizard` | `_send_assessment_completed_email` | Auto-transitions to `waiting_for_application` after write |
|
||
| `quotation → assessment_completed` (override) | `assessment_completed_wizard` with `override_reason` required | (same) | Yellow chatter card with override reason |
|
||
| `waiting_for_application → application_received` | `application_received_wizard` (3 intake modes: bundled/separate/remote) | `_send_application_received_email` | PDF magic-byte check, sets `x_fc_pages_11_12_in_original` for bundled mode |
|
||
| `application_received → ready_submission` | `ready_for_submission_wizard` | none | Validates client refs, claim auth date, reason, prev funding (if applicable), original PDF, signed pages |
|
||
| `ready_submission → submitted/resubmitted` | `submission_verification_wizard` w/ `submit_application=True` context | `_send_submission_email` (with PDF+XML attachments) | Creates `fusion.submission.history` record. Resets `x_fc_acceptance_reminder_sent` |
|
||
| `submitted → accepted` | `device_approval_wizard` from "Accepted" button | `_send_accepted_email` (no authorizer) | Updates submission history `result='accepted'` |
|
||
| `submitted → rejected` | `status_change_reason_wizard` | `_send_rejection_email` | Updates submission history `result='rejected'` w/ reason |
|
||
| `accepted → approved/approved_deduction` | `device_approval_wizard` w/ `mark_as_approved=True` context | `_send_approval_email` (with Approved Items PDF) | Stage 2 verification, claim number + approval letter saved, deductions set; if any deduction → `approved_deduction` |
|
||
| `* → needs_correction` | `status_change_reason_wizard` | `_send_correction_needed_email` | **Clears `x_fc_final_submitted_application`, `xml_file`, `claim_submission_date`** (preserved in chatter first) |
|
||
| `approved → ready_delivery` | (auto via task creation or `ready_for_delivery_wizard`) | `_send_ready_for_delivery_email` (no authorizer) | Creates `fusion.technician.task` w/ `pod_required=True`. Adds delivery technicians, scheduled datetime |
|
||
| `ready_delivery → ready_bill` | `ready_to_bill_wizard` | none | Requires POD + delivery date + verification complete |
|
||
| `ready_bill → billed` | "Mark as Billed" button | `_send_billed_summary_email` (no client) | Stamps `x_fc_billing_date` |
|
||
| `billed → case_closed` | `case_close_verification_wizard` (4-check gate) OR `_cron_auto_close_billed_cases` (30d after billed) | `_send_case_closed_email` | None |
|
||
| `approved/approved_deduction → on_hold` | `status_change_reason_wizard` (hold reason) | `_send_on_hold_email` | Records `x_fc_previous_status_before_hold`. Hold ONLY allowed from approved per 2026-04 |
|
||
| `on_hold → previous status` | "Resume" button | none | **3-month assessment expiry block** — rejects if `x_fc_assessment_end_date > 90 days ago` |
|
||
| `* → withdrawn` (cancel) | `status_change_reason_wizard` w/ `intent='cancel'` | `_send_withdrawal_email(intent='cancel')` (red urgent) | Permanent |
|
||
| `* → withdrawn` (resubmit) | `status_change_reason_wizard` w/ `intent='resubmit'` | `_send_withdrawal_email(intent='resubmit')` (amber attention) | Back to ready_submission |
|
||
| `* → denied` | `status_change_reason_wizard` | `_send_denial_email` | Records `x_fc_denial_reason` + free text |
|
||
| `denied → ready_submission` | "Resubmit from Denied" button | none | Fresh attempt |
|
||
| `* → cancelled` | `status_change_reason_wizard` | `_send_cancelled_email` | Records `x_fc_previous_status_before_cancel`. **`x_fc_cancel_reported_to_adp`** controls reopen behaviour |
|
||
| `cancelled → previous status` | "Reopen Cancelled" button | none | Only if `x_fc_cancel_reported_to_adp=False`. Otherwise redirects to `action_adp_duplicate_for_reassessment` |
|
||
| `expired/cancelled → new SO` | `action_adp_duplicate_for_reassessment` | none | Creates new SO with `x_fc_previous_sale_order_id` back-pointer |
|
||
| (12 months after approved) | `_cron_adp_expire_approved` | `_send_expired_email` | Auto-`expired` |
|
||
|
||
### 30.2 MOD — March of Dimes (`x_fc_mod_status`, 16 states, default `need_to_schedule`)
|
||
|
||
```
|
||
need_to_schedule → assessment_scheduled → assessment_completed → processing_drawings →
|
||
quote_submitted ─┬─ awaiting_funding → funding_approved → contract_received → in_production →
|
||
│ └─ funding_denied → resubmit/cancel
|
||
└─ handoff_to_client → awaiting_funding (after mod_submission_confirmed_wizard)
|
||
project_complete → pod_submitted → case_closed. Special: on_hold, cancelled.
|
||
```
|
||
|
||
| Transition | Wizard / Method | Email | Notes |
|
||
|---|---|---|---|
|
||
| `* → assessment_scheduled` | `action_mod_schedule_assessment` (bare write) | `_send_mod_assessment_scheduled_email` + Twilio SMS `assessment_scheduled` | Stamps `x_fc_mod_assessment_scheduled_date` |
|
||
| `→ assessment_completed` | `action_mod_complete_assessment` (bare write) | `_send_mod_assessment_completed_email` | Stamps `x_fc_mod_assessment_completed_date` |
|
||
| `→ processing_drawings → quote_submitted` | `action_mod_processing_drawing` opens `send_to_mod_wizard` (`mod_wizard_mode='drawing'`) | `_send_mod_quote_submitted_email` (Client + Authorizer + MOD contact) | Stamps `x_fc_mod_drawing_submitted_date`. Status moves through `processing_drawings` only briefly. |
|
||
| `→ awaiting_funding` (internal path) | `mod_awaiting_funding_wizard` | none | Captures `x_fc_mod_application_submitted_date` |
|
||
| `quote_submitted → handoff_to_client` | `action_mod_handoff_to_client` (requires `submitted_by ∈ {client, authorizer}`, `proposal_doc`, `drawing`) | `_send_mod_handoff_email` (client+authorizer variants differ) | Triggers `_cron_mod_handoff_followup` activities |
|
||
| `handoff_to_client → awaiting_funding` | `mod_submission_confirmed_wizard` (5 confirmation sources) | none | Confirms client/authorizer submitted |
|
||
| Set submission path | `mod_submission_path_wizard` (`internal`/`client`/`authorizer`) | First-time `internal` → `_send_mod_vod_request_email` (with blank VOD form attached) | Sets `x_fc_mod_submitted_by`. VOD email goes to authorizer with the form template from `company.x_fc_mod_vod_form` |
|
||
| `→ funding_approved` | `mod_funding_approved_wizard` | `_send_mod_funding_approved_email` + Twilio SMS `funding_approved` | Records case worker + HVMP ref. Auto-stamps `x_fc_case_approved=today` if blank. |
|
||
| `→ funding_denied` | `mod_funding_denied_wizard` (5 denial categories) | `_send_mod_funding_denied_email` | Writes `x_fc_mod_funding_denial_reason` as `'[{category}] {details}'` |
|
||
| `funding_denied → processing_drawings` | `mod_resubmit_wizard` (`revision_notes` req'd, optional `clear_old_documents`) | none | Resubmission path |
|
||
| `funding_denied → cancelled` | `action_mod_cancel_from_denied` | none | Permanent |
|
||
| `cancelled → need_to_schedule` | `action_mod_reopen_cancelled` | none | Clears `x_fc_mod_funding_denial_reason` |
|
||
| `→ contract_received` | `mod_pca_received_wizard` (full or partial approval — creates 1 or 2 invoices) | `_send_mod_contract_received_email` (Client only) | Uploads PCA, splits invoice if partial. Stamps `x_fc_mod_pca_received_date`, `x_fc_mod_payment_commitment`. |
|
||
| `→ in_production` | `action_mod_in_production` (bare write) | Twilio SMS `initial_payment_received` (no email) | Stamps `x_fc_mod_production_started_date` |
|
||
| `→ project_complete` | `action_mod_project_complete` (bare write) | `_send_mod_project_complete_email` + Twilio SMS `project_complete` | Stamps `x_fc_mod_project_completed_date` |
|
||
| `→ pod_submitted` | `action_mod_pod_submitted` opens `send_to_mod_wizard` (`mod_wizard_mode='completion'`) | `_send_mod_pod_submitted_email` (Client + Authorizer + MOD contact, 2026-04 fix) | Requires completion photos + POD |
|
||
| `→ case_closed` | `action_mod_close_case` (bare write) | `_send_mod_case_closed_email` | Stamps `x_fc_mod_case_closed_date`. Email mentions 1-year warranty. |
|
||
| `→ on_hold` | `action_mod_on_hold` | none | Records `x_fc_mod_previous_status_before_hold` (2026-04 fix; was hardcoded to in_production) |
|
||
| `on_hold → previous status` | `action_mod_resume` | Green chatter card | Restores from `x_fc_mod_previous_status_before_hold` |
|
||
| Built-in `action_cancel` | (override) | `_send_mod_cancelled_email` | When SO is cancelled, MOD status also goes to `cancelled` |
|
||
| Standalone "Send to MOD" | `send_to_mod_wizard` (no mode-driven status change, just sends) | `_send_mod_*_email` based on `send_mode` | Used outside the regular flow for ad-hoc sends |
|
||
| Daily cron | `_cron_mod_schedule_followups` | none (creates activity) | Creates bi-weekly follow-up activities for `quote_submitted`/`awaiting_funding`. Cap 2/30 days. |
|
||
| Daily cron | `_cron_mod_escalate_followups` | `_send_mod_followup_email` (when not capped) | Auto-emails 3 days after activity overdue. Unlinks activity on send. |
|
||
| Daily cron | `_cron_mod_handoff_followup` | none (creates activity) | Creates office-call activities for `handoff_to_client` orders |
|
||
|
||
### 30.3 ODSP — SA Mobility division (`x_fc_sa_status`, 12 states)
|
||
|
||
```
|
||
quotation → form_ready → submitted_to_sa → pre_approved → ready_delivery → delivered →
|
||
pod_submitted → payment_received → case_closed. Special: on_hold, cancelled, denied.
|
||
```
|
||
|
||
| Transition | Wizard / Method | Notes |
|
||
|---|---|---|
|
||
| `quotation → form_ready` | `odsp_sa_mobility_wizard` (fills gov form 13007E with pdfrw) | Generates filled SA Mobility PDF. 3 transient line types: parts, labour, fees, each with tax calc. |
|
||
| `form_ready → submitted_to_sa` | `odsp_submit_to_odsp_wizard` (Email / Fax / Email+Fax modes) — calls `_sa_mobility_submit_documents` | Email goes to `fusion_claims.sa_mobility_email` (default `samobility@ontario.ca`). Attaches signed SA form + POD + invoice PDFs. |
|
||
| `submitted_to_sa → pre_approved` | `odsp_pre_approved_wizard` (upload approval PDF — routes to `x_fc_sa_approval_form`) | Stores PDF as attachment |
|
||
| `pre_approved → ready_delivery` | `odsp_ready_delivery_wizard` (configure signature page + PDF preview overlay) | Opens delivery task form. Auto-advances via task hook `mark_odsp_ready_for_delivery=True` |
|
||
| `ready_delivery → delivered` | Auto when delivery task completes (technician_task hook `_on_complete_extra`) | OR `_apply_pod_signature_to_approval_form` fires when POD signature is set on SO |
|
||
| `delivered → pod_submitted` | "POD Submitted" button (probably `action_odsp_pod_submitted`) | none |
|
||
| `pod_submitted → payment_received` | Auto when paid invoice's `payment_state ∈ {paid, in_payment}` (account_move hook `_auto_advance_odsp_on_payment`) | Triggers `_send_odsp_submission_email` |
|
||
| `payment_received → case_closed` | `_cron_auto_close_odsp_paid_cases` (7 days later) | OR `action_odsp_close_case` |
|
||
| `→ on_hold` | `action_odsp_on_hold` | Records `x_fc_odsp_previous_status_before_hold` |
|
||
| `on_hold → previous status` | `action_odsp_resume` | 2026-04 fix; was hardcoded to `quotation` |
|
||
| `→ denied` | `action_odsp_denied` | none |
|
||
| `denied → submitted_to_sa` | (manual selection) | "Move back to Submitted to SA Mobility to reapply" per the help text |
|
||
|
||
### 30.4 ODSP — Standard division (`x_fc_odsp_std_status`, 11 states)
|
||
|
||
```
|
||
quotation → submitted_to_odsp → pre_approved → ready_delivery → delivered →
|
||
pod_submitted → payment_received → case_closed. Special: on_hold, cancelled, denied.
|
||
```
|
||
|
||
Same pattern as SA Mobility but no `form_ready` step (no gov form filling — uses regular quotation PDF). Submission flow uses `_odsp_std_submit_documents` (attaches approval doc + POD + invoice; auto-creates invoice if missing).
|
||
|
||
| Transition | Wizard / Method | Notes |
|
||
|---|---|---|
|
||
| `quotation → submitted_to_odsp` | `odsp_submit_to_odsp_wizard` | Email/Fax/Email+Fax. Quote PDF + authorizer letter. |
|
||
| `submitted_to_odsp → pre_approved` | `odsp_pre_approved_wizard` (routes to `x_fc_odsp_approval_document`) | |
|
||
| `pre_approved → ready_delivery` | `odsp_ready_delivery_wizard` | Same as SA |
|
||
| `payment_received → case_closed` | `_cron_auto_close_odsp_paid_cases` 7 days after | |
|
||
|
||
### 30.5 ODSP — Ontario Works division (`x_fc_ow_status`, 10 states — PAYMENT BEFORE DELIVERY)
|
||
|
||
```
|
||
quotation → documents_ready → submitted_to_ow → payment_received → ready_delivery →
|
||
delivered → case_closed. Special: on_hold, cancelled, denied.
|
||
```
|
||
|
||
**Critical: OW pays UPFRONT.** `payment_received` comes BEFORE `ready_delivery`. The auto-close cron treats `delivered` as the closer trigger for OW (not `payment_received`).
|
||
|
||
| Transition | Wizard / Method | Notes |
|
||
|---|---|---|
|
||
| `→ documents_ready` | `odsp_discretionary_wizard` (fills gov Discretionary Benefits form via PyPDF2) | Encrypted PDF — pdfrw can't decrypt; PyPDF2 handles it. Field names misleading (see §14.3.1). |
|
||
| `documents_ready → submitted_to_ow` | `odsp_submit_to_odsp_wizard` Email/Fax | |
|
||
| `submitted_to_ow → payment_received` | `action_odsp_payment_received` → `_ow_payment_create_invoice` | Auto-confirms SO if needed, calls `_create_invoices()`, advances status. Chatter mentions invoice ref. |
|
||
| `payment_received → ready_delivery` | (manual or via delivery task) | None |
|
||
| `ready_delivery → delivered` | technician_task `_on_complete_extra` hook | OW Ontario Works funder emails per `_HARDSHIP/MDC` patterns N/A here — OW has bespoke flow |
|
||
| `delivered → case_closed` | `_cron_auto_close_odsp_paid_cases` 7 days after **`delivered`** | **NOT `payment_received`** for OW |
|
||
|
||
### 30.6 WSIB (`x_fc_wsib_status`, 15 states)
|
||
|
||
```
|
||
quotation → assessment_scheduled → assessment_completed → documents_ready →
|
||
submitted_to_wsib → pre_approved → ready_delivery → delivered → pod_submitted →
|
||
invoice_submitted → payment_received → case_closed. Special: on_hold, denied, cancelled.
|
||
```
|
||
|
||
Email triggers via `_WSIB_EMAIL_TRIGGERS` dict (`models/sale_order.py:10572`):
|
||
|
||
| Status target | Emails fired (via `_fire_funder_emails`) |
|
||
|---|---|
|
||
| `documents_ready` | `_send_funder_package_ready_client_email`, `_send_funder_package_ready_authorizer_email` |
|
||
| `pre_approved` | `_send_funder_approval_client_email`, `_send_funder_approval_authorizer_email` (attaches `x_fc_wsib_approval_letter`) |
|
||
| `delivered` | `_send_funder_delivered_client_email`, `_send_funder_delivered_authorizer_email` |
|
||
| `case_closed` | `_send_funder_case_closed_client_email` (client only) |
|
||
| `denied` | `_send_funder_denial_client_email`, `_send_funder_denial_authorizer_email` |
|
||
|
||
Identifier fields: `x_fc_wsib_claim_number`, `x_fc_wsib_adjudicator_name`, `x_fc_wsib_form_7_date`, `x_fc_wsib_approval_date`, `x_fc_wsib_approval_letter`.
|
||
|
||
### 30.7 Insurance (`x_fc_insurance_status`, 16 states — BRANCHED FLOW)
|
||
|
||
**Two divergent paths from `documents_ready`:** client-submit vs direct-bill.
|
||
|
||
```
|
||
quotation → home_assessment_scheduled → home_assessment_completed → documents_ready
|
||
├─ Client-submit: → submitted_by_client → approval_received → payment_received_from_client → ...
|
||
└─ Direct-bill: → pre_auth_submitted → pre_auth_approved → ...
|
||
|
||
Both converge: → product_ordered → delivered → pod_to_client / pod_invoice_submitted →
|
||
payment_received → case_closed. Special: on_hold, denied, cancelled.
|
||
```
|
||
|
||
`x_fc_insurance_submission_mode` controls which branch (probably). Identifier fields: `x_fc_insurance_company_id`, `x_fc_insurance_policy_number`, `x_fc_insurance_claim_number`, `x_fc_insurance_pre_auth_amount`, `x_fc_insurance_pre_auth_expiry`, `x_fc_insurance_home_assessment_required`, `x_fc_insurance_letter_source`, `x_fc_insurance_approval_letter`.
|
||
|
||
Email triggers via `_INSURANCE_EMAIL_TRIGGERS`:
|
||
- `documents_ready` → client only
|
||
- `approval_received` / `pre_auth_approved` → client + authorizer
|
||
- `delivered` → client + authorizer
|
||
- `case_closed` → client only
|
||
- `denied` → client + authorizer
|
||
|
||
### 30.8 MDC — Muscular Dystrophy Canada (`x_fc_mdc_status`, 16 states)
|
||
|
||
```
|
||
quotation → awaiting_ot_letter → documents_ready → submitted_to_mdc → po_received →
|
||
product_ordered → delivered → pod_invoice_submitted → awaiting_payment → payment_received →
|
||
case_closed. Special: not_enrolled, on_hold, denied, withdrawn, cancelled.
|
||
```
|
||
|
||
Distinctive: `awaiting_ot_letter` step, dedicated `po_received` (instead of `approved`), and `not_enrolled` terminal-like state (client needs to be a MDC member). Help text: "From Client Not Enrolled, move back to Quotation once enrollment is verified."
|
||
|
||
Identifier fields: `x_fc_mdc_client_id_number`, `x_fc_mdc_enrollment_verified`, `x_fc_mdc_enrollment_verified_date`, `x_fc_mdc_submitted_by`, `x_fc_mdc_po_number`, `x_fc_mdc_po_date`, `x_fc_mdc_po_amount`, `x_fc_mdc_payment_due_date`, `x_fc_mdc_letter_source`, `x_fc_mdc_po_document`.
|
||
|
||
Email triggers via `_MDC_EMAIL_TRIGGERS`:
|
||
- `documents_ready` → client + authorizer
|
||
- `po_received` → client + authorizer (this is the "approval" trigger — attaches `x_fc_mdc_po_document`)
|
||
- `delivered` → client + authorizer
|
||
- `case_closed` → client only
|
||
- `denied` → client + authorizer
|
||
|
||
### 30.9 Hardship Funding (`x_fc_hardship_status`, 16 states)
|
||
|
||
```
|
||
quotation → awaiting_pre_assessment → pre_assessment_complete → application_package_ready →
|
||
submitted_to_hf → eligibility_interview → approval_received → product_ordered → delivered →
|
||
pod_invoice_submitted → payment_received → case_closed.
|
||
Special: eligibility_failed, on_hold, denied, cancelled.
|
||
```
|
||
|
||
Distinctive: pre-assessment + eligibility interview steps. Identifier fields: `x_fc_hardship_funder_id`, `x_fc_hardship_submitted_by`, `x_fc_hardship_interview_date`, `x_fc_hardship_approval_date`, `x_fc_hardship_approval_amount`, `x_fc_hardship_client_portion`, `x_fc_hardship_approval_received_via`, `x_fc_hardship_pre_assessment_source`, `x_fc_hardship_approval_letter`.
|
||
|
||
Email triggers via `_HARDSHIP_EMAIL_TRIGGERS`:
|
||
- `application_package_ready` → client + authorizer (note: different status name from other funders — not `documents_ready`)
|
||
- `approval_received` → client + authorizer
|
||
- `delivered` → client + authorizer
|
||
- `case_closed` → client only
|
||
- `denied` / **`eligibility_failed`** → client + authorizer (special — `eligibility_failed` also fires the denial path)
|
||
|
||
### 30.10 Direct/Private, Rental, Other (no funder workflow)
|
||
|
||
These sale types have NO funder-specific status field. They use the standard Odoo SO lifecycle (`draft → sent → sale → done`). The split-invoicing logic still applies via `_create_adp_split_invoice` if `_is_adp_sale()` is True (which it isn't for these types), but they typically bill the client directly via the standard "Create Invoice" wizard.
|
||
|
||
`Rental` specifically has rental-product extensions on `product.template` (`x_fc_security_deposit_type/amount/percent`) and integrates with the technician task's rental inspection flow (`task_type='pickup'` triggers inspection requirements; see §13).
|
||
|
||
### 30.11 Funder workflow comparison
|
||
|
||
| Aspect | ADP | MOD | ODSP-SA | ODSP-Std | ODSP-OW | WSIB | Insurance | MDC | Hardship |
|
||
|---|---|---|---|---|---|---|---|---|---|
|
||
| # states | 22 | 16 | 12 | 11 | 10 | 15 | 16 | 16 | 16 |
|
||
| Authorizer required | Yes (always) | Yes (always) | Optional | Optional | Optional | Yes (always) | Optional | Yes (always) | Optional |
|
||
| Pays the vendor or client? | Vendor | Vendor (90% upfront, 10% on POD) | Vendor | Vendor | **Vendor (upfront)** | Vendor | Either (branched) | Vendor | Vendor |
|
||
| Order of payment vs delivery | Delivery → bill → pay | Payment 90% → deliver → 10% | Deliver → bill → pay | Deliver → bill → pay | **Pay → deliver** | Deliver → bill → pay | Either order | Deliver → bill → pay | Deliver → bill → pay |
|
||
| Government PDF form filled | Yes (Page 11/12) | No (proposal docs) | Yes (SA Mobility 13007E via pdfrw) | No | **Yes (Discretionary Benefits via PyPDF2 — AES-encrypted)** | No (uses Form 7 manually) | No | No | No |
|
||
| Custom email senders | ~25 (`_adp_send_*`) | ~14 (`_send_mod_*`) | `_send_sa_mobility_*`, `_send_odsp_*` | `_send_odsp_*` | (uses standard SO confirm) | Generic funder emails | Generic funder emails | Generic funder emails | Generic funder emails |
|
||
| Twilio SMS | No | Yes (4 templates) | No | No | No | No | No | No | No |
|
||
| Split invoicing | Yes (Client 25% + ADP 75% for REG) | Yes (Client + MOD on partial PCA) | No | No | No | No | No | No | No |
|
||
| Cron auto-close | 30d after `billed` | (none) | 7d after `payment_received` | 7d after `payment_received` | **7d after `delivered`** | (none) | (none) | (none) | (none) |
|
||
| Cron auto-expire | 12 months after `approved` | No | No | No | No | No | No | No | No |
|
||
| Submission history records | Yes (`fusion.submission.history`) | No | No | No | No | No | No | No | No |
|
||
| Document locks by stage | Yes (3 stages: submitted/approved/billed) | No | No | No | No | No | No | No | No |
|
||
| Case lock available | Yes (`x_fc_case_locked`) | No | No | No | No | No | No | No | No |
|
||
|
||
---
|
||
|
||
## 31. `fusion.technician.task` base model deep dive (lives in `fusion_tasks`)
|
||
|
||
The 3,208-line base. fusion_claims hooks override 5 methods; the rest is the scheduling + GPS + push notification engine.
|
||
|
||
### 31.1 Task state machine
|
||
|
||
```python
|
||
status = fields.Selection([
|
||
('pending', 'Pending'),
|
||
('scheduled', 'Scheduled'), # default
|
||
('en_route', 'En Route'),
|
||
('in_progress', 'In Progress'),
|
||
('completed', 'Completed'),
|
||
('cancelled', 'Cancelled'),
|
||
('rescheduled', 'Rescheduled'),
|
||
])
|
||
```
|
||
|
||
Transitions (`action_*` methods, `fusion_tasks/models/technician_task.py`):
|
||
|
||
| From | Action | To | Side effects |
|
||
|---|---|---|---|
|
||
| `scheduled` | `action_start_en_route` | `en_route` | Writes GPS, posts chatter, sends `_send_task_en_route_email`, recalculates travel from current location, sends push notification to tech ("En Route to {client}, {N} more task(s) today"), pushes status to remote if shadow |
|
||
| `scheduled` / `en_route` | `action_start_task` | `in_progress` | Writes `started_latitude/longitude`, posts chatter |
|
||
| `scheduled` / `en_route` / `in_progress` | `action_complete_task` | `completed` | Sets `completion_datetime` + completed GPS, calls hook `_check_completion_requirements`, posts chatter, calls `_post_completion_to_linked_order` (fusion_claims hook), `_notify_scheduler_on_completion`, sends `_send_task_completion_email`, recalculates travel for remaining tasks, calls hook `_on_complete_extra` (fusion_claims hook) |
|
||
| any non-completed | `action_cancel_task` | `cancelled` | Writes GPS, posts chatter, pushes status to remote if shadow, otherwise calls hook `_on_cancel_extra` (defaults to `_send_task_cancelled_email`; fusion_claims overrides to also revert SO status if delivery) |
|
||
| any | `action_reschedule` | (opens form in `reschedule_mode` context) | On save, `write()` detects changes vs `old_date`/`old_time_start`/`old_time_end` and sends `_send_task_rescheduled_email` |
|
||
| any | `action_reset_to_scheduled` | `scheduled` | None |
|
||
|
||
**All actions call `_check_previous_tasks_completed()` first** — blocks the tech from starting a later task before completing earlier ones for the same date. Considers tasks where the tech is either lead OR additional.
|
||
|
||
### 31.2 Scheduling algorithm (`_find_next_available_slot`)
|
||
|
||
The 120-line gem that solves "where can this new task fit?":
|
||
|
||
1. **Domain**: tasks for `(technician_id OR additional_technician_ids)` on the given date, not cancelled, excluding `exclude_task_id` (for edits).
|
||
2. **Build intervals** clamped to store hours (`fusion_claims.store_open_hour` / `_close_hour`, default 9–18).
|
||
3. **Merge calendar events** for the tech via `_get_calendar_busy_intervals` — pulls `calendar.event` records where the tech is an attendee, **excluding events linked to a fusion task** (to avoid double-counting).
|
||
4. **Walk gaps starting from `preferred_start`**: for each booked interval, check if `cursor + duration + travel_to_next` fits before it. Travel time computed by `_quick_travel_time(from_lat, from_lng, to_lat, to_lng)` and **rounded up to the next 15-minute boundary**.
|
||
5. **Jump past booked intervals** adding `travel_from_prev`. Snap cursor to nearest 15 min.
|
||
6. If no gap found from `preferred_start` to close, **wraps and retries from store_open** to catch earlier slots.
|
||
7. Returns `(False, False)` if fully booked.
|
||
|
||
The `_get_available_gaps` function returns the inverse — list of free intervals for the "available slots" badges on the form.
|
||
|
||
### 31.3 Calendar integration
|
||
|
||
- `_get_calendar_busy_intervals` cross-checks against `calendar.event` so a tech blocked by an OT meeting also gets blocked in the scheduler.
|
||
- **Tasks linked to a calendar event** (`calendar_event_id` set) are EXCLUDED from the busy-interval check — otherwise they'd double-count themselves.
|
||
- `_sync_calendar_event` (in the `write` override) creates/updates a `calendar.event` whenever scheduling data changes.
|
||
|
||
### 31.4 Travel time + route optimization
|
||
|
||
- `_quick_travel_time(lat, lng, lat, lng)` — uses Google Distance Matrix via `_calculate_travel_time` (API key from `fusion_claims.google_maps_api_key` ICP).
|
||
- `_osrm_travel` — open-source fallback using OSRM.
|
||
- `_nominatim_geocode` — open-source fallback for geocoding.
|
||
- `_recalculate_day_travel_chains` — when a task moves, rebuilds travel times for ALL tasks for that tech+date.
|
||
- `_recalculate_travel_from_current_location` — when a tech goes `en_route`, recalculates travel from their actual GPS location (not from previous task's destination).
|
||
- `_recalculate_remaining_tasks_travel` — when a task completes, recalculates travel chain for remaining tasks from the completion location.
|
||
- `_cron_calculate_travel_times` — periodic background recalculation.
|
||
|
||
### 31.5 Overlap detection (`_check_no_overlap`)
|
||
|
||
Hard constraint that fires on write. Prevents booking the same tech across overlapping time windows. **Considers additional_technician_ids** — overlap with a task where the tech is additional also blocks.
|
||
|
||
`_snap_if_overlap` is a soft variant invoked during create — attempts to snap the new task to the next free slot via `_find_next_available_slot` instead of raising.
|
||
|
||
### 31.6 Late arrival cron (`_cron_check_late_arrivals`)
|
||
|
||
Scans `scheduled` tasks whose `time_start` was more than X minutes ago without an `en_route` transition. Sends a notification to the scheduler. Sets `x_fc_late_notified=True` to prevent re-firing.
|
||
|
||
### 31.7 Push notifications (`_send_push_notification`)
|
||
|
||
Uses `fusion.push.subscription` records (browser push API). Cron `_cron_send_push_notifications` fires per-task notifications `fusion_claims.push_advance_minutes` (default 30) before `time_start`. Setting `fusion_claims.push_enabled=False` disables all push.
|
||
|
||
### 31.8 Hook seam for fusion_claims
|
||
|
||
```python
|
||
def _check_completion_requirements(self):
|
||
"""Hook: override in fusion_claims for rental inspection."""
|
||
pass
|
||
|
||
def _on_complete_extra(self):
|
||
"""Hook: override in fusion_claims for ODSP advancement + rental inspection."""
|
||
pass
|
||
|
||
def _on_cancel_extra(self):
|
||
"""Hook: defaults to sending cancel email; fusion_claims overrides to also revert SO status."""
|
||
self._send_task_cancelled_email()
|
||
|
||
def _post_completion_to_linked_order(self):
|
||
"""Hook: post completion notes to linked SO/PO chatter. Override in fusion_claims."""
|
||
pass
|
||
|
||
def _create_vals_fill(self, vals):
|
||
"""Hook: pre-fill address from linked SO/PO during create. Override in fusion_claims."""
|
||
pass
|
||
```
|
||
|
||
fusion_claims/models/technician_task.py overrides all five.
|
||
|
||
## 32. `fusion.task.sync.config` cross-instance sync deep dive (lives in `fusion_tasks`)
|
||
|
||
The Westin↔Mobility task-sync mechanic (770 lines, full coverage below).
|
||
|
||
### 32.1 Configuration model
|
||
|
||
`fusion.task.sync.config` records:
|
||
- `name` — display label ("Westin Healthcare", "Mobility Specialties")
|
||
- `instance_id` — short identifier (`westin`, `mobility`) — used as `x_fc_sync_source` value on shadow tasks
|
||
- `url` — remote Odoo URL (e.g. `http://192.168.1.40:8069`)
|
||
- `database` — remote DB name
|
||
- `username` + `api_key` — for JSON-RPC auth
|
||
- `last_sync` + `last_sync_error` — health tracking
|
||
|
||
Local instance identifies itself via the ICP setting `fusion_claims.sync_instance_id`.
|
||
|
||
### 32.2 Technician matching (`x_fc_tech_sync_id` on `res.users`)
|
||
|
||
The same person on two instances has the SAME `x_fc_tech_sync_id` value (e.g. `"gordy"`). The sync uses three helpers:
|
||
|
||
- `_get_local_tech_map()` → `{local_user_id: sync_id}` for active field staff with sync_id set.
|
||
- `_get_remote_tech_map()` → `{sync_id: remote_user_id}` from the remote.
|
||
- `_get_local_syncid_to_uid()` → reverse `{sync_id: local_uid}`.
|
||
|
||
**Duplicate detection**: if two users on the same instance share a sync_id, only the LAST one is used — a warning is logged but the sync proceeds. Per memory `[[project_fusion_tasks_sync]]`, this is a known silent-failure mode — when "tasks not syncing", check duplicates first.
|
||
|
||
### 32.3 Push direction (local → remote)
|
||
|
||
`_push_tasks_to_remote(tasks, operation, local_instance_id)`:
|
||
- Filters tasks where the tech has a matched sync_id.
|
||
- Maps `additional_technician_ids` via sync_ids to the remote uids.
|
||
- Builds a `task_data` dict with **17 fields** including GPS, travel data, completion datetime, name prefixed with `[WESTIN]` or `[MOBILITY]` to make shadow tasks visible.
|
||
- Uses `x_fc_sync_uuid` as the dedup key — searches by UUID before deciding create vs write.
|
||
- On `unlink` operation, writes `status='cancelled', active=False` on the remote shadow instead of deleting.
|
||
- Sends with `context={'skip_task_sync': True, 'skip_travel_recalc': True}` to prevent ping-pong.
|
||
|
||
Triggered from the `write` override on the local instance — every meaningful task change pushes immediately.
|
||
|
||
### 32.4 Shadow-status push (shadow → originator)
|
||
|
||
`_push_shadow_status(shadow_tasks)` — when a tech changes the status of a shadow task locally (en_route / complete / cancel), pushes the new status BACK to the originating instance so the originator's client gets the right emails:
|
||
|
||
1. Looks up the originating instance via `x_fc_sync_source`.
|
||
2. Writes `status` + GPS + completion_datetime to the original task on the remote.
|
||
3. Calls `_trigger_parent_notifications(config, task)` which RPCs the appropriate `_send_*_email` method on the originating instance — `_send_task_completion_email`, `_send_task_en_route_email`, `_send_task_cancelled_email`, or `_notify_scheduler_on_completion`.
|
||
|
||
This is how the **"client gets emailed exactly once, from the originating instance only"** rule is enforced.
|
||
|
||
### 32.5 Pull direction (remote → local) — `_cron_pull_remote_tasks`
|
||
|
||
Daily cron walks all active configs, pulls tasks + technician locations:
|
||
|
||
1. **Match by sync_id**: only sync tasks whose tech is matched on BOTH sides.
|
||
2. **Cutoff**: only fetch tasks with `scheduled_date >= today - 7 days` (recent window).
|
||
3. **Exclude shadows**: `x_fc_sync_source = False` on the remote (don't sync someone else's shadows back).
|
||
4. **Per task**: lookup or create local shadow by `x_fc_sync_uuid`. Name gets prefixed with `[WESTIN]`/`[MOBILITY]` for visibility.
|
||
5. **Re-map additional_technician_ids** from remote uids → local uids via the sync_id table.
|
||
6. **`affected_combos`** set tracks `(tech_id, date)` pairs that changed — used by `_recalculate_day_travel_chains` so route planning accounts for both local AND shadow tasks.
|
||
7. **Stale-shadow cleanup**: tasks whose UUID disappeared from the remote feed get archived (`active=False`) by `_cron_cleanup_old_shadows`.
|
||
|
||
### 32.6 Per-tech location push (`_push_technician_location`)
|
||
|
||
When a tech triggers a status action (en_route, complete), `action_*` calls into this to push their GPS coordinates to the OTHER instance immediately — so the other instance's calendar sees where they are without waiting for the next pull cycle.
|
||
|
||
Creates a `fusion.technician.location` record on the remote with `source='sync'` and `sync_instance=<local_instance_id>`.
|
||
|
||
### 32.7 Sync trigger map
|
||
|
||
| Local event | Pushed to remote? | How |
|
||
|---|---|---|
|
||
| Task create | Yes | `write()` override calls `_push_tasks_to_remote([task], 'create')` |
|
||
| Task write (any change) | Yes | `_push_tasks_to_remote([task], 'write')` |
|
||
| Task unlink | Yes (as cancellation) | Sets remote `active=False, status='cancelled'` |
|
||
| Shadow task status change | Yes (back to originator) | `_push_shadow_status(task)` |
|
||
| Technician GPS update | Yes (broadcast) | `_push_technician_location(user_id, lat, lng, accuracy)` |
|
||
| Periodic full reconciliation | Yes (both directions) | `_cron_pull_remote_tasks` daily |
|
||
|
||
### 32.8 Skip-sync guard rails
|
||
|
||
- `context['skip_task_sync']` — prevents infinite ping-pong between instances.
|
||
- `context['skip_travel_recalc']` — prevents the pull from triggering local recalculations.
|
||
- Terminal-state tasks (`completed`, `cancelled`) — push side does write, but pull side does NOT update existing shadow records that are already terminal (defensive against late race conditions).
|
||
|
||
## 33. `fusion.assessment` (OT assessment model — lives in `fusion_authorizer_portal`)
|
||
|
||
The 1,636-line model that captures an OT's assessment of a client + their equipment needs, then generates the draft sale order.
|
||
|
||
### 33.1 State machine
|
||
|
||
```python
|
||
state = fields.Selection([
|
||
('draft', 'In Progress'),
|
||
('pending_signature', 'Pending Signatures'),
|
||
('completed', 'Completed'),
|
||
('cancelled', 'Cancelled'),
|
||
])
|
||
```
|
||
|
||
### 33.2 Equipment fields — by type
|
||
|
||
Type-driven UI: `equipment_type ∈ {rollator, wheelchair, powerchair}` controls which measurement fields are visible.
|
||
|
||
- **Rollator**: `rollator_type` (Type 1/2/3), `rollator_handle_height`, `rollator_seat_height`, `rollator_addons` (comma-separated text)
|
||
- **Wheelchair**: `wheelchair_type` (Type 1–5 — Standard / Lightweight / Ultra Lightweight / Tilt / Dynamic Tilt), `legrest_length`, `cane_height`, `frame_options`, `wheel_options`, `legrest_options`, `additional_adp_options`, `seatbelt_type` (5 options including 4-Point + Chest Harness), `cushion_info`, `backrest_info`
|
||
- **Powerchair**: `powerchair_type` (Adult Power Base 1/2/3), `powerchair_options`, `specialty_controls` (rationale required)
|
||
|
||
`additional_customization` — free-form notes section.
|
||
|
||
### 33.3 Client type → sale_type mapping
|
||
|
||
The assessment captures `client_type` and `_create_draft_sale_order` translates it:
|
||
|
||
```python
|
||
sale_type = 'adp'
|
||
if self.client_type in ['ods', 'acs', 'owp']:
|
||
sale_type = 'adp_odsp'
|
||
```
|
||
|
||
So an OT picking "ODS - ODSP" client type on the assessment yields an `adp_odsp` sale order on the fusion_claims side — and the ADP/ODSP workflow takes over.
|
||
|
||
### 33.4 Initial workflow status mapping
|
||
|
||
`_create_draft_sale_order` doesn't dump every new SO into `quotation` — it picks the right starting status based on what the OT has done:
|
||
|
||
```python
|
||
if assessment_start_date AND assessment_end_date:
|
||
target_status = 'waiting_for_application' # assessment is done
|
||
elif assessment_start_date:
|
||
target_status = 'assessment_scheduled' # only start filled
|
||
else:
|
||
target_status = 'quotation' # blank assessment
|
||
```
|
||
|
||
This avoids the `write` override's auto-transition from firing (which would happen if the SO entered `assessment_completed` first), letting the assessment-portal flow set the final state directly.
|
||
|
||
### 33.5 Page 11/12 signature capture
|
||
|
||
The model has `signature_page_11` + `signature_page_12` binary fields. `signatures_complete` is computed True when both are present. `action_complete()` raises if signatures incomplete; `action_complete_express()` (used for quick wheelchair specs without full assessment) bypasses the signature check.
|
||
|
||
`page11_sign_request._generate_signed_pdf` reads the most-recent `fusion.assessment` record linked to the SO to populate the signing context (client_first_name, last_name, middle_name, health_card, health_card_version). If no assessment exists, it falls back to `order._get_client_name_parts()`.
|
||
|
||
### 33.6 Output → sale.order
|
||
|
||
`action_complete()` does:
|
||
1. Create or link partner (`_ensure_partner`) — auto-creates `res.partner` from the assessment's client_* fields if `create_new_partner=True`.
|
||
2. `_create_draft_sale_order(partner)` — full SO with:
|
||
- `partner_id`, `user_id` (sales_rep_id), `state='draft'`, `origin=f'Assessment: {reference}'`
|
||
- `x_fc_sale_type`, `x_fc_authorizer_id`, `x_fc_client_ref_1/2`
|
||
- `x_fc_adp_application_status` = target_status from §33.4
|
||
- `assessment_id` back-link (added 2026-04 for traceability)
|
||
3. `_generate_signed_documents()` — renders signed Page 11/12 via `fusion.pdf.template` (category='adp', name like 'adp_page_11').
|
||
4. `_send_completion_notifications()` — emails the office.
|
||
|
||
`action_complete_express()` skips step 3 (signatures) entirely — used for the "express" assessment route from the sales-rep portal where the rep just needs to spec a wheelchair without doing the full ADP assessment.
|
||
|
||
## 34. `fusion.accessibility.assessment` (MOD/accessibility assessment — lives in `fusion_authorizer_portal`)
|
||
|
||
The 966-line sibling for accessibility modifications (not ADP).
|
||
|
||
### 34.1 Assessment types
|
||
|
||
```python
|
||
assessment_type = fields.Selection([
|
||
('stairlift_straight', 'Straight Stair Lift'),
|
||
('stairlift_curved', 'Curved Stair Lift'),
|
||
('vpl', 'Vertical Platform Lift'),
|
||
('ceiling_lift', 'Ceiling Lift'),
|
||
('ramp', 'Custom Ramp'),
|
||
('bathroom', 'Bathroom Modification'),
|
||
('tub_cutout', 'Tub Cutout'),
|
||
])
|
||
```
|
||
|
||
### 34.2 State machine
|
||
|
||
```python
|
||
state = fields.Selection([
|
||
('draft', 'Draft'),
|
||
('scheduled', 'Visit Scheduled'),
|
||
('in_progress', 'Visit In Progress'),
|
||
('pending_review', 'Pending Review'),
|
||
('completed', 'Completed'),
|
||
('cancelled', 'Cancelled'),
|
||
])
|
||
```
|
||
|
||
Six states — `_expand_states` keeps all 6 visible on the kanban regardless of which states have records.
|
||
|
||
### 34.3 Funding source → sale_type
|
||
|
||
`x_fc_funding_source` (2026-04 portal audit fix) is REQUIRED and picks the downstream workflow:
|
||
|
||
```python
|
||
('march_of_dimes', 'March of Dimes'),
|
||
('odsp', 'ODSP'),
|
||
('wsib', 'WSIB'),
|
||
('insurance', 'Private Insurance'),
|
||
('direct_private', 'Private Pay (Direct)'),
|
||
('other', 'Other'),
|
||
```
|
||
|
||
Maps directly to `x_fc_sale_type` on the generated SO — so a MOD-funded ramp project creates an SO with `sale_type='march_of_dimes'` and enters the MOD workflow (§30.2).
|
||
|
||
### 34.4 Booking source
|
||
|
||
```python
|
||
booking_source = fields.Selection([
|
||
('phone_authorizer', 'Phone - Authorizer'),
|
||
('phone_client', 'Phone - Client'),
|
||
('walk_in', 'Walk-In'),
|
||
('portal', 'Online Booking'),
|
||
])
|
||
```
|
||
|
||
Drives the SMS confirmation flag (`sms_confirmation_sent`).
|
||
|
||
### 34.5 Per-type measurement fields
|
||
|
||
The model has hundreds of measurement fields, only some of which are visible per assessment_type (form view conditions). Examples for `stairlift_straight`:
|
||
- `stair_steps` (Number of Steps)
|
||
- `stair_nose_to_nose` (inches)
|
||
- `stair_side` (left/right)
|
||
- `stair_style` (standard / heavy-duty / etc.)
|
||
|
||
`stairlift_curved`, `vpl`, `ceiling_lift`, `ramp`, `bathroom`, `tub_cutout` each have their own set of fields.
|
||
|
||
## 35. `fusion_authorizer_portal` controller routes — detailed
|
||
|
||
Full per-route inventory from `portal_main.py` (2,827 lines), `portal_assessment.py` (1,238), `portal_page11_sign.py` (206), `pdf_editor.py` (218).
|
||
|
||
### 35.1 Page 11 public signing (`portal_page11_sign.py`)
|
||
|
||
| Route | Type | Auth | What it does |
|
||
|---|---|---|---|
|
||
| `/page11/sign/<token>` | http | public | Renders signing form. Resolves request by `access_token`, branches on state (`signed`/`cancelled`/`expired`/`ok`). Pulls assessment data to pre-fill client name/health card. Pre-fills Google Maps API key for address autocomplete. |
|
||
| `/page11/sign/<token>/submit` | http POST | public | Writes signature_data + signer details + agent details (if `consent_signed_by='agent'`), sets state=`signed`, signed_date. Triggers `_generate_signed_pdf` + `_update_sale_order`. |
|
||
| `/page11/sign/<token>/download` | http | public | Downloads the signed PDF (only if `state=signed` and PDF generated). |
|
||
|
||
### 35.2 Authorizer (OT) portal (`portal_main.py`)
|
||
|
||
| Route | Type | Notes |
|
||
|---|---|---|
|
||
| `/my/authorizer/cases/search` | jsonrpc | Live search of cases — used by typeahead |
|
||
| `/my/authorizer/case/<int:order_id>` | http | Case detail page (sale_order view) — chatter, documents, status |
|
||
| `/my/authorizer/case/<int:order_id>/comment` | http POST | Add a comment (creates `fusion.authorizer.comment` record) |
|
||
| `/my/authorizer/case/<int:order_id>/upload` | http POST + csrf | Upload a document — creates `fusion.adp.document` record + binary on the SO |
|
||
| `/my/authorizer/document/<int:doc_id>/download` | http | Download an uploaded document |
|
||
|
||
### 35.3 Sales rep portal (`portal_main.py`)
|
||
|
||
| Route | Type | Notes |
|
||
|---|---|---|
|
||
| `/my/sales/cases/search` | jsonrpc | Live search |
|
||
| `/my/sales/case/<int:order_id>` | http | Sales rep view of an SO |
|
||
| `/my/sales/case/<int:order_id>/comment` | http POST | Add comment |
|
||
|
||
### 35.4 Technician portal (`portal_main.py`)
|
||
|
||
| Route | Type | Notes |
|
||
|---|---|---|
|
||
| `/my/technician/task/<int:task_id>` | http | Task detail page — directions, GPS, client info, action buttons |
|
||
| `/my/technician/task/<int:task_id>/action` | json | Trigger task actions (start_en_route / start / complete / cancel). Includes GPS coordinates in payload. |
|
||
| `/my/technician/tomorrow` | http | Next-day schedule preview |
|
||
| `/my/technician/schedule/<string:date>` | http | View any specific date's schedule |
|
||
| `/my/technician/admin/map` | http | Admin-only map of all techs' locations |
|
||
| `/my/technician/location/log` | json | Log technician GPS heartbeat (background) |
|
||
| `/my/technician/push/subscribe` | json | Subscribe to Web Push notifications |
|
||
| `/my/technician/delivery/<int:order_id>` | http | Delivery-specific page for a SO |
|
||
| `/my/technician/task/<int:task_id>/pod` | http | POD signing page (for any task) |
|
||
| `/my/technician/task/<int:task_id>/pod/sign` | json POST | Submit POD signature for the task |
|
||
|
||
### 35.5 Client-facing POD (`portal_main.py`)
|
||
|
||
| Route | Type | Notes |
|
||
|---|---|---|
|
||
| `/my/pod/<int:order_id>` | http | Client-facing POD page for direct signing on delivery |
|
||
| `/my/pod/<int:order_id>/sign` | json POST | Client submits POD signature |
|
||
|
||
### 35.6 Assessment portal (`portal_assessment.py`)
|
||
|
||
| Route | Type | Notes |
|
||
|---|---|---|
|
||
| `/my/assessment/new` | http | Create a new ADP assessment (full form) |
|
||
| `/my/assessment/<int:assessment_id>` | http | Edit an assessment |
|
||
| `/my/assessment/save` | http POST + csrf | Save changes |
|
||
| `/my/assessment/<int:assessment_id>/signatures` | http | Signature capture page (Page 11/12) |
|
||
| `/my/assessment/<int:assessment_id>/save_signature` | jsonrpc | Save Page 11 or Page 12 signature |
|
||
| `/my/assessment/<int:assessment_id>/complete` | http POST + csrf | Mark complete (triggers SO creation) — fails if signatures incomplete |
|
||
| `/my/assessment/express` | http | Quick assessment form (wheelchair specs without signatures) |
|
||
| `/my/assessment/express/<int:assessment_id>` | http | Edit express assessment |
|
||
| `/my/assessment/express/save` | http POST + csrf | Save express assessment + create SO via `action_complete_express` |
|
||
|
||
### 35.7 Accessibility (MOD) portal (`portal_main.py`)
|
||
|
||
| Route | Type | Notes |
|
||
|---|---|---|
|
||
| `/my/accessibility` | http | Landing page — pick assessment type |
|
||
| `/my/accessibility/list` | http | List of existing accessibility assessments |
|
||
| `/my/accessibility/stairlift/straight` | http | New straight stairlift assessment form |
|
||
| `/my/accessibility/stairlift/curved` | http | New curved stairlift assessment form |
|
||
| `/my/accessibility/vpl` | http | New VPL assessment |
|
||
| `/my/accessibility/ramp` | http | New custom ramp assessment |
|
||
| `/my/accessibility/bathroom` | http | New bathroom mod assessment |
|
||
| `/my/accessibility/save` | json POST + csrf | Save accessibility assessment (creates `fusion.accessibility.assessment` record) |
|
||
|
||
### 35.8 PDF template editor (`pdf_editor.py`)
|
||
|
||
| Route | Type | Notes |
|
||
|---|---|---|
|
||
| `/pdf_template/preview/<int:template_id>` | http | Render preview image of the PDF page |
|
||
| `/pdf_template/save_fields` | json POST | Save updated field positions from the visual editor |
|
||
| `/pdf_template/regenerate_previews/<int:template_id>` | http | Trigger preview regeneration |
|
||
|
||
### 35.9 Timezone detection (`portal_main.py`)
|
||
|
||
| Route | Type | Notes |
|
||
|---|---|---|
|
||
| `/my/timezone/detect` | jsonrpc | Logs the user's browser timezone — useful for displaying technician schedules in their local TZ |
|
||
|
||
## 36. `sale.order` constraint methods (`@api.constrains`)
|
||
|
||
10 constraint methods enforce field-level validation rules independent of the status-machine gates. These fire on EVERY write, not just status transitions, so a write to one of these fields with invalid data raises `ValidationError` regardless of context.
|
||
|
||
| Constraint | Field | Rule |
|
||
|---|---|---|
|
||
| `_check_odsp_division_change` | `x_fc_odsp_division` | Blocks changing the division once the active status field is past `quotation`. Prevents orphaning workflow progress. **No bypass** — must close/cancel the current case first. |
|
||
| `_check_claim_number` | `x_fc_claim_number` | Exactly **10 digits**, numbers only. Regex `^\d{10}$`. Whitespace stripped. |
|
||
| `_check_client_ref_1` | `x_fc_client_ref_1` | Up to **4 letters** total (comma allowed and excluded from count). Regex `^[A-Za-z,]+$`. Convention: first 2 letters of first name + last 2 letters of last name (e.g. "John Doe" → "JODO"). |
|
||
| `_check_client_ref_2` | `x_fc_client_ref_2` | Exactly **4 digits**. Regex `^\d{4}$`. Convention: last 4 of health card number. |
|
||
| `_check_original_application_pdf` | `x_fc_original_application_filename` | Must end in `.pdf` (case-insensitive). |
|
||
| `_check_signed_pages_pdf` | `x_fc_signed_pages_filename` | Must end in `.pdf`. |
|
||
| `_check_final_application_pdf` | `x_fc_final_application_filename` | Must end in `.pdf`. |
|
||
| `_check_xml_file` | `x_fc_xml_filename` | Must end in `.xml`. |
|
||
| `_check_proof_of_delivery_pdf` | `x_fc_proof_of_delivery_filename` | Must end in `.pdf`. |
|
||
| `_check_delivery_date_after_approval` | `x_fc_adp_delivery_date` + `x_fc_claim_approval_date` | `delivery_date >= approval_date`. **For early delivery cases**, the POD doc should show the approval date, NOT the actual delivery date — the error message tells the user this directly. |
|
||
|
||
**Gotchas for these constraints:**
|
||
|
||
- They fire on **every write** to the listed field — not just user-driven writes. Cron/sync code that touches these fields with invalid data will raise.
|
||
- The file-extension constraints check `*_filename`, NOT the binary content. Combined with the application_received_wizard's PDF magic-byte check (§4.4), this gives two layers of defense.
|
||
- `_check_client_ref_1` allows commas — useful for clients with two-part last names ("De,Mo" for "De Souza Morales" → `DEMO`).
|
||
- `_check_delivery_date_after_approval` is the reason for the **"early delivery" pattern** — the POD doc must use the approval_date as its delivery_date when the client takes physical possession before ADP approves. See §4.x and the early-delivery flag on the SO.
|
||
- These constraints fire AFTER the `write` override's status-transition gates have run — so if both fail, the user sees the constraint error first (because it's raised inside the same write transaction).
|
||
|
||
---
|
||
|
||
## 37. PDF report templates — business logic
|
||
|
||
12 QWeb templates. All follow the same skeleton: `<t t-call="web.html_container">` → `web.external_layout` → set `is_*` flags + `primary`/`secondary` colors → render page contents. The `report_templates.xml` shared snippets (§19.1) are `t-call`-able from any of them.
|
||
|
||
### 37.1 `sale_report_landscape.xml` — Quotation/Order (ADP)
|
||
|
||
Bound to `sale.order`. Distinguishes draft/sent (`Quotation`) vs confirmed (`Sales Order`) via `doc.state in ['draft','sent']`. Conditional sections:
|
||
|
||
| Block | Condition | Notes |
|
||
|---|---|---|
|
||
| **ADP Info Table** | `is_adp` (`doc.x_fc_is_adp_sale`) | Shows Claim # / Application Type (Reason for Application — labelled via `dict(...selection)`) / Client Ref 2 / Delivery / Authorization / Approval dates. Blue `#e3f2fd` background. |
|
||
| **PLCMT column** | `is_adp` | Adds the Device Placement (L/R/N/A) column to the order-lines table — only when ADP |
|
||
| **ADP/Client portion columns** | always | Two color-coded columns: ADP `#1976d2` header / `#e3f2fd` row bg; Client `#e65100` header / `#fff3e0` row bg |
|
||
| **Section/Note rows** | `display_type in ('line_section','line_note')` | Section rows get grey `#f0f0f0` bg with bold; note rows get italic |
|
||
| **Line description cleanup** | always | Strips `[internal_ref]` prefix — same logic as §19.2: `if '] ' in line.name: clean_name = line.name.split('] ', 1)[1]` |
|
||
| **Unit price** | `if x_fc_adp_price` | Shows ADP price if available, otherwise `price_unit` — i.e. the **list price shown to the user is the ADP maximum**, not the retail price |
|
||
| **Totals table** | always | Subtotal + Total ADP Portion + Total Client Portion + Taxes + Grand Total. ADP/Client rows use the matching pastel backgrounds |
|
||
| **Payment Terms** | `doc.payment_term_id.note` | Left column |
|
||
| **Signature** | `doc.signature` | Rendered as base64 image, max 4cm × 8cm, with `doc.signed_by` underneath |
|
||
| **Terms and Conditions** | `doc.note` | Below totals if present |
|
||
|
||
Colspan adjustments: `'10' if is_adp else '9'` for section/note rows (the PLCMT column adds one).
|
||
|
||
### 37.2 `invoice_report_landscape.xml` — Invoice (ADP)
|
||
|
||
Bound to `account.move`. Same structure as sale_report_landscape with these additions:
|
||
|
||
| Block | Condition | Notes |
|
||
|---|---|---|
|
||
| **Title** | `move_type == 'out_invoice' and state == 'posted'` | "Invoice" prefix; else just the name |
|
||
| **Payment Reference** | `doc.payment_reference` | Displayed near payment terms |
|
||
| **ADP Portion totals** | `x_fc_adp_invoice_portion == 'adp'` | Special ADP totals block — only for ADP-portion invoices |
|
||
| **Amount Residual** | `amount_residual and != amount_total` | "Amount Due" row when partially paid |
|
||
| **Payment Details** | `payment_state != 'invoicing_legacy'` | Bottom-of-report payment history. If `payment_state == 'paid'`: green "✓ PAYMENT DETAILS - PAID IN FULL" banner. Lists all account.payment records linked to the invoice |
|
||
| **Outstanding Balance** | `amount_residual > 0 and payment_state != 'paid'` | Highlight box with remaining due |
|
||
| **Narration** | `doc.narration` | Notes section at the bottom |
|
||
|
||
### 37.3 `invoice_report_portrait.xml`
|
||
|
||
Portrait variant — same conditional logic, narrower column layout. ADP fields collapsed into a 2-column info table instead of 6.
|
||
|
||
### 37.4 `sale_report_portrait.xml`
|
||
|
||
Portrait variant of the quotation report. Same color conventions.
|
||
|
||
### 37.5 `report_proof_of_delivery.xml` — ADP POD
|
||
|
||
Bound to `sale.order`. **No prices** anywhere — POD is a delivery acknowledgment, not a price doc. Key blocks:
|
||
|
||
| Block | Notes |
|
||
|---|---|
|
||
| Title | "ADP PROOF OF DELIVERY" — centered |
|
||
| Reference line | `doc.name` + Claim # if set |
|
||
| Customer + Delivery address tables | Side-by-side bordered boxes |
|
||
| Order Info | Order Date / Delivery Date / Client Type / Sales Rep / Authorizer |
|
||
| Products Delivered | ADP Code / Description (cleaned) / Serial # / Placement (if ADP) / Qty / **Device Type** (no prices) |
|
||
| Acknowledgment block | Yellow `#fff8e1` bg with left border. Full refund policy link, legal acknowledgment text, uppercase "I HAVE RECEIVED ALL OF THE PRODUCTS AND SERVICES PROMISED TO ME..." |
|
||
| Signature section | Captured digital signature OR empty signature lines. If captured: shows `x_fc_pod_client_name`, `x_fc_pod_signature_date`, base64-decoded `x_fc_pod_signature` image (max 80×300px), and "Collected by: {x_fc_pod_signed_by_user_id.name}" |
|
||
| Page 2 | **Forced page break** (`page-break-before: always`). Full RETURN AND REFUND POLICY — Change Order/Cancellation, Restocking Fees (30% for Patient Lifts/Hospital Beds/Transport Wheelchairs/Standard Rollators&Walkers), Non-Returnable Items list (~13 categories) |
|
||
|
||
### 37.6 `report_proof_of_delivery_standard.xml`
|
||
|
||
Non-ADP POD variant. Same structure but no ADP-specific columns. Uses `doc.commitment_date` instead of `x_fc_adp_delivery_date`.
|
||
|
||
### 37.7 `report_proof_of_pickup.xml`
|
||
|
||
For rental pickups. Same skeleton, "Proof of Pickup" title, references `doc.commitment_date`. Used at end of rental term when collecting equipment back.
|
||
|
||
### 37.8 `report_approved_items.xml`
|
||
|
||
The PDF version of the `_build_approved_items_html` table (§16.1). Generated standalone via `_generate_approved_items_pdf` and attached to the `_send_approval_email`. Conditional fields: `x_fc_assessment_end_date`, `x_fc_claim_approval_date`.
|
||
|
||
### 37.9 `report_grab_bar_waiver.xml`
|
||
|
||
Liability waiver for grab-bar installations. Standalone form clients sign acknowledging installation responsibility.
|
||
|
||
### 37.10 `report_accessibility_contract.xml`
|
||
|
||
Accessibility modification contract. For MOD/accessibility projects (stairlifts, ramps, etc.). Fields: validity date, payment terms, project notes.
|
||
|
||
### 37.11 `report_mod_quotation.xml`
|
||
|
||
MOD-specific quotation. Distinctive fields shown:
|
||
- `x_fc_mod_estimated_weeks` — project duration estimate
|
||
- `x_fc_estimated_completion_date` — target completion
|
||
- `x_fc_authorizer_id` — assigned OT/specialist
|
||
|
||
### 37.12 `report_mod_invoice.xml`
|
||
|
||
MOD-specific invoice. Distinctive layout because MOD invoices go to the MOD partner (`March of Dimes Canada (HVMP)`) not the client — see §7.5. Includes:
|
||
- Company VAT in header
|
||
- Partner shipping = original client (delivery address)
|
||
- HVMP reference number prominent
|
||
|
||
## 38. `views/sale_order_views.xml` — form structure
|
||
|
||
The 2,768-line form view inheritance file. Key patterns:
|
||
|
||
### 38.1 Inheritance priorities
|
||
|
||
Multiple views inherit `sale.view_order_form` at different priorities to layer in fusion_claims fields:
|
||
|
||
- **Header fields**: sale_type, client_type, authorizer (visibility-gated by `_compute_show_*` flags)
|
||
- **Status statusbar**: clickable, `filtered_status_selection` widget hides controlled statuses
|
||
- **Tabs**: ADP Case Details / ADP Documents / MOD / ODSP / WSIB / Insurance / MDC / Hardship — each `invisible=` driven by `x_fc_is_*_sale` or `x_fc_show_*_fields`
|
||
- **Smart buttons**: Invoice counts, Vendor Bills, Submission History, Technician Tasks, MOD Invoices, Submission/Page11 counts
|
||
|
||
### 38.2 Conditional field visibility rules
|
||
|
||
The pattern throughout: `invisible="not x_fc_show_<funder>_fields"` on the per-funder tabs. The compute methods (`_compute_show_*_fields`) all check `x_fc_sale_type` and only return True for the matching funder.
|
||
|
||
This means an ADP order shows ONLY the ADP tab; a MOD order shows ONLY the MOD tab — preventing field clutter.
|
||
|
||
### 38.3 Kanban view data attributes
|
||
|
||
The kanban card has `<main>` with `data-stage="info/warning/success/danger/secondary"` / `data-priority="1"` / `data-emergency="1"` attributes that the SCSS `:has()` selectors (§20.4) style. Status → data-stage mapping is in the kanban template itself.
|
||
|
||
### 38.4 Search view
|
||
|
||
- Filters per status (Quotation / In Progress / Approved / Billed / Closed / On Hold / ...)
|
||
- Filters per funder (ADP / MOD / ODSP / WSIB / etc.)
|
||
- Group-by: Status, Sale Type, Sales Rep, Client Type, Authorizer
|
||
- Default search: `my_quotation` (user_id = current user) on Quotation Stage list
|
||
|
||
## 39. `views/adp_claims_views.xml` — per-stage action inventory
|
||
|
||
~80 action records. Pattern: every stage has its own `ir.actions.act_window` with:
|
||
|
||
- `res_model='sale.order'`
|
||
- `view_mode='list,kanban,form'`
|
||
- Custom `view_ids` referencing the ADP-specific list and kanban views (`view_sale_order_list_adp`, `view_sale_order_kanban_adp`)
|
||
- Custom `search_view_id` (`view_sale_order_search_adp`)
|
||
- **Domain filter** that pins the action to a specific status (and sale type)
|
||
|
||
### 39.1 ADP action domain matrix (sample)
|
||
|
||
```python
|
||
'All ADP Orders' [('x_fc_is_adp_sale', '=', True)]
|
||
'Quotation Stage' [..., ('x_fc_adp_application_status', '=', 'quotation')]
|
||
'Assessment Scheduled' [..., ('x_fc_adp_application_status', '=', 'assessment_scheduled')]
|
||
'Waiting for Application' [..., ('x_fc_adp_application_status', 'in', ('assessment_completed', 'waiting_for_application'))]
|
||
'Application Received' [..., ('x_fc_adp_application_status', '=', 'application_received')]
|
||
'Ready for Submission' [..., ('x_fc_adp_application_status', '=', 'ready_submission')]
|
||
'Application Submitted' [..., ('x_fc_adp_application_status', 'in', ['submitted', 'resubmitted'])]
|
||
'Accepted by ADP' [..., ('x_fc_adp_application_status', '=', 'accepted')]
|
||
'Rejected by ADP' [..., ('x_fc_adp_application_status', '=', 'rejected')]
|
||
'Needs Correction' [..., ('x_fc_adp_application_status', '=', 'needs_correction')]
|
||
'Application Approved' [..., ('x_fc_adp_application_status', 'in', ['approved', 'approved_deduction'])]
|
||
'Ready for Delivery' [..., ('x_fc_adp_application_status', '=', 'ready_delivery')]
|
||
'Ready to Bill' [..., ('x_fc_adp_application_status', '=', 'ready_bill')]
|
||
'Billed to ADP' [..., ('x_fc_adp_application_status', '=', 'billed')]
|
||
'Case Closed' [..., ('x_fc_adp_application_status', '=', 'case_closed')]
|
||
'On Hold' [..., ('x_fc_adp_application_status', '=', 'on_hold')]
|
||
'Withdrawn' [..., ('x_fc_adp_application_status', '=', 'withdrawn')]
|
||
'Denied' [..., ('x_fc_adp_application_status', '=', 'denied')]
|
||
'Cancelled' [..., ('x_fc_adp_application_status', '=', 'cancelled')]
|
||
'Expired' [..., ('x_fc_adp_application_status', '=', 'expired')]
|
||
```
|
||
|
||
### 39.2 ODSP action domain matrix
|
||
|
||
The ODSP menu has THREE division-specific tracks. Each track has per-stage actions filtered by BOTH `x_fc_odsp_division` AND the division-specific status field:
|
||
|
||
```python
|
||
'All ODSP Cases' [('x_fc_sale_type', 'in', ['odsp', 'adp_odsp'])]
|
||
'ODSP Standard' [..., ('x_fc_odsp_division', '=', 'standard')]
|
||
'SA Mobility' [..., ('x_fc_odsp_division', '=', 'sa_mobility')]
|
||
'Ontario Works' [..., ('x_fc_odsp_division', '=', 'ontario_works')]
|
||
|
||
# Per status under Standard:
|
||
'ODSP Standard - Quotation' [..., ('x_fc_odsp_division', '=', 'standard'), ('x_fc_odsp_std_status', '=', 'quotation')]
|
||
'ODSP Standard - Submitted' [..., ('x_fc_odsp_division', '=', 'standard'), ('x_fc_odsp_std_status', '=', 'submitted_to_odsp')]
|
||
# ... etc for all 11 SA states, 11 Standard states, 10 OW states
|
||
|
||
# OW notably:
|
||
'OW - Payment Received' [..., ('x_fc_ow_status', '=', 'payment_received')] # BEFORE delivery
|
||
'OW - Ready for Delivery' [..., ('x_fc_ow_status', '=', 'ready_delivery')]
|
||
```
|
||
|
||
### 39.3 MOD action domain matrix
|
||
|
||
```python
|
||
'All MOD Cases' [('x_fc_sale_type', '=', 'march_of_dimes')]
|
||
'Need to Schedule' [..., ('x_fc_mod_status', '=', 'need_to_schedule')]
|
||
'Assessment Scheduled' [..., ('x_fc_mod_status', '=', 'assessment_scheduled')]
|
||
'Assessment Done' [..., ('x_fc_mod_status', '=', 'assessment_completed')]
|
||
'Processing Drawing' [..., ('x_fc_mod_status', '=', 'processing_drawings')]
|
||
'Quote Submitted' [..., ('x_fc_mod_status', '=', 'quote_submitted')]
|
||
'Awaiting Funding' [..., ('x_fc_mod_status', '=', 'awaiting_funding')]
|
||
'Funding Approved' [..., ('x_fc_mod_status', '=', 'funding_approved')]
|
||
'PCA Received' [..., ('x_fc_mod_status', '=', 'contract_received')]
|
||
'In Production' [..., ('x_fc_mod_status', '=', 'in_production')]
|
||
'Project Complete' [..., ('x_fc_mod_status', '=', 'project_complete')]
|
||
'POD Sent' [..., ('x_fc_mod_status', '=', 'pod_submitted')]
|
||
'Case Closed' [..., ('x_fc_mod_status', '=', 'case_closed')]
|
||
'On Hold' [..., ('x_fc_mod_status', '=', 'on_hold')]
|
||
'Funding Denied' [..., ('x_fc_mod_status', '=', 'funding_denied')]
|
||
'Cancelled' [..., ('x_fc_mod_status', '=', 'cancelled')]
|
||
```
|
||
|
||
### 39.4 Other funder actions (no per-stage drill-down)
|
||
|
||
WSIB / Insurance / MDC / Hardship / Rental / Direct-Private / Other each get ONE list action with `[('x_fc_sale_type', '=', '<funder>')]`. No per-stage actions — the user filters by status in-list.
|
||
|
||
### 39.5 Invoice action domain matrix
|
||
|
||
```python
|
||
'All Funder Invoices' [('x_fc_invoice_type', '!=', False), ('x_fc_invoice_type', '!=', 'regular'), ...]
|
||
'ADP Client Invoices' [('x_fc_invoice_type', '=', 'adp_client'), ...]
|
||
'ODSP Invoices' [('x_fc_invoice_type', 'in', ['odsp', 'adp_odsp']), ...]
|
||
'MOD Invoices' [('x_fc_invoice_type', '=', 'march_of_dimes'), ...]
|
||
'WSIB Invoices' [('x_fc_invoice_type', '=', 'wsib'), ...]
|
||
'Insurance Invoices' [('x_fc_invoice_type', '=', 'insurance'), ...]
|
||
'Direct/Private Invoices' [('x_fc_invoice_type', '=', 'direct_private'), ...]
|
||
'Hardship Invoices' [('x_fc_invoice_type', '=', 'hardship'), ...]
|
||
'Rental Invoices' [('x_fc_invoice_type', '=', 'rental'), ...]
|
||
'Muscular Dystrophy Invoices' [('x_fc_invoice_type', '=', 'muscular_dystrophy'), ...]
|
||
'Other Invoices' [('x_fc_invoice_type', '=', 'other'), ...]
|
||
```
|
||
|
||
All filtered to `move_type in ['out_invoice', 'out_refund']` (customer invoices + refunds only — not vendor bills).
|
||
|
||
### 39.6 Special non-funder action: ACSD
|
||
|
||
```python
|
||
'ACSD Cases' [('x_fc_client_type', '=', 'ACS')]
|
||
```
|
||
|
||
ACSD (Assistance to Children with Severe Disabilities) is a CLIENT TYPE, not a sale type. The menu has a dedicated ACSD entry that catches any sale type but with `client_type='ACS'`.
|
||
|
||
## 40. `fusion_authorizer_portal.sale_order` extensions (266 lines)
|
||
|
||
Adds 6 fields to `sale.order` + 5 methods:
|
||
|
||
| Field | Purpose |
|
||
|---|---|
|
||
| `portal_comment_ids` (O2m → `fusion.authorizer.comment`) | Comments left by OT/sales rep on the portal |
|
||
| `portal_comment_count` (computed) | Smart-button count |
|
||
| `portal_document_ids` (O2m → `fusion.adp.document`) | Documents uploaded via the portal |
|
||
| `portal_document_count` (computed) | Smart-button count |
|
||
| `assessment_id` (M2o → `fusion.assessment`) | Back-link to the ADP equipment assessment that created this SO |
|
||
| `accessibility_assessment_id` (M2o → `fusion.accessibility.assessment`) | 2026-04 fix — back-link to the accessibility assessment (stair lift / VPL / etc.) that created this SO |
|
||
| `portal_authorizer_id` (computed) | Consolidated authorizer reference — mirrors `x_fc_authorizer_id` for portal access checks |
|
||
|
||
`write` override fires `_send_authorizer_assignment_notification` whenever `x_fc_authorizer_id` changes to a new value.
|
||
|
||
`action_message_authorizer` opens a mail composer pre-filled with the authorizer as recipient.
|
||
|
||
JSON-RPC methods (called from portal JS):
|
||
- `get_authorizer_portal_cases(partner_id, search_query, limit, offset)` — paginated list of cases the OT is authorizer for
|
||
- `get_sales_rep_portal_cases(user_id, search_query, limit, offset)` — same for sales reps
|
||
- `_build_search_domain(query)` — fuzzy search across name, partner name, claim number, client refs
|
||
|
||
`get_portal_display_data()` — returns a dict with all display data for one SO (called from portal page render).
|
||
`_get_partner_address_display()` — formatted address string.
|
||
`_get_product_lines_for_portal()` — product lines minus internal-only data.
|
||
|
||
## 41. `fusion_authorizer_portal.res_partner` extensions (767 lines)
|
||
|
||
Adds geolocation + portal access management:
|
||
|
||
| Field | Purpose |
|
||
|---|---|
|
||
| `x_fc_latitude`, `x_fc_longitude` | Geocoded coords for the partner's address (set by `_geocode_address` on write) |
|
||
| (computed) `assigned_case_count` | # of SOs where this partner is authorizer |
|
||
| (computed) `assessment_count` | # of `fusion.assessment` records for this partner |
|
||
| (computed) `assigned_delivery_count` | # of delivery tasks |
|
||
| (computed) `portal_access_status` | "Active" / "Pending" / "None" — depends on whether a portal user exists |
|
||
|
||
Key actions:
|
||
- `action_grant_portal_access` — provisions a `res.users` portal account, creates a welcome knowledge article (`_create_welcome_article`), sends invitation email (`_send_portal_invitation_email`)
|
||
- `action_resend_portal_invitation` — re-fires the invitation
|
||
- `action_view_assigned_cases` / `action_view_assessments` / `action_view_assigned_deliveries` — smart-button targets
|
||
- `action_mark_as_authorizer` / `action_mark_as_technician` — sets `x_fc_contact_type` and assigns the matching role group
|
||
- `action_batch_send_portal_invitation` / `action_mark_and_send_invitation` — bulk operations
|
||
- `_assign_portal_role_groups(portal_user)` / `_assign_internal_role_groups(internal_user)` — auto-assigns groups based on `x_fc_contact_type` (occupational_therapist → authorizer group, etc.)
|
||
|
||
`write` override calls `_geocode_address` whenever address fields change — keeps `x_fc_latitude`/`x_fc_longitude` in sync.
|
||
|
||
## 42. `google_address_autocomplete.js` (1,506 lines) — what it actually does
|
||
|
||
OWL patch on `FormController` that adds Google Places autocomplete to address fields on `res.partner` and other models. Key behaviors:
|
||
|
||
| Behavior | Details |
|
||
|---|---|
|
||
| **Lazy API load** | Loads Google Maps JS API dynamically only when first needed. Singleton — `googleMapsLoaded` flag prevents duplicate loads. API key read from `ir.config_parameter['fusion_claims.google_maps_api_key']`. |
|
||
| **Conflict avoidance** | Checks `registry.category("fields").contains("google_address_autocomplete")` — if Odoo Enterprise's own google_address_autocomplete module is installed, **skips** our patch (theirs handles it). |
|
||
| **Canada-only** | `componentRestrictions: { country: 'ca' }` — autocomplete suggestions limited to Canadian addresses. |
|
||
| **Field parsing** | Maps Google place components → Odoo fields: `street_number + route → street`; `subpremise → street2`; `floor → 'Floor N' → street2`; `locality / sublocality_level_1 → city`; `administrative_area_level_1 → state code`; `postal_code → zip`; `country.short_name → country code` |
|
||
| **Saved record path** | For records with `resId`, uses `globalOrm.write('res.partner', [resId], updatePayload)`. Writes country first, then state in a separate write 100ms later (Odoo's state field depends on country onchange to populate domain). |
|
||
| **New record path** | For unsaved records, uses `record.update()` for text fields + `simulateMany2OneSelection` DOM hack for country/state (since Many2one updates on unsaved records don't propagate cleanly via `update()`). Waits 300ms between country and state for the onchange to settle. |
|
||
| **Reload after write** | Calls `record.load()` to refresh the form display after the ORM writes. |
|
||
|
||
The 1,506 lines are mostly DOM-manipulation fallback logic for the new-record case + debug logging. The core integration is the ~200-line `initAutocompleteOnField` function.
|
||
|
||
## 43. `static/src/xml/document_preview.xml` (204 lines) — OWL templates
|
||
|
||
Three OWL component templates:
|
||
|
||
- **`fusion_claims.DocumentPreviewDialog`** — full-screen PDF/XML preview dialog. Uses `<iframe>` pointing at the attachment URL with PDF.js viewer parameters. Maximize toggle button (xl ↔ fullscreen). Loading spinner.
|
||
- **`fusion_claims.PreviewButtonWidget`** — button widget rendered inside list view cells. Click opens DocumentPreviewDialog with the attachment_id from `record.data.attachment_id`.
|
||
- **Inline error states** — "No document to preview" warning notification template.
|
||
|
||
Used by:
|
||
- `views/page11_sign_request_views.xml` for preview button on the signed PDF
|
||
- Any form/list view that uses `widget="preview_button"` on an attachment-id field
|
||
- The custom `fusion_claims.preview_document` client action (referenced by `odsp_ready_delivery_wizard.action_preview_full`)
|
||
|
||
## 44. fusion_tasks smaller models
|
||
|
||
### 44.1 `fusion.email.builder.mixin` (241 lines) — full anatomy
|
||
|
||
The shared email builder. Two main public methods:
|
||
|
||
**`_email_build(title, summary, sections, note, note_color, email_type, attachments_note, button_url, button_text, sender_name, extra_html)`** — assembles the full email HTML. Builds in this order:
|
||
1. Outer wrapper div (font + max-width 600px + centered)
|
||
2. Accent bar (4px high, color from `_EMAIL_COLORS[email_type]`)
|
||
3. Company name (uppercase, accent color)
|
||
4. Title (h2)
|
||
5. Summary (muted opacity)
|
||
6. Sections (per `_email_section(heading, rows)` — bordered table with label/value rows)
|
||
7. Note (per `_email_note(text, color)` — left-border accent block)
|
||
8. Extra HTML (raw insert)
|
||
9. Attachment note (per `_email_attachment_note(description)` — dashed-border callout)
|
||
10. CTA button (per `_email_button(url, text, color)` — centered, rounded, accent bg)
|
||
11. Sign-off (`Best regards, <strong>{signer}</strong><br/>{company}`)
|
||
12. Footer (`{company} · {phone} · {email}` + "This is an automated notification from the ADP Claims Management System.")
|
||
|
||
The footer line is what `_mod_email_build` and `_odsp_email_build` overwrite to change the voice (§16.5.1).
|
||
|
||
**Building-block helpers** (callable independently):
|
||
- `_email_section(heading, rows)` — labelled details table
|
||
- `_email_note(text, color)` — left-border accent note
|
||
- `_email_button(url, text, color)` — centered CTA button
|
||
- `_email_attachment_note(description)` — dashed-border attachment callout
|
||
- `_email_status_badge(label, color)` — inline pill badge (4 background tints mapped from foreground colors)
|
||
|
||
**`_get_company_info()`** — pulls `name`, `phone`, `email` from `self.company_id` or `self.env.company`.
|
||
|
||
**`_email_is_enabled()`** — reads `fusion_claims.enable_email_notifications` (default True).
|
||
|
||
`_EMAIL_COLORS` palette:
|
||
```python
|
||
'info': '#2B6CB0' # blue
|
||
'success': '#38a169' # green
|
||
'attention': '#d69e2e' # amber
|
||
'urgent': '#c53030' # red
|
||
```
|
||
|
||
### 44.2 `fusion.push.subscription` (73 lines) — Web Push subscriptions
|
||
|
||
| Field | Purpose |
|
||
|---|---|
|
||
| `user_id` | Owner (technician) |
|
||
| `endpoint` | Web Push endpoint URL (browser-supplied) |
|
||
| `p256dh_key`, `auth_key` | Browser encryption keys for VAPID push |
|
||
| `browser_info` | User agent string |
|
||
| `active` | Soft-delete flag |
|
||
|
||
Unique constraint on `endpoint` so a user re-subscribing from the same browser updates the existing record instead of creating duplicates.
|
||
|
||
`register_subscription(user_id, endpoint, p256dh_key, auth_key, browser_info)` — idempotent upsert called from portal JS (`/my/technician/push/subscribe` route).
|
||
|
||
### 44.3 `fusion.technician.location` (131 lines) — GPS history
|
||
|
||
| Field | Purpose |
|
||
|---|---|
|
||
| `user_id` | Technician |
|
||
| `latitude`, `longitude` | 10-digit precision |
|
||
| `accuracy` | GPS accuracy in meters |
|
||
| `logged_at` | Timestamp (indexed) |
|
||
| `source` | `portal` / `app` / `sync` |
|
||
| `sync_instance` | Origin instance ID if synced (e.g. `westin`, `mobility`) |
|
||
|
||
**`log_location(latitude, longitude, accuracy)`** — JSON-RPC entrypoint from portal JS heartbeat.
|
||
|
||
**`get_latest_locations()`** — admin map data. Uses Postgres `DISTINCT ON (user_id)` to fetch the most-recent point per technician within the last 24 hours. **Includes both local + synced locations** — the admin map shows all shared techs regardless of which Odoo instance they're clocked into. Falls back to local instance ID if `sync_instance` is blank.
|
||
|
||
**`_cron_cleanup_old_locations()`** — configurable retention via `fusion_claims.location_retention_days`:
|
||
- Empty / unset → 30 days (default)
|
||
- `"0"` → delete at end of day (keep today only)
|
||
- `"N"` → keep N days
|
||
|
||
### 44.4 `fusion.task.sync.config` extras not covered earlier
|
||
|
||
- **`_cron_cleanup_old_shadows`** — removes shadow tasks whose remote UUID has disappeared. Runs after each pull cycle.
|
||
- **Action buttons**: `action_test_connection` (logs in + counts matched techs), `action_sync_now` (manual force-sync trigger).
|
||
- **Settings UI**: `fusion.task.sync.config` records visible in Settings → Technical → Task Sync. New configs need URL + database + username + api_key + instance_id.
|
||
|
||
## 46. Production deployment — Westin + Mobility
|
||
|
||
When the user says **"deploy to westin"** or **"deploy to mobility"**, the targets are:
|
||
|
||
| Target | VM | LAN IP | Proxmox node | Service URL | DB name | Containers |
|
||
|---|---|---|---|---|---|---|
|
||
| **Westin** | VM **101** `odoo-westin` | `192.168.1.40` | `pve-worker1` (`192.168.1.6`) | `erp.westinhealthcare.ca` | **`westin-v19`** | `odoo-dev-app` + `odoo-dev-db` |
|
||
| **Mobility** | VM **115** `odoo-mobility` | `192.168.1.102` | `pve-worker3` (`192.168.1.8`) | `erp.mobilityspecialties.ca` | **`mobility`** | `odoo-mobility-app` + `odoo-mobility-db` |
|
||
|
||
Compose path on both VMs: `/opt/odoo/docker-compose.yml` (config at `/opt/odoo/odoo.conf`).
|
||
|
||
### 46.1 SSH access (via Tailscale jump host)
|
||
|
||
Jump host: `supabase-prod` at Tailscale IP `100.74.28.73` (root).
|
||
|
||
```bash
|
||
# Windows (use the SSH config aliases — ProxyCommand via supabase-prod)
|
||
ssh odoo-westin
|
||
ssh odoo-mobility
|
||
|
||
# Mac (explicit jump)
|
||
ssh -J root@100.74.28.73 root@192.168.1.40 # westin
|
||
ssh -J root@100.74.28.73 root@192.168.1.102 # mobility
|
||
|
||
# Run a one-off command
|
||
ssh odoo-westin "<command>"
|
||
ssh odoo-mobility "<command>"
|
||
```
|
||
|
||
Both VMs accept the ed25519 key at `~/.ssh/id_ed25519`. Both have the `windows-home` and `gurpreet@mac-studio` keys authorized.
|
||
|
||
### 46.2 Deploy workflow
|
||
|
||
```bash
|
||
# 1. SSH in and pull the latest module code (modules live in /opt/odoo/addons/ — confirm path per VM)
|
||
# 2. Restart the Odoo container with -u to upgrade the module:
|
||
|
||
# Westin
|
||
ssh odoo-westin "docker exec odoo-dev-app odoo -d westin-v19 -u fusion_claims --stop-after-init && docker restart odoo-dev-app"
|
||
|
||
# Mobility
|
||
ssh odoo-mobility "docker exec odoo-mobility-app odoo -d mobility -u fusion_claims --stop-after-init && docker restart odoo-mobility-app"
|
||
```
|
||
|
||
For multiple modules: `-u fusion_claims,fusion_tasks,fusion_authorizer_portal`.
|
||
|
||
### 46.3 Database probes
|
||
|
||
```bash
|
||
# List all databases on a VM
|
||
ssh odoo-westin "docker exec odoo-dev-db psql -U odoo -d postgres -lqt | cut -d'|' -f1 | grep -v -e '^$' -e template -e postgres"
|
||
ssh odoo-mobility "docker exec odoo-mobility-db psql -U odoo -d postgres -lqt | cut -d'|' -f1 | grep -v -e '^$' -e template -e postgres"
|
||
|
||
# Run SQL against the production DB
|
||
ssh odoo-westin "docker exec odoo-dev-db psql -U odoo -d westin-v19 -c 'SELECT count(*) FROM sale_order;'"
|
||
ssh odoo-mobility "docker exec odoo-mobility-db psql -U odoo -d mobility -c 'SELECT count(*) FROM sale_order;'"
|
||
```
|
||
|
||
**Westin has a fleet of databases** (`westin-v19`, `westin-v19-ref`, `westin-v19-cra-aligned`, `westin-v19-reconciled`, `westin-v19-assets-done`, etc.) — these are test/reference copies for accounting work. Production is always `westin-v19`. The `db_name = westin-v19` line in `/opt/odoo/odoo.conf` confirms it's the active DB.
|
||
|
||
### 46.4 Logs + restart
|
||
|
||
```bash
|
||
# Tail Odoo logs
|
||
ssh odoo-westin "docker logs -f --tail 100 odoo-dev-app"
|
||
ssh odoo-mobility "docker logs -f --tail 100 odoo-mobility-app"
|
||
|
||
# Hard restart (compose-aware)
|
||
ssh odoo-westin "cd /opt/odoo && docker compose restart odoo"
|
||
ssh odoo-mobility "cd /opt/odoo && docker compose restart odoo"
|
||
```
|
||
|
||
### 46.5 DNS / Cloudflare
|
||
|
||
Both production domains (`erp.westinhealthcare.ca` and `erp.mobilityspecialties.ca`) route through **Cloudflare** with proxied DNS pointing at the reverse-proxy IP `142.112.5.37` (production-caddy VM 104 on pve-worker1), which reverse-proxies to the VM's internal IP and Odoo port 8069.
|
||
|
||
For Cloudflare API operations (cache purge, WAF rules, DNS record changes), the **Supabase MCP holds Cloudflare API access** — query `public.credentials_vault` or use the MCP's Cloudflare tooling rather than embedding keys in this file or the repo. Don't commit any Cloudflare API tokens.
|
||
|
||
### 46.6 Other Nexa Odoo VMs (for reference — don't deploy here unless told)
|
||
|
||
| VM | Name | IP | Purpose |
|
||
|---|---|---|---|
|
||
| 311 | `odoo-apex` | (worker1) | Apex Mobility Solutions production |
|
||
| 315 | `odoo-nexa` | (worker1) | NEXA Systems internal Odoo |
|
||
| 316 | `odoo-trial` | `192.168.1.112` | `tryfusion.nexasystems.ca` trial instance |
|
||
|
||
"Deploy to westin" or "deploy to mobility" means **only** VM 101 / VM 115 — never these.
|
||
|
||
---
|
||
|
||
## 45. Final inventory — what's definitely captured
|
||
|
||
After 9 rounds of deep diving, here's what CLAUDE.md covers vs the codebase:
|
||
|
||
**Captured (everything that affects runtime behaviour):**
|
||
- Every model field, computed and stored
|
||
- Every workflow state machine across 9 funders
|
||
- Every wizard with allowed-from + purpose + fields-set
|
||
- Every email sender with recipients + triggers
|
||
- Every cron job with cadence + logic
|
||
- Every constraint method with regex + rule
|
||
- Every special-character/edge-case behaviour I encountered
|
||
- Every cross-module integration point with both sibling modules (fusion_tasks, fusion_authorizer_portal)
|
||
- Every PDF report's conditional sections + business logic
|
||
- Every ICP setting (~60+)
|
||
- Every gotcha (~83)
|
||
- Every portal route (~40+)
|
||
- Every PDF template field-name quirk (SA Mobility 13007E + OW Discretionary Benefits)
|
||
|
||
**Not captured (because it doesn't affect behaviour):**
|
||
- Pure form-layout XPaths (which tab a field appears under)
|
||
- QWeb rendering styling (font sizes, table padding)
|
||
- Per-page CSS pixel positioning
|
||
- DOM-manipulation fallback paths in google_address_autocomplete.js
|
||
- Repeated XML parser field declarations (same pattern as 2a)
|
||
- Demo data scripts (`import_demo_pool.py`, `cleanup_demo_pool.py`)
|
||
- The orphan fusion_task_map_view.* files (loaded by fusion_tasks, not us)
|
||
|
||
**Future Claude session reading this file should be able to:**
|
||
- Predict the outcome of any status transition
|
||
- Understand which fields are required at which stage
|
||
- Know which constraint will fire on which field value
|
||
- Know which email goes to whom for any workflow event
|
||
- Trace the path from a sale_order field rename → portal templates that break
|
||
- Debug cross-instance sync issues with the Westin↔Mobility setup
|
||
- Build new reports following the established color/header/footer conventions
|
||
- Add new gotchas in the right format
|
||
- Understand the soft-dep on `fusion_faxes` + `fusion_pdf_preview`
|
||
- Know the deployment fact that fusion_authorizer_portal is always co-installed
|