diff --git a/CLAUDE.md b/CLAUDE.md index bee454de..c5033b04 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -83,6 +83,24 @@ Odoo content-hashes the compiled bundle URL (`/web/assets//...`). When CSS - Local URL: http://localhost:8069 - Test before deploying. Edit existing files — don't create unnecessary new ones. +## PDF Preview — Prefer fusion_pdf_preview Over Downloads/New-Tab +When a Python action opens an attachment, route it through `fusion_pdf_preview` instead of returning `ir.actions.act_url` with `download=true` or `target=new`. The preview dialog gives operators preview + print + download in one place and writes an audit log; non-PDF attachments fall back to the legacy download path automatically. + +The drop-in replacement is the new helper on `ir.attachment`: +```python +return att.action_fusion_preview(title='My Doc') +# vs. the old pattern: +# return {'type': 'ir.actions.act_url', +# 'url': '/web/content/%s?download=true' % att.id, +# 'target': 'new'} +``` + +The helper auto-detects mimetype: PDFs go to the dialog, everything else (ZPL, CSV, XML, images) stays on download. So a callsite that today serves CSV today and a PDF tomorrow doesn't need a code change — same call, different routing. + +If you need to invoke the client action directly (rare — only when you don't have a recordset handy), the tag is `fusion_pdf_preview.open_attachment` and the params are `{attachment_id, title, model_name, record_ids, report_name}`. See `fusion_pdf_preview/static/src/js/open_attachment_action.js`. + +Existing reports (`ir.actions.report` of type `qweb-pdf`) are intercepted automatically by `fusion_pdf_preview/static/src/js/pdf_preview.js`; the helper above is for the *other* pattern — attachments opened by custom buttons. + ## Supabase Knowledge Base Before starting unfamiliar work, check Supabase for context: ```bash diff --git a/fusion_canada_post/models/fusion_cp_shipment.py b/fusion_canada_post/models/fusion_cp_shipment.py index 59766873..e23f1445 100644 --- a/fusion_canada_post/models/fusion_cp_shipment.py +++ b/fusion_canada_post/models/fusion_cp_shipment.py @@ -252,10 +252,23 @@ class FusionCpShipment(models.Model): } def _action_open_attachment(self, attachment): - """Open an attachment PDF in the browser viewer (new tab).""" + """Open an attachment for the operator. + + Delegates to ir.attachment.action_fusion_preview when + fusion_pdf_preview is installed — PDFs render in the preview + dialog, anything else downloads. Falls back to the legacy + new-tab URL when the helper isn't available. See CLAUDE.md + "PDF Preview" for the contract. + """ self.ensure_one() if not attachment: return False + if hasattr(attachment, 'action_fusion_preview'): + return attachment.action_fusion_preview( + title=attachment.name or 'Shipping Label', + model_name=self._name, + record_ids=self.id, + ) return { 'type': 'ir.actions.act_url', 'url': '/web/content/%s?download=false' % attachment.id, diff --git a/fusion_claims/.gitignore b/fusion_claims/.gitignore new file mode 100644 index 00000000..7a95436d --- /dev/null +++ b/fusion_claims/.gitignore @@ -0,0 +1 @@ +.superpowers/ diff --git a/fusion_claims/CLAUDE.md b/fusion_claims/CLAUDE.md new file mode 100644 index 00000000..062617a8 --- /dev/null +++ b/fusion_claims/CLAUDE.md @@ -0,0 +1,3106 @@ +# fusion_claims — Claude Code Instructions + +> Read together with the repo-root `../CLAUDE.md` (Odoo 19 rules, naming, dark-mode SCSS pattern, PDF preview helper, Supabase KB credentials). This file documents only what is specific to `fusion_claims`. + +## 1. What this module is + +- **Name**: Fusion Claims (declared as `application: True` — it owns its own top-level menu "ADP Claims"). +- **Category**: Sales. +- **License**: OPL-1 (Nexa Systems Inc.). +- **Version**: `19.0.8.0.7` (bump on every CSS/asset change — see asset cache-busting in repo CLAUDE.md). +- **Purpose**: end-to-end management of Ontario funder claims for assistive devices. Single sale-order record drives the entire lifecycle across **eight distinct funder workflows**, each with its own state machine, wizards, emails, and reports. The largest module in the repo by far. +- **Customers**: Westin Healthcare (primary) and NEXA Systems internal use. + +## 2. Dependencies + +### Odoo addons (`__manifest__.py:75-88`) + +``` +base, sale, sale_management, sale_margin, purchase, +account, sales_team, stock, calendar, ai, +fusion_ringcentral, fusion_tasks +``` + +- **`ai`** (Odoo 19 native): used for the `ai.agent` integration. The `ai_agent_fusion_claims` record + 3 server-action tools (`_fc_tool_search_clients`, `_fc_tool_client_details`, `_fc_tool_claims_stats`) live in `data/ai_agent_data.xml` and `models/ai_agent_ext.py`. Channels of type `ai_chat` are spawned from `fusion.client.profile.action_open_ai_chat`. +- **`fusion_tasks`** (sibling Nexa module): provides + - `fusion.email.builder.mixin` — the `_email_build(title, summary, sections, note, email_type, button_url, ...)` helper used by ~50 email methods in `models/sale_order.py`. Without this dependency, every email send breaks. + - `fusion.technician.task` — the base model that `models/technician_task.py` inherits from to add `sale_order_id` / `purchase_order_id` links, delivery/pickup hooks, and rental inspection fields. +- **`fusion_ringcentral`** (sibling Nexa module): click-to-dial + softphone for any phone field. No code in this module calls into it directly — it's a runtime UX dependency. +- **`calendar`**: `schedule_assessment_wizard` creates `calendar.event` records with an email alarm 1 day before. +- **`sale_margin`**: used by the landscape report (margin column). + +### ⚠ Soft (undeclared) dependency: `fusion_faxes` + +`wizard/odsp_submit_to_odsp_wizard.py` calls into `fusion_faxes.send.fax.wizard` (the fax composer) and reads `partner.x_ff_fax_number` — **but `fusion_faxes` is NOT in `__manifest__.py.depends`**. The fax actions are guarded by `hasattr` checks so the wizard still loads if `fusion_faxes` is missing, but the "Send Fax" / "Send Email + Fax" buttons will fail at click-time. If you're moving this module to a new database, install `fusion_faxes` alongside it. + +### ⚠ Reverse-dependency: `fusion_authorizer_portal` always installed alongside + +The dependency direction is **`fusion_authorizer_portal` → `fusion_claims`** (hard, declared in fusion_authorizer_portal's manifest), but fusion_claims uses APIs that only exist when fusion_authorizer_portal is installed: + +- `sale.order._apply_pod_signature_to_approval_form` imports `PDFTemplateFiller` from `odoo.addons.fusion_authorizer_portal.utils.pdf_filler` — `ImportError` if missing. +- `fusion.page11.sign.request` renders PDFs using `fusion.pdf.template` records — that **model lives in fusion_authorizer_portal**, not here. +- The `/page11/sign/` URL that the Page 11 wizard generates is handled by `fusion_authorizer_portal.controllers.portal_page11_sign` — without it the public signing flow is dead. +- `page11_sign_request._generate_signed_pdf` references `fusion.assessment` records — that model also lives in fusion_authorizer_portal. + +In practice both modules are always installed together. See §29 for the full integration map. + +### External Python (`__manifest__.py:89-91`) + +- **`pdf2image`**, **`PIL`** — required (manifest declares). +- **`pdfrw`** — optional, used by `wizard/odsp_sa_mobility_wizard.py` to fill the SA Mobility government PDF form. Module logs a warning and disables that wizard if missing. +- **`requests`** — used implicitly by AI calls (`models/client_chat.py`) and SMS (`_twilio_send_sms`). Inherited from base Odoo deps. + +### Post-init hook (`__init__.py:14-54`) + +`_load_adp_device_codes` runs on install **and** every upgrade. Two idempotent steps: + +1. `fusion.adp.device.code._load_packaged_device_codes()` imports `data/device_codes/adp_mobility_manual.json` (~hundreds of records) via `import_from_json`. +2. `_link_products_to_device_codes()` runs two SQL `UPDATE` statements: one links `product_template.x_fc_adp_device_code_id` for products that already have `x_fc_adp_device_code` set and matches a device code, and one toggles `x_fc_is_adp_product = TRUE` for products with a code but no flag. Both are guarded by `IS NULL` checks — preserve idempotence if you edit them. + +## 3. Funder workflows (the architecture) + +`x_fc_sale_type` on `sale.order` (`models/sale_order.py:320-339`) selects one of: + +``` +adp, adp_odsp, odsp, wsib, direct_private, insurance, +march_of_dimes, muscular_dystrophy, other, rental, hardship +``` + +Once any non-quotation status is set on a funder workflow, `x_fc_sale_type_locked` becomes True (`models/sale_order.py:347-371`) and the dropdown is read-only — override via setting `fusion_claims.allow_sale_type_override`. + +Each funder has its own status field, wizards, kanban columns, emails, and (in some cases) submission helpers: + +| Funder | Status field | Module is_* flag | show_* flag | Key wizards | +|---|---|---|---|---| +| **ADP** (Assistive Devices Program) | `x_fc_adp_application_status` (22 states) | `x_fc_is_adp_sale` | implicit | schedule_assessment, assessment_completed, application_received, ready_for_submission, submission_verification, device_approval, ready_for_delivery, ready_to_bill, case_close_verification, status_change_reason, send_page11 | +| **MOD** (March of Dimes HVMP) | `x_fc_mod_status` (14 states) | `x_fc_is_mod_sale` | `x_fc_show_mod_fields` | mod_submission_path, send_to_mod (drawing/quotation/POD), mod_awaiting_funding, mod_funding_approved, mod_funding_denied, mod_pca_received, mod_resubmit, mod_submission_confirmed | +| **ODSP Standard** | `x_fc_odsp_std_status` | `x_fc_is_odsp_sale` | `x_fc_show_odsp_fields` | odsp_submit_to_odsp, odsp_pre_approved, odsp_ready_delivery | +| **ODSP SA Mobility** | `x_fc_sa_status` | (subset of ODSP) | | odsp_sa_mobility (fills gov PDF via pdfrw) | +| **ODSP Ontario Works** | `x_fc_ow_status` | (subset of ODSP) | | odsp_discretionary | +| **WSIB** | `x_fc_wsib_status` | `x_fc_is_wsib_sale` | `x_fc_show_wsib_fields` | (generic funder transitions) | +| **Insurance** | `x_fc_insurance_status` | `x_fc_is_insurance_sale` | `x_fc_show_insurance_fields` | (generic funder transitions) | +| **MDC** (Muscular Dystrophy) | `x_fc_mdc_status` | `x_fc_is_mdc_sale` | `x_fc_show_mdc_fields` | (generic funder transitions) | +| **Hardship** | `x_fc_hardship_status` | `x_fc_is_hardship_sale` | `x_fc_show_hardship_fields` | (generic funder transitions) | +| Direct/Private, Other, Rental | — | — | — | invoiced directly, no funder lifecycle | + +`x_fc_odsp_division` distinguishes the three ODSP sub-workflows; `_get_odsp_status()` returns whichever of `x_fc_sa_status`/`x_fc_odsp_std_status`/`x_fc_ow_status` is active. + +## 4. The ADP workflow (the spine) + +### 4.1 Status field + +`x_fc_adp_application_status` (`models/sale_order.py:2302-2333`) — 22 states. Workflow sequence enforced by `_STATUS_ORDER` (`models/sale_order.py:2361-2384`) which drives the kanban column order (`_read_group` override at line 2393). + +``` +quotation → assessment_scheduled → assessment_completed → waiting_for_application → +application_received → ready_submission → submitted → + accepted (within 24h) / rejected → resubmitted → + needs_correction → (corrected then back to submitted/resubmitted) → +approved / approved_deduction → ready_delivery → ready_bill → billed → case_closed + +Special branches: + on_hold (any time) ←→ resume_from_hold (back to previous status) + withdrawn → resubmit_from_withdrawn (back to ready_submission) + denied → resubmit_from_denied (back to ready_submission) + cancelled → reopen (only if not reported to ADP) + expired (12 months after approved with no delivery) → reopen / duplicate_for_reassessment +``` + +There is also a **legacy** `x_fc_adp_status` field (7-state, simpler) — keep it in mind but do NOT use it for new logic. + +### 4.2 Status transitions are NEVER set via dropdown + +Every controlled status (every transition that should fire an email, write to chatter, or update related records) lives on a button that opens a dedicated wizard. `static/src/js/status_selection_filter.js` registers a `filtered_status_selection` field that **hides** controlled statuses from the dropdown: + +```javascript +const CONTROLLED_STATUSES = [ + 'assessment_scheduled', 'assessment_completed', 'application_received', + 'ready_submission', 'submitted', 'resubmitted', 'approved', + 'approved_deduction', 'ready_bill', 'billed', 'case_closed', + 'on_hold', 'withdrawn', 'denied', 'cancelled', 'needs_correction', +]; +``` + +When you must bypass validation (legitimate framework calls like sync paths), pass `with_context(skip_status_validation=True)`. + +### 4.3 Two-stage verification system + +| Stage | Wizard | What it captures | When | +|---|---|---|---| +| **1. Submission** | `submission_verification_wizard.py` (397 lines) | `x_fc_submitted_device_types` (JSON dict `{device_type: True}` of device types submitted), groups lines by `fusion.adp.device.code.device_type` | Before `submitted` | +| **2. Approval** | `device_approval_wizard.py` (724 lines) | per-line `x_fc_adp_approved`, plus optional deductions and approval-letter attachments | After ADP approves | + +Stage 1 is dual-purpose — when invoked with context `submit_application=True`, the SAME wizard also writes status to `submitted` (or `resubmitted` if current status is `needs_correction`) and stores the final application PDF (`x_fc_final_submitted_application`) + XML file (`x_fc_xml_file`). It validates the application is PDF + XML is `.xml`. + +When stage 2 is complete (`x_fc_device_verification_complete = True`) lines with `x_fc_adp_approved = False` flip to client-100% (see §6). Header-level rollups: `x_fc_approved_device_count`, `x_fc_total_device_count`, `x_fc_device_approval_done`, `x_fc_has_unapproved_devices`. + +The device approval wizard can be re-opened from the **client invoice** if it was created before approval — `account.move.action_open_device_approval_wizard` finds the linked SO and routes there. + +**Stage 2 in `mark_as_approved` mode** (context flag) — the wizard does THREE more things: +1. Sets status to `approved` (no deductions) or `approved_deduction` (any deduction applied). +2. Saves `claim_number`, `claim_approval_date`, `approval_letter` (single PDF) — plus a Many2many `approval_photo_ids` of approval screenshots. +3. **Critical attachment persistence fix**: many2many_binary attachments uploaded to a TransientModel are garbage-collected when the wizard closes. The wizard **copies** the attachment data into NEW `ir.attachment` records linked to `sale.order` so they persist. Both single-file attachments AND screenshots are posted in a SINGLE chatter card (`alert-success`) with all `attachment_ids` attached at once. +4. Calls `_sync_approval_to_invoices(updated_lines)` — for each existing invoice, updates lines so unapproved items get 100% to client (price=0 on ADP invoice, full subtotal on client invoice) and approved items get the normal split. Tracks `invoices_updated` count for the notification. + +### 4.3.1 Assessment completed override mode + +`assessment_completed_wizard` has a **scheduling-override** branch: if status is currently `quotation` (the user is skipping the scheduled-assessment step entirely), then: +- `is_override` computes True. +- `override_reason` becomes mandatory (raises `UserError` if blank). +- The chatter card uses yellow `alert-warning` styling with the override reason shown. +- If `notify_authorizer=True`, the email to the authorizer includes a yellow note explaining the override. +- Normal (non-override) path uses green `alert-success` styling. + +Also validates `completion_date >= assessment_start_date`. + +### 4.4 Three-mode "Application Received" intake (`application_received_wizard`) + +The wizard handles three distinct ways Pages 11 & 12 (client consent) can arrive — this is a key business invariant: + +| Mode | Field set | Description | +|---|---|---| +| **bundled** | `x_fc_pages_11_12_in_original = True` | A single PDF that already contains the signed pages 11 & 12 (no separate file needed) | +| **separate** | `x_fc_signed_pages_11_12` + `x_fc_signed_pages_filename` populated | Original application + a separate PDF with the signed pages | +| **remote** | A `fusion.page11.sign.request` exists in state `sent` or `signed` | Page 11 sent to client/agent for digital signature via `/page11/sign/` | + +`x_fc_has_signed_pages_11_12` is a **computed boolean** that returns True if ANY of the three conditions hold — DO NOT check `x_fc_signed_pages_11_12` directly to gate workflow steps, that misses bundled and remote modes. (`models/sale_order.py:2921-2942`.) + +The wizard does **two layers of PDF validation**: +1. Filename constraint — must end in `.pdf` (case-insensitive). +2. **Magic-byte check** — base64-decoded payload must start with `%PDF-`. Test fixtures use `b'%PDF-1.4\n%fake pdf for tests'` to pass. + +`default_get` picks the initial mode based on existing state: bundled flag set → bundled; separate file present → separate; pending sign request → remote; otherwise bundled. + +The wizard has an inline button `action_request_page11_signature` that opens `send_page11_wizard` without leaving the parent wizard. + +### 4.5 Document locks (separate from `x_fc_case_locked`) + +`sale.order.write` enforces document-level locks based on workflow status (`models/sale_order.py:7289-7382`): + +| Document field(s) | Locked when status ≥ | +|---|---| +| `x_fc_original_application`, `x_fc_signed_pages_11_12` (+ filenames) | `submitted` | +| `x_fc_final_submitted_application`, `x_fc_xml_file` (+ filenames) | `approved` | +| `x_fc_approval_letter` (+ filename) | `billed` | +| `x_fc_proof_of_delivery` (+ filename) | `billed` | + +Bypass requires **both**: setting `fusion_claims.allow_document_lock_override = True` AND user in `group_document_lock_override`. Context flag `skip_document_lock_validation=True` bypasses for programmatic writes only. + +### 4.6 Case-wide lock (`x_fc_case_locked`) + +Distinct from the per-document status locks above — this is a **manual** lock toggled via the "Case Locked" switch in the ADP Order Trail tab. When True, the `write` override blocks **all** `x_fc_*` field writes except: + +- `x_fc_case_locked` (so you can untoggle it) +- `message_main_attachment_id`, `message_follower_ids`, `activity_ids` (Odoo plumbing) + +Used for archiving completed legacy cases. Bypass with `with_context(skip_all_validations=True)` (used by crons/email-tracking). + +### 4.7 Document audit trail in chatter + +The same `write` override (`models/sale_order.py:7384-7430`) preserves the OLD copy of any replaced document (`x_fc_original_application`, `x_fc_signed_pages_11_12`, `x_fc_final_submitted_application`, `x_fc_xml_file`, `x_fc_proof_of_delivery`, `x_fc_approval_letter`) in chatter **before** it gets overwritten. Set `with_context(skip_document_chatter=True)` to suppress. + +### 4.8 `reason_for_application` field (12 values) + +`x_fc_reason_for_application` controls invoicing rules and required fields: + +``` +first_access — First Time Access (NO previous ADP) +additions — Additions +mod_non_adp — Modification/Upgrade — original NOT through ADP +mod_adp — Modification/Upgrade — original through ADP +replace_status — Replacement — Change in Status +replace_size — Replacement — Change in Body Size +replace_worn — Replacement — Worn out (past useful life) +replace_lost — Replacement — Lost +replace_stolen — Replacement — Stolen +replace_damaged — Replacement — Damaged beyond repair +replace_no_longer_meets — Replacement — No longer meets needs +growth — Growth/Change in condition +``` + +Rules: +- `previous_funding_date` is **required** for all reasons except `first_access` and `mod_non_adp`. +- `<5 years` warning: `x_fc_under_5_years` (computed from `previous_funding_date`) — if True and reason ∈ {`replace_status`, `replace_size`, `replace_worn`}, posts a chatter warning when creating client invoice. (Surfaces possible ADP deductions.) +- **Modification reasons** (`mod_non_adp`, `mod_adp`) **block** client invoice creation until status reaches `approved` or `approved_deduction` — surfaced as a `danger`-styled sticky notification. + +### 4.9 Status-driven side effects in `sale.order.write` + +The 800-line `write` override (`models/sale_order.py:7225-8023`) does much more than save fields. When ADP status changes, it: + +**Auto-populates dates** (only if not already in `vals`): +| Status target | Date field auto-set | +|---|---| +| `assessment_scheduled` | `x_fc_assessment_start_date` = today | +| `assessment_completed` | `x_fc_assessment_end_date` = today, then auto-advances to `waiting_for_application` | +| `submitted` / `resubmitted` | `x_fc_claim_submission_date` = today | +| `accepted` | `x_fc_claim_acceptance_date` = today | +| `approved` / `approved_deduction` | `x_fc_claim_approval_date` = today | +| `billed` | `x_fc_billing_date` = today | + +**Required-field gates** (raise `UserError` if missing — also enforced by the dedicated wizards, but this is the second safety net): + +| Status target | Required fields | +|---|---| +| `assessment_completed` | assessment_start_date, assessment_end_date | +| `application_received` | assessment_start_date | +| `ready_submission` | assessment dates, reason_for_application, client_ref_1, client_ref_2, claim_authorization_date, previous_funding_date (if reason needs it), original_application, **`x_fc_signed_pages_11_12`** — NOTE: this gate uses the raw field, NOT `x_fc_has_signed_pages_11_12`. The application_received_wizard sidesteps this by populating one of the three computed-sources, but a direct write may fail; see gotcha #21 below. | +| `submitted` / `resubmitted` | final_submitted_application, xml_file, claim_submission_date | +| `approved` / `approved_deduction` | claim_number, claim_approval_date | +| `ready_bill` | adp_delivery_date, proof_of_delivery | +| `billed` | billing_date | +| `case_closed` | billing_date | +| MOD `contract_received` | x_fc_case_reference (HVMP Reference Number) | +| MOD `pod_submitted` | x_fc_mod_proof_of_delivery | + +**Authorizer required-field gate** (only fires when authorizer-related fields are in vals: `x_fc_sale_type`, `x_fc_authorizer_id`, `x_fc_authorizer_required`, `x_fc_adp_application_status`): +- Always required for: `adp`, `adp_odsp`, `wsib`, `march_of_dimes`, `muscular_dystrophy` +- Optional based on `x_fc_authorizer_required='yes'` for: `odsp`, `direct_private`, `insurance`, `other` +- Never required for: `rental` + +**Resume from on_hold checks 3-month assessment validity** — if `x_fc_assessment_expired` is True (>90 days since `x_fc_assessment_end_date`), blocks resume with `UserError` and a chatter warning showing days past expiry. The OT must redo the assessment. + +**`needs_correction` document clearing** — when status changes to `needs_correction`, the override: +1. Posts the existing `x_fc_final_submitted_application` and `x_fc_xml_file` to chatter for preservation. +2. **Clears** these fields plus `x_fc_final_application_filename`, `x_fc_xml_filename`, `x_fc_claim_submission_date`. +3. Posts a yellow warning notice. + +**Submission history auto-creation** — on `submitted` / `resubmitted`, creates a `fusion.submission.history` record (type `initial` or `resubmission`) and **resets `x_fc_acceptance_reminder_sent`** so the acceptance reminder fires again for the new submission cycle (2026-04 anti-spam fix). + +**Submission history result update** — on `accepted` or `rejected`, finds the most recent pending submission record (by date desc, limit 1) and calls `update_result()` to mark it. + +**MOD follow-up counter reset** — on ANY real MOD status change (detected via pre-write snapshot `old_mod_status_by_id`), resets `x_fc_mod_followup_month_count`, `_month_start`, `_escalated`, `_cap_notified`. This is the "new chapter" reset that makes the rolling cap work correctly. + +**MOD auto-stamp dates** — `quote_submitted` stamps `x_fc_case_submitted` if blank; `funding_approved` stamps `x_fc_case_approved` if blank. + +**POD signature auto-overlay** — when `x_fc_pod_signature` is set, calls `_apply_pod_signature_to_approval_form` (overlays the client signature onto the SA Mobility government PDF), and auto-advances SA Mobility from `ready_delivery` → `delivered`. Bypass via `skip_pod_signature_hook=True` context. + +**Field-mapping aware recompute** — reads ICP `fusion_claims.field_sale_type` / `field_so_client_type` and triggers `_compute_is_adp_sale` + line-level `_compute_adp_portions` if those fields (or their mapped equivalents) changed. + +**Sync to invoices** — when sync fields change (`x_fc_claim_number`, `x_fc_client_ref_1/2`, `x_fc_adp_delivery_date`, `x_fc_authorizer_id`, `x_fc_client_type`, `x_fc_primary_serial`, `x_fc_service_start/end_date`), calls `_sync_fields_to_invoices` to push to all linked invoices. + +### 4.10 Special transitions matrix + +Each special transition has its own allowed-from set (`models/sale_order.py:4426-4762`): + +| Action | Allowed FROM | Effect | +|---|---|---| +| `action_adp_put_on_hold` | `approved`, `approved_deduction` ONLY (2026-04 rule) | Status → `on_hold`, records previous status, resets hold-reminder flags | +| `action_adp_withdraw` | `submitted`, `resubmitted`, `accepted`, `ready_submission`, `on_hold` | Status → `withdrawn`, records previous status, sends withdrawal email | +| `action_adp_mark_rejected` | `submitted`, `resubmitted`, `accepted` | Status → `rejected` | +| `action_adp_mark_denied` | `submitted`, `resubmitted`, `accepted`, `approved`, `approved_deduction` | Status → `denied` | +| `action_adp_mark_needs_correction` | `submitted`, `resubmitted`, `accepted`, `approved`, `approved_deduction`, `rejected` | Status → `needs_correction` (clears submission docs, see §4.9) | +| `action_adp_cancel` | Anything except `case_closed`, `cancelled`, `expired`, `billed` | Status → `cancelled`, records previous status | +| `action_adp_reopen_cancelled` | `cancelled` AND `x_fc_cancel_reported_to_adp=False` | Restores to previous status. If `x_fc_cancel_reported_to_adp=True`, REJECTS and points to `duplicate_for_reassessment`. | +| `action_adp_reopen_expired` | `expired` | **Back-compat shim** — just calls `action_adp_duplicate_for_reassessment` (per 2026-04 policy: expired cases cannot be self-renewed) | +| `action_adp_duplicate_for_reassessment` | `expired`, `cancelled` | Creates a new SO with `x_fc_previous_sale_order_id` pointing here; new order starts at `quotation` | +| `action_adp_resubmit_from_denied` | `denied` | Status → `ready_submission` for fresh attempt | + +`_adp_chatter_transition(title, icon, colour_class, details)` is the shared helper for posting Bootstrap-styled cards (`alert-warning` / `alert-danger` / `alert-secondary` / `alert-info` / etc.). + +### 4.11 Cron-driven transitions + +- `_cron_adp_expire_approved` (3 AM daily): approved orders past `fusion_claims.adp_approval_expiry_months` (default 12) auto-transition to `expired`. Now scans `approved`, `approved_deduction`, **and** `on_hold` + `ready_delivery` (2026-04 update — funding window applies regardless of intermediate state). +- `_cron_auto_close_billed_cases` (daily): `billed` → `case_closed` 1 month after billing. +- `_cron_adp_hold_expiry_reminders` (9:30 AM daily): monthly reminder to client (authorizer **excluded** per 2026-04 policy) on on-hold cases; one final pre-expiry warning to client + authorizer ~30 days before funding window closes. + +## 5. The MOD (March of Dimes HVMP) workflow + +The Home and Vehicle Modification Program is its own complete lifecycle in `models/sale_order.py:438-985, 8590-9805`. + +### 5.1 Status (`x_fc_mod_status`) + +``` +quotation → assessment_scheduled → assessment_completed → processing_drawing → +quote_submitted → handed_off (client or authorizer) → awaiting_funding → + funding_approved → contract_received (PCA) → in_production → + project_complete → pod_submitted → case_closed + funding_denied → resubmit / cancel +on_hold / resume +``` + +### 5.2 Submission paths + +`mod_submission_path_wizard` records who submits the application to MOD: + +- **internal** — we submit; auto-triggers `_send_mod_vod_request_email` to the authorizer the first time it's selected (so they fill the Verification of Disability form and send it back). Settings: `company.x_fc_mod_vod_form` is the latest blank VOD form, auto-attached. +- **client** — client submits themselves. +- **authorizer** — OT submits. + +For non-internal paths, `_cron_mod_handoff_followup` creates an `mail.activity.type.mod_followup` activity for the office contact (or sales rep, per `company.x_fc_mod_followup_assignee_mode`) every 14 days. + +### 5.3 Follow-up rolling cap & cron architecture + +The MOD follow-up system has THREE distinct loops sharing one rolling 2-per-30-days cap (`fusion_claims.mod_followup_max_per_month` × `_window_days`): + +| Cron | Time | What it does | +|---|---|---| +| `_cron_mod_schedule_followups` | 8 AM daily | For orders in `quote_submitted` / `awaiting_funding`: if no open follow-up activity AND cap not reached AND `x_fc_mod_next_followup_date` is in the past, creates a new `mail.activity` (assignee = sales rep), `automated=True` to suppress Odoo's default "activity assigned" email. Increments month counter, bumps `x_fc_mod_next_followup_date` by 14 days. Per-run throttle `fusion_claims.mod_followup_schedule_max_per_cron_run` = 10 (default). | +| `_cron_mod_escalate_followups` | 10 AM daily | For overdue follow-up activities (`date_deadline <= today - 3 days`): processes oldest-first. If status moved past follow-up phase → unlinks stale activity. Else calls `_send_mod_followup_email`; if it returns True (sent), unlinks activity. If cap blocks the send, activity stays put so a human can action it. Per-run throttle `fusion_claims.mod_followup_max_per_cron_run` = 10. | +| `_cron_mod_handoff_followup` | 9 AM daily | For `handoff_to_client` orders: creates an activity (assignee from `company.x_fc_mod_followup_assignee_mode`) with deadline +3 days. Same rolling cap, dedup against existing open activities. Summary includes `({N} days since handoff)`. | + +`_mod_followup_cap_state()` is a **pure read** that returns `(within_cap, reset_needed, new_start, max_per_month)` — call it before creating activities or sending emails, then mutate the order's counters via the returned tuple. The `x_fc_mod_followup_cap_notified` one-shot flag posts a chatter note the first time the cap blocks a send in a given window, resets when the window expires. + +**Activity dedup pattern**: every MOD cron checks for an existing open `mail.activity` of type `mail_activity_type_mod_followup` on the order before creating a new one — prevents daily activity spam. + +`_send_mod_followup_email` enforces the cap internally and returns True only when an email actually goes out. The escalator cron uses that boolean to decide whether to unlink the activity. + +### 5.4 send_to_mod_wizard (multi-mode email composer) + +Three modes — `drawing`, `quotation`, `completion` — driven by the `mod_wizard_mode` context flag. The wizard pre-fills recipients differently per mode, validates per-mode required files, advances MOD status, and attaches the right documents. + +| Mode | Required uploads | Status target | TO | CC | Attachments | +|---|---|---|---|---|---| +| `drawing` | `drawing_file` | `quote_submitted`, stamps `x_fc_mod_drawing_submitted_date` | Client | MOD partner (auto-created by email), Authorizer, Sales Rep | Quotation PDF (rendered from `fusion_claims.action_report_mod_quotation`) + Drawing + Initial Photos | +| `quotation` | none — just re-send | (no change) | Client | MOD partner, Authorizer, Sales Rep | Quotation PDF + (toggle-controlled) Drawing + Initial Photos | +| `completion` | `completion_photos_file` AND `pod_file` | `pod_submitted`, stamps `x_fc_mod_pod_submitted_date` | Case worker (or MOD partner fallback) | Authorizer, Sales Rep | Completion Photos + POD | + +**Subject line includes HVMP reference if set**: `"{prefix} - {ref} - {client_name}"` when `x_fc_case_reference` is populated; otherwise `"{prefix} - {client_name} - {order.name}"`. + +`_DOC_NAMES` dict (`wizard/send_to_mod_wizard.py` module-level) maps internal field name → user-facing label: +```python +'x_fc_mod_drawing' → 'Drawing' +'x_fc_mod_initial_photos' → 'Assessment Photos' +'x_fc_mod_pca_document' → 'Payment Commitment Agreement' +'x_fc_mod_proof_of_delivery' → 'Proof of Delivery' +'x_fc_mod_completion_photos' → 'Completion Photos' +``` + +`_pro_name(field_name, order, orig_filename)` builds professional attachment names: `"{display_name} - {client_name_underscored} - {order.name}.{ext}"`. Example: `"Drawing - John_Doe - S29958.pdf"`. Spaces and commas are stripped from the client name. + +`_get_field_att(order, field_name)` — finds the existing `ir.attachment` for a binary field (Odoo auto-creates one per `attachment=True` field), **renames it** in-place to the pro format, and returns the record. Don't create a new attachment for binary fields — the helper reuses the existing one. + +The MOD partner is auto-created on first use via `_get_mod_partner(email)` — `name='March of Dimes Canada (HVMP)'`, `is_company=True`, `company_type='company'`. + +### 5.5 Documents tracked on the SO + +Binary fields prefixed `x_fc_mod_*`: `drawing`, `initial_photos`, `pca_document` (Payment Commitment Agreement), `proof_of_delivery`, `completion_photos`, `application_form_doc`, `vod_letter`, `notice_of_assessment`, `property_tax`, `proposal_doc`. Plus the audit-trail booleans `x_fc_mod_trail_*` (computed by `_compute_mod_trail` / `_compute_mod_audit_trail`). + +### 5.6 MOD invoicing + +`_create_mod_invoice(partner_id, invoice_lines, portion_type, label)` and `action_mod_send_invoice` on `account.move`. Invoices use `x_fc_invoice_type = 'march_of_dimes'`; the MOD invoice template + send action attach the PDF and email the case worker. Distinct from ADP split invoicing. + +## 6. ADP/Client portion calculation rules + +The heart of the billing logic lives in `models/sale_order_line.py:148-267` (`_compute_adp_portions`) and is mirrored in `models/account_move_line.py:96-164` for invoice lines. + +For each line, the algorithm: + +1. **Skip if not ADP sale** — `order._is_adp_sale()` returns False unless `x_fc_sale_type` contains `adp`. ADP portion = 0, client portion = 0 (the line is "regular" billing). +2. **NON-ADP funded products → client 100%**. `product.product.is_non_adp_funded()` returns True when the device code (case-insensitive, prefix match) starts with: `NON-ADP`, `NON-FUNDED`, `UNFUNDED`, `NOT-FUNDED`, `ACS`, `ODS`, `OWP`. These are explicitly out-of-ADP-scope codes used to bill ancillary items on a single SO. +3. **Product without a valid ADP device code → client 100%**. The line must have a code that resolves against `fusion.adp.device.code` (`active=True`). Code lookup order on the product template (see `sale_order_line._get_adp_device_code`): + 1. `x_fc_adp_device_code` (this module's field) + 2. `x_adp_code` (Studio/legacy field) + 3. `default_code` (internal reference) + 4. Code in parentheses in the product name, e.g. `[MXA-1618] GEOMATRIX SILVERBACK MAX BACKREST - ACTIVE (SE0001109)` → `SE0001109`. The strict regex on `account.move.line` is `r'\(([A-Z]{2}\d{7})\)'`. +4. **Verification complete AND line not approved → client 100%**. (`x_fc_device_verification_complete=True` and `x_fc_adp_approved=False`.) +5. **Otherwise — split by client type:** + - **REG** → 75% ADP / 25% client. + - **ODS, OWP, ACS, LTC, SEN, CCA** → 100% ADP / 0% client. +6. **Deductions** (applied after the base split): + - **PCT** — `effective_adp_pct = base_adp_pct × (deduction_value / 100)`; client gets the rest. + - **AMT** — subtract `deduction_value` (per unit) from the base ADP portion (floored at 0); client gets the rest. + +### 6.1 Price-source priority for the calculation + +When computing the ADP base (NOT `price_unit` × qty): + +1. `product.product_tmpl_id.x_fc_adp_price` (this module's stored price) +2. line `x_fc_adp_max_price` (override at the line level) +3. line `price_unit` (last resort) + +Invoice lines (`account_move_line._compute_adp_portions`) prefer `fusion.adp.device.code.adp_price` directly via a `search` — slightly different priority chain, but the deduction maths is identical. + +Always use `product.product.get_adp_price()` / `.get_adp_device_code()` rather than reading the fields directly: those helpers honour the legacy field-mapping ICP (see §15). + +### 6.2 Recomputation triggers + +`_compute_adp_portions` `@api.depends` on: `price_subtotal`, `product_uom_qty`, `price_unit`, `product_id`, `order_id.x_fc_sale_type`, `order_id.x_fc_client_type`, `order_id.x_fc_device_verification_complete`, `x_fc_deduction_type`, `x_fc_deduction_value`, `x_fc_adp_max_price`, `x_fc_adp_approved`. Header rollups `x_fc_adp_portion_total` / `x_fc_client_portion_total` recompute on any line change. + +## 7. Split invoicing (the model under §3.1) + +ADP REG sales typically yield **two** invoices on the same SO: + +- **Client invoice** (`x_fc_adp_invoice_portion = 'client'`) — 25% in REG. `action_create_client_invoice` (`models/sale_order.py:5321-5417`) does NOT require device verification (clients can pay before ADP approval) but **blocks** modification-reason cases (`mod_non_adp`, `mod_adp`) until approval is in. A `<5y` replacement triggers a chatter warning. +- **ADP invoice** (`x_fc_adp_invoice_portion = 'adp'`) — 75% in REG, 100% for ODS/OWP/ACS/LTC/SEN/CCA. `action_create_adp_invoice` requires verification + POD (`x_fc_proof_of_delivery`). + +Both link back to the SO via `x_fc_source_sale_order_id` (indexed). Per-order quick-access fields `x_fc_adp_invoice_id` and `x_fc_client_invoice_id` can be set manually for invoices that pre-date this module's tracking. + +### 7.1 Two-way sync + +`account.move.action_sync_to_sale_order` (`models/account_move.py:698-789`) treats the invoice as source of truth: copies `x_fc_claim_number`, `x_fc_client_ref_1/2`, `x_fc_adp_delivery_date`, `x_fc_authorizer_id`, `x_fc_client_type`, `x_fc_service_start/end_date`, `x_fc_primary_serial` back to the SO, then calls `sale.order._sync_fields_to_invoices` to push the values out to all sibling invoices. Serial numbers sync line-by-line (`_sync_line_fields_to_sale_order`) via `sale_line_ids` → `invoice_line_ids` mapping. Loop prevention: pass `with_context(skip_sync=True)` on the writes. + +Mark `is_manually_modified = True` on an invoice to opt it out of the SO → invoice sync direction (the model honours this flag in `_sync_fields_to_invoices`). + +**`_sync_fields_to_invoices` body** (`models/sale_order.py:8067-8145`): +1. For each non-cancelled invoice on the order: +2. Build a `vals` dict of `x_fc_*` fields from the SO, **but ONLY include each key if the field exists on `account.move`** (`in invoice._fields` check). This is defensive — the module doesn't assume Studio fields are present. +3. Write with `skip_sync=True` to prevent recursion. +4. After all invoices are updated, call `_sync_serial_numbers_to_invoices`. + +**`_sync_serial_numbers_to_invoices` body** (`models/sale_order.py:8147-8197`): +- Uses **dynamic field mappings** from settings (`mappings['sol_serial']` and `mappings['aml_serial']`). +- **Each SO line syncs its OWN serial to its linked invoice lines** — no header-fallback. If the SOL has no serial, the AML's serial is left alone. +- Searches via `sale_line_ids` link to find matching invoice lines. +- Bypass: `skip_sync=True` context returns early. + +### 7.2 Sibling totals + +`x_fc_sibling_adp_total` / `x_fc_sibling_client_total` (computed) read the **other** portion's total off the source SO so the PDF report can always show both halves even before the sibling invoice exists. + +### 7.3 `sale.advance.payment.inv` extension + +`wizard/sale_advance_payment_inv.py` adds two new options to the **Create Invoice** wizard's `advance_payment_method`: + +- `adp_client` — "ADP Client Invoice (25%)" — only valid when client_type = `REG`; raises if not ADP sale. +- `adp_portion` — "ADP Invoice (75%/100%)" — for any ADP client type. + +Both route through `sale.order._create_adp_split_invoice(invoice_type='client'|'adp')`, the same method the per-order action buttons use. This means the standard "Create Invoice" UI also produces split invoices when used on ADP orders. + +### 7.4 Payment registration extension + +`wizard/account_payment_register.py` extends the payment register wizard with: + +- `x_fc_card_last_four` (size 4) — required when paying via a card method +- `x_fc_payment_note` — free text +- `x_fc_is_card_payment` (computed) — reads `payment_method_line_id.x_fc_requires_card_digits` (set on the journal form via `views/account_journal_views.xml`). Fallback: keyword match on method name (`credit`, `visa`, `mastercard`, `amex`) + +`action_create_payments` override validates the last-4 input is **exactly 4 numeric digits** before delegating. Values persist onto the created `account.payment` via the `x_fc_card_last_four` / `x_fc_payment_note` fields (`models/account_payment.py`). + +The journal form view adds the "Req. Card #" column to **both** inbound and outbound payment method lists. + +### 7.5 The `_create_adp_split_invoice` body (`models/sale_order.py:5553-5960`) + +Things to know when reading or modifying this 400-line core method: + +1. **Customer switch on ADP invoice** — when `invoice_type='adp'`, the invoice's `partner_id` is set to the ADP partner record (searched by name: `'ADP (Assistive Device Program)'`, `'Assistive Device Program'`, `'ADP'`, or `'ADP -'`). The original client becomes `partner_shipping_id`. So an ADP invoice is billed to ADP, shipped to the client. Client invoices keep the original customer. +2. **`x_fc_invoice_type` is sale-type aware** — client invoices always get `'adp_client'`. ADP-portion invoices use the sale type directly (`adp`, `adp_odsp`, `odsp`, `wsib`, ...), preserving funder context for downstream reports. +3. **`x_fc_adp_billing_status='waiting'`** is auto-set on creation for ADP invoices (kicks off the billing-deadline cron). +4. **`invoice_origin` carries the portion suffix** — `S29958 (Client 25%)` or `S29958 (ADP 75%)`. This is what surfaces in the user-facing breadcrumbs. +5. **Price-mismatch detection AND auto-correction** — when the device-code DB price differs from the product's `x_fc_adp_price` by > $0.01: + - Posts a chatter warning listing each mismatched product. + - Auto-updates the product's `x_fc_adp_price` to the DB price (only for products WITHOUT a `x_fc_adp_device_code_id` link — products with the Many2one are kept managed via the link instead). +6. **`[NOT APPROVED - 100% Client]`** suffix added to the line name on client invoices when an ADP device exists in DB but wasn't approved. Useful audit trail. +7. **Unapproved + non-ADP-funded items are SKIPPED from the ADP invoice entirely** (not even a $0 line). They appear only on the client invoice. +8. **`Markup` chatter cards** at the end of the method are styled with Bootstrap alerts — `alert-primary` (blue) for client invoice, `alert-success` (green) for ADP invoice, both with a "View Invoice" link. + +### 7.6 `_get_invoiceable_lines` override + +`sale.order._get_invoiceable_lines` is overridden (`models/sale_order.py:5965`) to include **ALL** `line_section`, `line_subsection`, and `line_note` lines regardless of position. Standard Odoo only includes display lines that have an invoiceable product line AFTER them — which drops warranty notes, refund policy sections, etc. placed at the bottom of the order. This override keeps them on every invoice. + +### 7.7 `_prepare_invoice` override + invoice type normalization + +`sale.order._prepare_invoice` is overridden (`models/sale_order.py:5995-6021`) to copy ADP fields to the invoice on creation. It **normalizes `x_fc_sale_type`** to lowercase and validates against the selection. If the normalized value isn't in the valid list: +- Contains `'adp'` → falls back to `'adp'` +- Otherwise → falls back to `'other'` + +Note: when called by `_create_adp_split_invoice`, this base `x_fc_invoice_type` gets immediately overwritten — client invoices → `'adp_client'`, ADP invoices → the raw sale_type. The override in `_prepare_invoice` matters mainly for invoices created OUTSIDE the split flow (e.g., via the standard "Create Invoice" button without the ADP method selection). + +### 7.8 Document chatter helper (`_post_document_to_chatter`) + +The shared helper for document audit trail (`models/sale_order.py:6026-6087`): +- Default mode — references the existing `ir.attachment` (Odoo creates one for each `attachment=True` binary field). +- `preserve_copy=True` — creates a SEPARATE `_archived.` copy. Used when the original is about to be deleted/replaced (e.g., needs_correction clearing) and we need to snapshot before Odoo's attachment is removed. +- Posts a `{label} uploaded by {user}` chatter message with the attachment attached. + +Companion utilities: `_build_attachment_name(field_name)` builds the user-facing filename, `_get_document_attachment(field_name)` resolves the existing attachment, `_prepare_attachment_for_email(attachment, field_name)` renames it for outbound mail. + +### 7.9 MOD action method index + +All on `sale.order`, all in `models/sale_order.py:8594-8901`: + +| Method | What it does | Status target | +|---|---|---| +| `action_mod_schedule_assessment` | bare write | `assessment_scheduled`, stamps `x_fc_mod_assessment_scheduled_date` | +| `action_mod_complete_assessment` | bare write | `assessment_completed`, stamps `x_fc_mod_assessment_completed_date` | +| `action_mod_processing_drawing` | writes `processing_drawings`, then opens `send_to_mod_wizard` in `mod_wizard_mode='drawing'` | progresses to `quote_submitted` via the wizard | +| `action_mod_awaiting_funding` | opens `mod_awaiting_funding_wizard` | `awaiting_funding` | +| `action_mod_funding_approved` | opens `mod_funding_approved_wizard` (records case worker + HVMP ref) | `funding_approved` | +| `action_mod_funding_denied` | opens `mod_funding_denied_wizard` (category + reason) — 2026-04 was bare write, now captures denial reason | `funding_denied` | +| `action_mod_contract_received` | opens `mod_pca_received_wizard` (PCA upload + full/partial invoice split) | `contract_received` | +| `action_mod_in_production` | bare write | `in_production`, stamps `x_fc_mod_production_started_date` | +| `action_mod_project_complete` | bare write | `project_complete`, stamps `x_fc_mod_project_completed_date` | +| `action_mod_pod_submitted` | opens `send_to_mod_wizard` in `mod_wizard_mode='completion'` | `pod_submitted` via the wizard | +| `action_mod_close_case` | bare write | `case_closed`, stamps `x_fc_mod_case_closed_date` | +| `action_mod_on_hold` | saves previous status into `x_fc_mod_previous_status_before_hold` (2026-04 fix — was being lost) | `on_hold` | +| `action_mod_resume` | restores from `x_fc_mod_previous_status_before_hold` (default `in_production`) — 2026-04 fix — was hardcoded to `in_production` | previous status | +| `action_mod_set_submission_path` | opens `mod_submission_path_wizard` (internal/client/authorizer) | n/a — sets `x_fc_mod_submitted_by` | +| `action_mod_request_vod` | emails authorizer the blank VOD form (also auto-fired when internal path first selected) | n/a | +| `action_mod_handoff_to_client` | only when `submitted_by ∈ (client, authorizer)`; requires `proposal_doc` + `drawing` | `handoff_to_client`, stamps `x_fc_mod_handoff_date` | +| `action_mod_confirmed_submission` | opens `mod_submission_confirmed_wizard` | confirms client/authorizer submitted | +| `action_mod_resubmit_from_denied` | opens `mod_resubmit_wizard`; only from `funding_denied` | back to earlier status via wizard | +| `action_mod_cancel_from_denied` | only from `funding_denied` | `cancelled` | +| `action_mod_reopen_cancelled` | only from `cancelled` | `need_to_schedule`, clears `x_fc_mod_funding_denial_reason` | +| `action_cancel` (override on `sale.order`) | when the SO is cancelled (built-in), also force-sets `x_fc_mod_status='cancelled'` for MOD orders | `cancelled` | + +`_get_mod_partner()` — finds or **creates** the MOD partner by email `fusion_claims.mod_default_email` (default `hvmp@marchofdimes.ca`). New record is created with name `'March of Dimes Canada (HVMP)'`, `is_company=True`. + +`_create_mod_invoice(partner_id, invoice_lines, portion_type, label)` — the shared MOD invoice creator. Sets `x_fc_invoice_type='march_of_dimes'`, `x_fc_adp_invoice_portion=portion_type`, populates `narration` with HVMP reference + client + case worker + SO + vendor code as an HTML block. + +### 7.10 Stage 2 invoice sync (`_sync_approval_to_invoices`) — re-posts posted invoices + +When the device approval wizard's `mark_as_approved` mode runs `_sync_approval_to_invoices(updated_lines)`, the method walks every non-cancelled invoice linked to the order and rewrites line `price_unit` + suffixes the line name based on the new approval state: + +| State | Client invoice line | ADP invoice line | +|---|---|---| +| Non-ADP funded item (code in NON-ADP/NON-FUNDED/etc.) | `price_unit = full subtotal`, name unchanged | `price_unit = 0`, name suffixed `[NON-ADP - Excluded]` | +| Unapproved ADP device | `price_unit = full subtotal`, name suffixed `[NOT APPROVED - 100% Client]` | `price_unit = 0`, name suffixed `[NOT APPROVED - Excluded]` | +| Approved ADP device | `price_unit = x_fc_client_portion / qty`, name unchanged | `price_unit = x_fc_adp_portion / qty`, name unchanged | + +For **POSTED invoices**, the method calls `invoice.button_draft() → write → action_post()` — i.e. it resets to draft, rewrites the lines, then re-posts. If the posted invoice has already been exported to ADP or emailed to the client, **the reset-then-repost cycle can break sequence numbers, re-fire post-actions, or invalidate exports**. Set `is_manually_modified=True` on the invoice to opt it out of this sync direction if you need to lock it. + +The wizard reports the count of `invoices_updated` in the success notification. + +### 7.11 Activity scheduling pattern (`_schedule_or_renew_adp_activity`) + +On `account.move`, the helper `_schedule_or_renew_adp_activity(activity_type_xmlid, user_id, date_deadline, summary, note)` is the shared pattern for ADP billing/correction activities: + +- **Finds existing** activity of the same type AND same user_id on the record. +- **If found**: UPDATES `date_deadline`, `summary`, `note` (preserves existing note if new is blank). +- **If not found**: creates new via `activity_schedule`. + +Companion `_complete_adp_activities(activity_type_xmlid)` calls `activity.action_feedback(feedback='Completed automatically')` on every activity of the given type — used to auto-close ADP Billing activities when the invoice flips to `submitted`/`payment_issued`, and ADP Correction activities when flipped to `resubmitted`/`payment_issued`. + +This dedup pattern shows up in three places: ADP billing reminders, ADP correction reminders, and MOD follow-ups — different fields, same logic. + +### 7.12 MOD PCA dual-invoice split (`wizard/mod_pca_received_wizard.py`) + +When `contract_received` is set via this wizard, the user picks `approval_type`: + +- **`full`** — creates ONE invoice (to the MOD partner) for the full order amount; client owes nothing. +- **`partial`** — creates TWO invoices simultaneously: + - **MOD invoice**: each line × `(approved_amount / order.amount_untaxed)` = MOD's share. + - **Client invoice**: each line × `(1 - ratio)` = client's share. If the client's per-line amount is ≤ 0 (fully covered), the line name is suffixed `\n[Covered by March of Dimes]` and `price_unit=0`. + +Live preview (`preview_line_ids` — `fusion_claims.mod.funding.approved.wizard.line` transient lines) updates via `_onchange_compute_preview` as the user types `approved_amount`. Both invoices flow through `sale.order._create_mod_invoice(partner_id, invoice_lines, portion_type, label)`. + +**`portion_type='adp'` for the MOD partial invoice**: the wizard reuses the `x_fc_adp_invoice_portion` field (set to `'adp'`) for the MOD invoice as well as the ADP one — so `x_fc_adp_invoice_portion == 'adp'` does NOT mean ADP. Always check `x_fc_invoice_type` (`'march_of_dimes'` vs `'adp'`) when disambiguating. + +## 8. ADP billing lifecycle & posting schedule + +### 8.1 Posting schedule mixin + +`fusion_claims.adp.posting.schedule.mixin` (`models/adp_posting_schedule.py`) is inherited by `sale.order`, `account.move`, and `fusion_claims.adp.export.record`. + +``` +posting cycle = every `fusion_claims.adp_posting_frequency_days` days (default 14) + starting from `fusion_claims.adp_posting_base_date` (default 2026-01-23) +submission deadline = Wednesday 6 PM of the posting week +delivery reminder = Tuesday of the posting week +billing reminder = Monday of the posting week +payment processed = posting day + 7 +payment received = posting day + 10 +``` + +Methods: `_get_next_posting_date`, `_get_current_posting_date`, `_get_posting_week_monday/tuesday/wednesday`, `_get_expected_payment_date`, `_get_payment_processed_date`, `_is_past_submission_deadline` (checks past 6 PM Wed), `_get_adp_billing_reminder_user`, `_get_adp_correction_reminder_users`. + +### 8.2 Billing status (post-export) + +`x_fc_adp_billing_status` on `account.move`: + +``` +not_applicable → waiting → submitted → resubmitted / need_correction → payment_issued / cancelled +``` + +Cron `_cron_renew_billing_reminders` (`account.move`) reschedules overdue `mail_activity_type_adp_billing` activities on `waiting` invoices to the next posting week's Monday. `_cron_renew_correction_reminders` does the same for `need_correction` against `mail_activity_type_adp_correction` activities (scheduled for **all** users in `fusion_claims.adp_correction_reminder_user_ids`). + +Auto-write hook: when `payment_state` transitions to `paid` / `in_payment` on an ADP invoice currently `submitted` / `resubmitted` / `waiting`, the billing status flips to `payment_issued` automatically (`account.move.write` override at `models/account_move.py:884-892`). + +## 9. ADP claim export + +`wizard/adp_export_wizard.py` (`fusion_claims.export.wizard`): + +- Pulls invoices via `active_ids`, filters to `out_invoice` / `out_refund`. +- For each line, regenerates the comma-separated ADP claim line using the device-code database price, then **verifies** the stored `x_fc_adp_portion` / `x_fc_client_portion` against the recomputed values (tolerance `$0.01 × qty`). **CRITICAL: Verification mismatches RAISE a `UserError` that BLOCKS the export** — the previous draft of this doc was wrong; mismatches don't just warn, they hard-stop and the user must fix invoices before re-trying. +- Skips lines whose device code resolves to one of `FUNDING`, `NON-FUNDED`, `N/A`, `NA`, `NON-ADP`, `LABOUR`, `DELIVERY`, or empty. +- **Per-unit-quantity expansion**: ADP expects `qty=1` per line. A line with `qty=3` generates 3 export rows, each with the per-unit portion (`stored_portion / qty`). +- **Filename format is fixed**: `{vendor_code}_{YYYY-MM-DD}.txt` — ADP rejects renamed files. If the same filename already exists in `fusion_claims.adp.export.record`, the wizard adds a yellow warning but still proceeds; the user must manually rename for the resubmission case. +- **CSV format** (no header) — 16 fields per row, comma-separated, with 3 reserved/empty fields at positions 7, 8, 11 (1-indexed): + ``` + vendor_code, claim_number, client_ref_2, invoice_number, invoice_date, + delivery_date, [empty], [empty], device_code, serial_number, + [empty], qty(=1), device_price, adp_portion, client_portion, client_type + ``` +- Creates a `fusion_claims.adp.export.record` (`models/adp_export_record.py`) — the file lives there, the model auto-extracts invoice numbers from the file content and back-links them via `invoice_ids`. Records are grouped by `year` / `month` / `posting_period_label` in the menu. + +### 9.1 ADP export record helpers (`models/adp_export_record.py`) + +| Method | Purpose | +|---|---| +| `_parse_export_filename(filename)` | Regex `^(.+?)_(\d{4}-\d{2}-\d{2})\.\w+$` extracts `(vendor_code, file_date)` from a filename. Returns `(None, None)` if unparseable. | +| `_get_posting_period_for_file(file_date)` | Maps a file date to the **posting period** it belongs to. If `file_date <= current_posting`, returns `current_posting`. Otherwise returns `current_posting + frequency` (next posting). Handles pre-base-date floor division correctly. | +| `_collect_subfolder_ids(Document, parent_ids)` | Recursively finds all `documents.document` subfolder IDs under given parents (used by migration helper). | +| `migrate_from_documents()` | One-shot migration — searches `documents.document` records under "ADP Billing Files" hierarchy, creates export records, archives the originals (`active=False`). Idempotent (skips by filename). Called via "Migrate ADP Export Files" button in Settings. Only runs if the `documents` app is installed. | +| `action_download` | Single-file download via `/web/content` URL. | +| `action_download_zip` | **Multi-record action** — zips all selected export records into a single `ADP_Export_Files_{YYYY-MM-DD}.zip` via `zipfile.ZIP_DEFLATED` and serves it as a transient `ir.attachment`. | +| `action_view_invoices` | Opens the list of `invoice_ids` linked to the record. | +- **Per-invoice flags updated** on each invoice in the export: `adp_exported=True`, `adp_export_date=now`, `adp_export_count += 1`. +- Settings: `fusion_claims.vendor_code`. + +The `_validate_dates` helper warns on future invoice/delivery dates and delivery-after-invoice mismatches — these are soft warnings, only the calculation mismatches are hard blocks. + +## 10. ADP Mobility Manual / device codes + +`fusion.adp.device.code` (`models/fusion_adp_device_code.py`, 428 lines): + +Fields: `device_code` (unique, indexed, required), `device_type` (indexed), `manufacturer`, `build_type` (modular / custom_fabricated), `device_description`, `adp_price`, `max_quantity`, `sn_required`, `active`, `last_updated`. Display name auto-formats as `CODE - DESCRIPTION ($PRICE)`. + +Loaded on every install/upgrade from `data/device_codes/adp_mobility_manual.json`. Importable via JSON or CSV (`device_import_wizard`, manager-only). The model defines `_clean_text`, `_parse_price` utilities; CSV expects columns `Device Type, Manufacturer, Device Description, Device Code, Qty, Approved Price, Serial`. + +Write override (`fusion_adp_device_code.write`): when `adp_price` or `device_code` changes, propagates to all `product.template.x_fc_adp_device_code_id` linked products (`x_fc_adp_price`, `x_fc_adp_device_code` denormalized fields). + +### 10.1 Linking products + +`product.template` (`models/product_template.py`): +- `x_fc_is_adp_product` (bool) — toggle to mark ADP product. `@api.constrains` requires a `x_fc_adp_device_code_id` when True. +- `x_fc_adp_device_code_id` (Many2one) — the canonical link. +- `x_fc_adp_device_code` (Char) — denormalized for query/legacy use. +- `x_fc_adp_price` (Float) — denormalized for query/legacy use. +- `x_fc_adp_device_type`, `x_fc_adp_build_type`, `x_fc_adp_max_quantity` — all `related` fields stored. +- Also: `x_fc_security_deposit_type/amount/percent` for rentals (sibling module fusion_rental). +- `action_sync_adp_price_from_database` — admin button to re-sync the link from the JSON for a given product. + +## 11. Client profile, applications, XML parser + +### 11.1 fusion.client.profile (`models/client_profile.py`) + +One record per client (linked to `res.partner`). Stores personal info, address, contact, benefit eligibility (ODSP/OWP/ACSD/WSIB/VAC), and latest medical condition + mobility status. `_compute_claim_stats` aggregates `claim_count`, `total_adp_funded`, `total_client_portion`, `total_amount` across the partner's `sale_order_ids` filtered to ADP sale types. `_compute_ai_analysis` writes a human-readable summary into `ai_summary` and risk flags (`ai_risk_flags`) — frequency analysis (avg days between applications), multiple-replacement detection. + +### 11.2 fusion.adp.application.data (`models/adp_application_data.py`, 670 lines) + +One record per submitted ADP application (parsed from XML). Captures **all ~300 XML fields** for round-trip fidelity: +- Section 1: applicant biographical + benefits confirmation. +- Section 2 (devices/eligibility): medical condition, mobility status, previously-funded checkboxes (12 device types), currently-required device checkboxes (16 device types). +- Section 2a: Ambulation Aids / walkers + paediatric. +- Section 2b: Manual Wheelchairs. +- Section 2c: Power Bases / Scooters. +- Section 2d: Positioning / Seating. + +Each section captures every confirmation checkbox, every prescription field (seat width/depth/height, handle height, brakes, wheels, back support, custom modifications, etc.). `raw_xml` stores the original; `xml_data_json` stores a dot-notation JSON dict for export. + +### 11.3 fusion.xml.parser (`models/xml_parser.py`, 772 lines) + +`AbstractModel`. Public API: +- `parse_from_binary(binary_data, sale_order=None)` — base64 → XML → records. +- `parse_and_create(xml_content, sale_order=None)` — string → records. +- `reparse_existing(app_data_record)` — re-parse existing `raw_xml` in place. + +Flow: XML → 1:1 dot-notation JSON dict (`_xml_to_json`) → model values (`_json_to_model_vals`) → find-or-create `fusion.client.profile` keyed by health card → create `fusion.adp.application.data` → auto-link the application's authorizer to the SO by ADP registration number (`x_fc_authorizer_number` on `res.partner` → matched against `authorizer_adp_number` from the XML). + +Auto-parse: setting `fusion_claims.auto_parse_xml = True` runs the parser when `x_fc_xml_file` is uploaded to a SO. + +Bulk import: `xml_import_wizard` (manager-only) processes multiple XML files at once. + +**Profile matching priority** (`_find_or_create_profile`): +1. Health Card Number (exact). +2. First + Last name + DOB (case-insensitive, exact DOB). + +If neither matches, a new profile is created. Either way, `profile_vals` is always written — existing profiles get updated with the latest XML data, so the most recent application's personal info wins. + +**Authorizer auto-linking** (`_link_authorizer_by_adp_number`): +1. Match `res.partner` by `x_fc_authorizer_number` (exact). +2. Fallback: fuzzy name match (`name ilike "{first} {last}"` OR `"{last}, {first}"`). +3. **Learn-as-we-go enrichment**: when a partner matches by NAME AND has no `x_fc_authorizer_number`, the parser **writes the ADP number to the partner**. Future imports for the same OT then match by number directly. This is how the authorizer database gets populated over time without manual data entry. +4. Skips if SO already has `x_fc_authorizer_id` set; skips ADP numbers that are `'NA'`, `'N/A'`, or empty. + +**Date parsing** (`_pd`): tries `%Y/%m/%d`, `%Y-%m-%d`, `%Y%m%d` in that order. Returns `False` on failure. Used for every date field in the XML. + +**Section 4 fields** also captured: Vendor 1 + Vendor 2 (business name, ADP number, representative, position, location, phone+ext, sign date); Equipment Spec table (`Table2.Row1.Cell1–5`); POD (received_by, date); Note to ADP section markers (boolean per section: `section1`, `section2a–d`, `section3and4`) plus vendor replacement / custom / funding chart / letter free-text notes. + +## 12. AI integration + +### 12.1 Native AI agent (Odoo 19 `ai` module) + +`data/ai_agent_data.xml`: +- 3 `ir.actions.server` records with `use_in_ai=True` — call methods on `ai.agent` (extended via `models/ai_agent_ext.py`): + - `_fc_tool_search_clients(search_term, city_filter, condition_filter)` + - `_fc_tool_client_details(profile_id)` + - `_fc_tool_claims_stats()` +- One `ai.topic` ("Fusion Claims Client Intelligence") bundles them. +- One `ai.agent` ("Fusion Claims Intelligence") with the topic + system prompt + model `gpt-4.1` + `analytical` response style. + +`fusion.client.profile.action_open_ai_chat` opens a chat (channel of type `ai_chat`) seeded with the client's context. + +### 12.2 Legacy `fusion.client.chat.session` (`models/client_chat.py`) + +Hand-rolled OpenAI chat layer. Calls `https://api.openai.com/v1/chat/completions` directly. Settings: `fusion_claims.ai_api_key` (manager-only), `fusion_claims.ai_model` (`gpt-4o-mini` / `gpt-4o` / `gpt-4.1-mini` / `gpt-4.1`). Falls back to local DB-only responses (`_generate_local_response`) when no key is configured. Kept for back-compat; the native AI agent above is the preferred path going forward. + +## 13. Technician tasks integration + +`models/technician_task.py` (674 lines) extends `fusion.technician.task` (from `fusion_tasks`): + +- Adds `sale_order_id` + `purchase_order_id` (validates exactly one is set unless task is from cross-instance sync or is an `ltc_visit`). +- Onchange auto-fills `partner_id` + address from the SO/PO shipping/destination address. +- Hook overrides: + - `_create_vals_fill` — copy partner + address into create vals. + - `_on_create_post_actions` — chatter notice; if `mark_ready_for_delivery` context flag is set, advance SO to `ready_delivery`; if `mark_odsp_ready_for_delivery`, advance ODSP order. + - `_check_completion_requirements` — rental pickup tasks block completion until inspection done. + - `_on_complete_extra` — ODSP `ready_delivery` → `delivered`; rental pickup → write inspection back to SO and refund security deposit (if passed) or schedule activity (if flagged). + - `_on_cancel_extra` — delivery cancellation reverts SO to `x_fc_status_before_delivery` (if no other delivery tasks remain), sends cancellation email. +- Rental inspection fields: `rental_inspection_condition` (excellent/good/fair/damaged), `rental_inspection_notes`, `rental_inspection_photo_ids` (max 6, opens in FileViewer via gallery hook). +- Email overrides: routes through `sale_order._email_build` so technician emails match the rest of the project's email style; CCs the SO sales rep + office notification recipients. + +## 14. Page 11/12 signing workflow + +ADP form 13027E pages 11 & 12 require client and authorizer signatures. + +### 14.1 Page 11 (client consent) + +`fusion.page11.sign.request` (`models/page11_sign_request.py`): +- Standalone signing request: random `access_token` (UUID4), public URL `/page11/sign/`, `state` of `draft / sent / signed / expired / cancelled`. +- Signer can be client, spouse, parent, legal_guardian, power_of_attorney, public_trustee. Agent details (full address) captured when not the client. +- `consent_signed_by`: `applicant` (client signs themselves) or `agent` (anyone else). `send_page11_wizard` auto-sets this based on `signer_type`. +- Public security ACL: `base.group_public` has read-only access to `fusion.page11.sign.request` so the public sign page can resolve the token. +- `_generate_signed_pdf` uses `fusion.pdf.template` (from another module; the active template is named `adp_page_11` or `page 11`) to render a filled PDF, then writes the result to `x_fc_signed_pages_11_12` on the SO + creates an `ir.attachment`. +- `send_page11_wizard` opens the composer. Default expiry 7 days. Pre-fills signer from `partner_id.name` + `partner_id.email`. Signer relationship auto-fills from the signer_type label (spouse → "Spouse", etc.). +- Form view header buttons: Resend Email (sent/expired), Request New Signature (signed/cancelled, with confirm dialog), Cancel (draft/sent). Statusbar: draft → sent → signed. +- Cron `_cron_expire_requests` (2 AM daily) marks expired unsigned requests. + +### 14.2 Page 12 (authorizer + vendor) + +Tracked directly on the SO via two booleans + signer fields: +- `x_fc_page12_authorizer_signed` — OT signs after page 11 is received. +- `x_fc_page12_vendor_signed` + `x_fc_page12_vendor_signer_id` — designated vendor signer (`fusion_claims.designated_vendor_signer` setting) signs on the company's behalf. + +### 14.3 SA Mobility + OW Discretionary signing + +Two government-issued PDFs are filled by Python (using `pdfrw`): + +| Form | Template | Wizard | +|---|---|---| +| **SA Mobility** form (ODSP SA division) | `static/src/pdf/sa_mobility_form_template.pdf` (482 KB) | `odsp_sa_mobility_wizard` (560 lines) | +| **OW Discretionary Benefits** form | `static/src/pdf/discretionary_benefits_form_template.pdf` (1.1 MB) | `odsp_discretionary_wizard` (395 lines) | + +The SA Mobility wizard's `_build_field_mapping()` is the **field-name reference for the gov form 13007E**: + +- Vendor section: `Text 1` (with space — gov form quirk), `Text2`, `Text3`, ..., `Text7`. Salesperson: `Text8`, `Text9`. Client: `Text10`, `Text11`. +- **Member ID is 9 separate text boxes** (`Text12`-`Text20`) — one per digit, left-justified via `.ljust(9)`. +- Relationship checkboxes: `Check Box16` (self), `17` (spouse), `18` (dependent). +- Device type checkboxes: `Check Box19` (manual_wheelchair) ... `Check Box24` (other). +- Parts table (up to **6 rows**): `Text30`-`Text59` in groups of 5 (qty, description, unit_price, taxes, amount). Total at `Text60`. +- Labour table (up to **5 rows**): `Text61`-`Text80` in groups of 4 (hours, rate, taxes, amount). Total at `Text81`. +- Additional Fees (up to **4 rows**): `Text82`-`Text97` in groups of 4 (description, rate, taxes, amount). Total at `Text98`. +- Estimated totals summary: `Text99` (parts), `Text100` (labour), `Text101` (fees), `Text102` (grand total). +- Page 2 Notes/Comments area: **`Text1`** (collides with vendor `Text 1` — different field, gov-form naming quirk). + +The wizard pre-populates from the SO automatically: products with `default_code == 'LABOR'` → labour tab; everything else → parts tab. + +**`_get_template_path()`** uses raw `os.path` operations to resolve the template file, NOT Odoo's `tools.misc.file_path()`. **Brittle if the module is loaded as a zip** — consider migrating to `file_path('fusion_claims/static/src/pdf/sa_mobility_form_template.pdf')` if that issue arises. + +### 14.3.1 OW Discretionary Benefits wizard quirks + +This wizard fills a different gov form (`discretionary_benefits_form_template.pdf`, 1.1 MB) with its own set of footguns: + +- **Uses `PyPDF2`, not `pdfrw`** — because the gov PDF is AES-encrypted (no password, just "protected mode"). pdfrw cannot decrypt; PyPDF2 handles it via `reader.decrypt('')`. If pdfrw is the only PDF library available, the wizard will fail. Both are optional Python deps. +- Preserves `/AcroForm` from the original document and sets `/NeedAppearances` = True so the filled form renders correctly in Acrobat (without this, the values are stored but not visible). +- Splits text and checkbox fields into separate dicts. Text fields use PyPDF2's bulk `update_page_form_field_values`; **checkboxes are updated directly by mutating each annotation's `/V` and `/AS` to `/1` (checked) or `/Off` (unchecked)** via `NameObject`. PyPDF2 doesn't have a clean API for this. +- **The gov form has misleading field names** — they don't match physical layout. Hard-earned mapping: + | Field name (PDF) | Actually populates | + |---|---| + | `txt_First[0]` | Client Name | + | `txt_CITY[1]` | **Member ID** (not city — note the `[1]` index) | + | `txt_add[0]` | Address | + | `txt_CITY[0]` | City (the `[0]` index — different from `[1]`) | + | `txt_email[0]` | **Phone** (not email) | + | `txt_bphone[0]` | Alternate Phone | + | `txt_emp_phone[0]` | **Email** (not employer phone) | + | `txt_clientnumber[0]` | **Date** (not client number) | + | `CheckBox15[0]` | Medical Equipment | + | `CheckBox11[0]` | Dentures | + | `CheckBox11[1]` | Vision Care | + | `CheckBox13[0]` | Other | + | `TextField1[0]` | Description/details | + + Don't normalize or rename when building the mapping — write to the names exactly as ODSP shipped them. +- 4 item types: `medical_equipment`, `vision_care`, `dentures`, `other`. + +ODSP signing positions (signature/initial fields) are managed via Configuration > PDF Templates with a drag-and-drop visual editor — managed in `fusion.pdf.template` records (category=ODSP), not as static templates in `data/pdf_template_data.xml` (which is now an empty placeholder with a "templates retired" note). + +`static/src/pdf/sa_mobility_page2_sample.pdf` (241 KB) is a reference sample showing what page 2 should look like when filled. + +### 14.4 ODSP submission paths (`odsp_submit_to_odsp_wizard`) + +Three actions on the wizard, all of which (a) render the standard `sale.action_report_saleorder` PDF, (b) attach `x_fc_odsp_authorizer_letter`, and (c) advance status `quotation`/`documents_ready` → `submitted_to_odsp`: + +| Action | What it does | +|---|---| +| `action_send_email` | Emails the ODSP office (`x_fc_odsp_office_id`) with both attachments. Uses `_send_odsp_submission_email` with `email_body_notes`. | +| `action_send_fax` | Opens `fusion_faxes.send.fax.wizard` (soft-depended on) with attachments pre-loaded and the office's `x_ff_fax_number` if available. | +| `action_send_fax_and_email` | Sends email first, then chains into the fax wizard via `display_notification.next`. | + +The wizard can override `x_fc_odsp_office_id` per-submission and syncs the choice back to the SO. + +### 14.4.1 ODSP submission document bundling + +Two methods on `sale.order` build the email payload sent to the funder: + +| Method | Used by | Attaches | +|---|---|---| +| `_sa_mobility_submit_documents` | SA Mobility flow | (1) signed SA form (`x_fc_sa_signed_form` OR `x_fc_sa_physical_signed_copy`), (2) internal POD PDF via `_get_sa_pod_pdf`, (3) latest **posted** invoice PDF (Odoo's `account.account_invoices` report) | +| `_odsp_std_submit_documents` | ODSP Standard flow | (1) `x_fc_odsp_approval_document`, (2) internal POD PDF, (3) **auto-confirms SO and creates invoice** if no posted one exists, then attaches invoice PDF | + +Both call the corresponding `_send_*_email` method (SA Mobility → `_send_sa_mobility_completion_email`; Standard → `_send_odsp_submission_email`). Both post a green chatter card listing the attachment names. + +### 14.4.2 Ontario Works invoice flow + +`_ow_payment_create_invoice` (`action_odsp_payment_received` for OW orders): + +1. Auto-confirms the SO if `state != 'sale'`. +2. Calls standard `self._create_invoices()`. +3. Writes `x_fc_source_sale_order_id` on the invoice. +4. Advances ODSP status to `payment_received` with chatter note "Ontario Works payment confirmed. Invoice {name} created." +5. Returns an action that opens the new invoice form. + +This is the ONE case where payment comes BEFORE delivery — the OW office pays the vendor, the vendor delivers later (and the auto-close cron uses `delivered` not `payment_received` as the timer trigger — see §18). + +### 14.4.3 ODSP on_hold / resume (2026-04 fix) + +Mirrors the MOD on_hold pattern. `x_fc_odsp_previous_status_before_hold` saves the current status; `action_odsp_resume` restores it (falls back to `quotation` only for legacy records held before the fix shipped). Blocks hold from `on_hold`/`case_closed`/`cancelled`. Calls `_odsp_advance_status` which routes to whichever of `x_fc_sa_status`/`x_fc_odsp_std_status`/`x_fc_ow_status` is active. + +### 14.4.4 SA Mobility signature overlay — TWO mechanisms + +| Method | Used when | Mechanism | +|---|---|---| +| `action_sign_sa_mobility_form` | Client signs the SA Mobility form directly (Page 2 client consent) | **Hard-coded coordinates**: writes printed name at `(180, h-180)` and `(72, h-560)`, date at `(350, h-560)`, signature image at `(72, h-540, 200×50px)`. Uses `reportlab.pdfgen.canvas` + `odoo.tools.pdf.PdfFileReader/Writer`. **Brittle** — if the gov PDF layout changes, the coordinates must be re-measured. | +| `_apply_pod_signature_to_approval_form` | POD signature collected (auto-fired by `write` override when `x_fc_pod_signature` is set) | **PDFTemplateFiller** from `fusion_authorizer_portal` — reads field positions from the active `fusion.pdf.template` (category=`odsp`), uses per-case `x_fc_sa_signature_page`. Configurable via drag-and-drop visual editor, not code. Bypass via `skip_pod_signature_hook=True` context. | + +The PDFTemplateFiller approach is the preferred path going forward — it survives gov form revisions because positions live in the database, not in Python code. + +### 14.5 ODSP signature-page setup (`odsp_ready_delivery_wizard`) + +When transitioning SA Mobility / OW orders to `ready_delivery`, this wizard: + +1. Loads field positions from the active `fusion.pdf.template` (category=`odsp`). +2. Renders a **preview image** of the chosen `signature_page` using `pdf2image.convert_from_bytes` + PIL `ImageDraw`, with colored markers overlaid at each field position: + - **blue** for text fields (sample text: "John Smith") + - **purple** for date fields (sample: "2026-02-17") + - **red rectangles** for signature fields +3. Writes `x_fc_sa_signature_page` to the SO. +4. **Returns an action that opens the technician task form** pre-filled with `default_task_type='delivery'`, `default_pod_required=True`, and `mark_odsp_ready_for_delivery=True` context — the task model's `_on_create_post_actions` hook then advances ODSP status to `ready_delivery`. + +`action_preview_full` opens the full PDF via the custom `fusion_claims.preview_document` client action tag. + +## 15. Field-mapping system (legacy Studio support) + +This module ships with a **field mapping layer** that lets it run against existing Studio-created fields in production databases. The mapping uses `ir.config_parameter` keys (`fusion_claims.field_*`) → field name. Defaults live in `data/ir_config_parameter_data.xml`. + +Getters that respect the mapping: +- `sale.order._get_sale_type / _get_client_type / _get_authorizer / _get_claim_number / _get_client_ref_1 / _get_client_ref_2 / _get_adp_delivery_date` +- `account.move._get_invoice_type / _get_client_type / _get_authorizer / _get_claim_number / _get_client_ref_1 / _get_client_ref_2 / _get_adp_delivery_date` +- `sale.order.line._get_serial_number / _get_device_placement` +- `account.move.line._get_serial_number / _get_device_placement` +- `product.product.get_adp_device_code / get_adp_price` + +**Always go through the getters when reading these fields.** Direct attribute access breaks legacy databases where the canonical field name is `x_studio_*` instead of `x_fc_*`. + +Auto-detection: +- `fusion_claims.config.action_detect_existing_fields` (`models/fusion_central_config.py`) scans for custom `x_*` fields on the canonical models, fuzzy-matches against keyword lists, and writes the discovered names into the matching `ir.config_parameter` keys. Surfaces unmapped fields for review. +- `field_mapping_config_wizard` is the form-based config UI. + +The full list of mapping keys is in `models/res_config_settings.py` (`fc_field_*`) — ~25 ICP keys covering SO, SOL, invoice, invoice line, and product fields. + +## 16. Email system + +Every workflow transition has a corresponding `_send_*_email` method on `sale.order`, all routed through `fusion.email.builder.mixin._email_build(...)`: + +### 16.1 ADP emails (~25) + +Each method is on `sale.order` and routed through `_email_build`. Key ones: + +| Method | Trigger | Recipients | Attachments | +|---|---|---|---| +| `_send_submission_email` | status → `submitted`/`resubmitted` | Client (TO), Authorizer + Sales Rep + Office (CC) | Final Application PDF + XML File | +| `_send_assessment_scheduled_email` | status → `assessment_scheduled` | Client (TO), Authorizer + Sales Rep (CC) | none | +| `_send_application_received_email` | status → `application_received` | Authorizer (TO), Sales Rep (CC) | none | +| `_send_application_reminder_email` | cron, X=4 days after `assessment_end_date` if still `waiting_for_application`/`assessment_completed` AND `x_fc_application_reminder_sent=False` | Authorizer (TO), Sales Rep + Office (CC). Sets `x_fc_application_reminder_sent=True` after send. | none | +| `_send_application_reminder_2_email` | cron, X+Y=8 days after `assessment_end_date` if first reminder sent AND `x_fc_application_reminder_2_sent=False`. Email mentions 90-day assessment validity. | Authorizer (TO), Sales Rep + Office (CC) | none | +| `_send_accepted_email` | status → `accepted` | Client + Authorizer (TO) | none | +| `_send_approval_email` | status → `approved`/`approved_deduction` | Client (TO), Authorizer + Sales Rep + Office (CC). Differentiates `approved` (standard message) vs `approved_deduction` (extra note about deduction). | **Generates** `action_report_approved_items` PDF via `_generate_approved_items_pdf`. Also embeds `_build_approved_items_html` table inline. | +| `_send_denial_email` | status → `denied` | Client (TO), Authorizer + Sales Rep + Office (CC). Urgent style. | none | +| `_send_rejection_email` | status → `rejected` | Client (TO), Authorizer + Sales Rep + Office (CC). 2026-04 fix: client previously excluded. Includes `rejection_reason` label (5 enum values) + free-text `x_fc_rejection_reason_other`. | none | +| `_send_correction_needed_email(reason)` | status → `needs_correction` (typically with wizard's reason text) | Client + Authorizer + Sales Rep + Office | none | +| `_send_billed_summary_email` | status → `billed` | Authorizer + Sales Rep (TO). Green success card with totals. | none | +| `_send_case_closed_email` | status → `case_closed` | (uses `_get_email_recipients` standard) | none | +| `_send_ready_for_delivery_email` | called from `ready_for_delivery_wizard` after task creation | Client + Authorizer + technicians (CC) | none | +| `_send_on_hold_email`, `_send_withdrawal_email`, `_send_expired_email`, `_send_cancelled_email` | corresponding status transitions | varies | none | + +`_build_approved_items_html(for_pdf=False)` builds an HTML table with columns S/N | ADP Code | Device Type | Product | Qty | ADP Portion | Client Portion | (Deduction — only when any line has one). Total row at bottom. Different font stack for PDF (`Arial,Helvetica,sans-serif`) vs email (system font). Truncates product names > 40 chars in email mode. + +`_generate_approved_items_pdf` renders `fusion_claims.action_report_approved_items` QWeb report → attaches with filename `{first}_{last}_Approved_Items.pdf`. + +### 16.1.1 Quotation/SO send override + +`sale.order.action_quotation_send` is overridden to auto-select the ADP email template: + +- Draft state → `email_template_adp_quotation` +- Sale/done state → `email_template_adp_sales_order` +- Layout: `mail.mail_notification_layout` +- Context flags: `mark_so_as_sent=True`, `force_email=True` + +Falls back to the standard behaviour for non-ADP sales. Mirror override exists on `account.move.action_invoice_sent` for ADP invoices (selects `email_template_adp_invoice`). + +### 16.2 MOD emails (~14) +`_send_mod_assessment_scheduled_email`, `_send_mod_assessment_completed_email`, `_send_mod_quote_submitted_email`, `_send_mod_vod_request_email`, `_send_mod_handoff_email`, `_send_mod_funding_approved_email`, `_send_mod_funding_denied_email`, `_send_mod_contract_received_email`, `_send_mod_invoice_submitted_email`, `_send_mod_initial_payment_email`, `_send_mod_project_complete_email`, `_send_mod_pod_submitted_email`, `_send_mod_final_payment_email`, `_send_mod_case_closed_email`, `_send_mod_cancelled_email`, `_send_mod_followup_email`. Build helper: `_mod_email_build`. + +### 16.3 ODSP emails +`_send_sa_mobility_email`, `_send_sa_mobility_completion_email`, `_send_odsp_submission_email`. Build helper: `_odsp_email_build`. + +### 16.4 Generic funder emails (WSIB / Insurance / MDC / Hardship) + +Five client-facing methods + five authorizer-facing variants, all routed through the unified `_send_funder_email(recipient, milestone, email_type, title, summary, attachment_ids, attachments_note)`: + +| Method | Used at | +|---|---| +| `_send_funder_package_ready_*` | Quotation + application package prepared | +| `_send_funder_approval_*` | Funding approved (attaches the funder-specific approval letter via `_get_funder_approval_attachments`) | +| `_send_funder_delivered_*` | Product delivered | +| `_send_funder_case_closed_*` (client only) | Case closed | +| `_send_funder_denial_*` | Funding denied | + +**Per-funder trigger maps** (class constants on `sale.order`) connect statuses to methods. The write override calls `_fire_funder_emails(trigger_map, new_status)` for each funder workflow. + +| Trigger map | Status keys → methods | +|---|---| +| `_WSIB_EMAIL_TRIGGERS` | `documents_ready` → package ready (both), `pre_approved` → approval (both), `delivered` → delivery (both), `case_closed` → client only, `denied` → denial (both) | +| `_INSURANCE_EMAIL_TRIGGERS` | `documents_ready` → package ready (client only), `approval_received` / `pre_auth_approved` → approval (both), `delivered` → delivery (both), `case_closed` → client only, `denied` → denial (both) | +| `_MDC_EMAIL_TRIGGERS` | `documents_ready` → package ready (both), `po_received` → approval (both), `delivered` → delivery (both), `case_closed` → client only, `denied` → denial (both) | +| `_HARDSHIP_EMAIL_TRIGGERS` | `application_package_ready` → package ready (both), `approval_received` → approval (both), `delivered` → delivery (both), `case_closed` → client only, `denied` / `eligibility_failed` → denial (both) | + +`_FUNDER_LABELS` maps the sale type to a human label (`'wsib': 'WSIB'`, `'insurance': 'Insurance'`, `'muscular_dystrophy': 'Muscular Dystrophy Canada'`, `'hardship': 'Hardship Funding'`). Used for subject lines and email body. + +`_build_funder_case_rows()` builds the email's "Case Details" section dynamically per funder — WSIB adds claim#, adjudicator, approval date; Insurance adds company, policy#, claim#, pre-auth expiry; MDC adds Client ID, PO number/date; Hardship adds funder partner, approval date. + +### 16.5 Recipients & CC + +`_get_email_recipients(include_client, include_authorizer, include_sales_rep)` collects the to/cc list. The CC list always includes contacts in `company.x_fc_office_notification_ids`. Master toggle: `fusion_claims.enable_email_notifications` (`_is_email_notifications_enabled`). + +MOD has its own recipient helper: `_get_mod_email_recipients(include_client, include_authorizer, include_mod_contact, include_sales_rep)` — adds `include_mod_contact` (uses `x_fc_mod_contact_email`). Returns a dict with `to`, `cc`, `office_cc` plus the partner records (`authorizer`, `sales_rep`, `client`). Authorizer becomes TO if no client email, else CC. + +**Per-MOD-method recipient rules** (some include CC of the MOD case worker, some don't): +| Method | Client | Authorizer | MOD contact | Notes | +|---|---|---|---|---| +| `assessment_scheduled` | ✓ | ✓ | — | | +| `assessment_completed` | ✓ | ✓ | — | 2026-04 fix: authorizer was previously excluded | +| `quote_submitted` | ✓ | ✓ | ✓ | | +| `funding_approved` | ✓ | ✓ | — | with amounts section | +| `funding_denied` | ✓ | ✓ | — | urgent style | +| `contract_received` | ✓ | — | — | client only | +| `invoice_submitted` | — | — | ✓ | MOD contact only | +| `initial_payment` | ✓ | — | — | client only | +| `project_complete` | ✓ | ✓ | — | | +| `pod_submitted` | ✓ | ✓ | ✓ | 2026-04 fix: client was previously excluded | + +### 16.4.1 Authorizer email policy (2026-04 audit) + +A consistent rule across the module: the authorizer (OT) is only CC'd / notified for states that **require their action** or are **definitive outcomes**. They are deliberately excluded from passive intermediate states to reduce noise. + +| State | Authorizer in email? | Why | +|---|---|---| +| `assessment_scheduled` | ✓ | They're conducting it | +| `assessment_completed` | ✓ | Triggers next-step actions for them | +| `application_received` | ✓ | Confirms handoff to office | +| `submitted` / `resubmitted` | ✓ | Material progress | +| `accepted` | ✗ | Passive intermediate state, no OT action needed | +| `approved` / `approved_deduction` | ✓ | Definitive funding outcome | +| `denied` / `rejected` / `needs_correction` | ✓ | OT may need to act | +| `on_hold` | ✓ | OT should know case paused | +| `ready_for_delivery` | ✗ | Operational scheduling, not delivery confirmation | +| `case_closed` | ✓ | Definitive outcome — they get the "delivered" notification at this point | +| `cancelled` / `expired` / `withdrawn` | ✓ | Closing the case | + +Hold reminders + final warning have a slightly different rule (see §4.11) — monthly client reminder excludes authorizer, but the final warning includes them. + +### 16.4.2 `_adp_send_stage_email` helper + +The shared sender for ADP stage emails (`_send_assessment_scheduled_email`, `_send_assessment_completed_email`, `_send_application_received_email`, `_send_accepted_email`, `_send_cancelled_email`, `_send_expired_email`). Signature: + +```python +_adp_send_stage_email(title, subject, summary, note, + note_color='#4f8cff', email_type='info', + include_authorizer=True) +``` + +Defaults to client + authorizer + sales rep. `include_authorizer=False` is the one knob — used by `_send_accepted_email` and explained in the docstring with reference to the 2026-04 audit rule. + +### 16.4.3 `_send_withdrawal_email` three-intent dispatch + +Takes `reason` (free text) and `intent` arguments: + +| `intent` | Title / subject | Note | Color | +|---|---|---|---| +| `'cancel'` | "Application Withdrawn & Cancelled" | "Permanently withdrawn and cancelled. SO and invoices cancelled." | `#dc3545` urgent | +| `'resubmit'` | "Application Withdrawn for Correction" | "Will be resubmitted. Back to Ready for Submission." | `#d69e2e` attention | +| (None) | "Application Withdrawn" | "Withdrawn from ADP." | `#d69e2e` attention | + +`action_adp_withdraw` calls with `intent='cancel'` by default; the `status_change_reason_wizard` exposes both intents. + +### 16.5.1 Three footer voices + +The module branches the footer line of every transactional email by funder type, via two thin wrappers around `_email_build`: + +| Helper | Footer reads | +|---|---| +| `_email_build` (default) | "This is an automated notification from the **ADP Claims Management System**." | +| `_mod_email_build` | "This is an automated notification from the **Accessibility Case Management System**." | +| `_odsp_email_build` | "This is an automated notification from the **ODSP Case Management System**." | + +Both wrappers do a literal string replace on the result of `_email_build`. If you change the master footer text, update the two replace calls or the branching will silently break. + +### 16.6 mail.template records (`data/mail_template_data.xml`) + +Three templates, all with `auto_delete=True` and a report attached: +- `email_template_adp_quotation` — sale.order, landscape report. +- `email_template_adp_sales_order` — confirmation, landscape report. +- `email_template_adp_invoice` — account.move, landscape report. `account.move.action_invoice_sent` is overridden to pre-select this template for ADP invoices (don't remove this without preserving the routing). + +## 17. SMS via Twilio + +Settings: `fusion_claims.twilio_enabled`, `fusion_claims.twilio_account_sid` (manager-only), `fusion_claims.twilio_auth_token` (manager-only), `fusion_claims.twilio_phone_number`. + +Helpers on `sale.order`: +- `_twilio_send_sms(to_number, message)` — low-level POST to `https://api.twilio.com/2010-04-01/Accounts/{sid}/Messages.json` with basic auth, 10-second timeout. Returns True on HTTP 200/201, False otherwise. +- `_send_mod_sms(trigger)` — picks a message template from a hard-coded dict keyed by trigger string. Reads `partner.mobile` then falls back to `partner.phone`. Silently bails if Twilio isn't enabled or the partner has no phone. + +**4 MOD SMS message templates** (`models/sale_order.py:9823-9840`): + +| Trigger | Sample message | +|---|---| +| `assessment_scheduled` | "Hi {name}, your accessibility assessment with **Westin Healthcare** has been scheduled. We will confirm the exact date and time shortly. For questions, call {company_phone}." | +| `funding_approved` | "Hi {name}, great news! Your March of Dimes funding has been approved. Our team will be in touch with next steps. Questions? Call {company_phone}." | +| `initial_payment_received` | "Hi {name}, we have received the initial payment for your project. Work is in progress. We will keep you updated. Call {company_phone} for info." | +| `project_complete` | "Hi {name}, your accessibility modification project is now complete! If you have any questions or concerns, call us at {company_phone}." | + +> ⚠ **Multi-tenant gotcha**: the `assessment_scheduled` template hard-codes the string `"Westin Healthcare"` — it's NOT pulled from `self.company_id.name`. If this module is deployed at a non-Westin customer, that message reads wrong. Fix or parameterize before going multi-tenant. + +ODSP also has `_send_sa_mobility_email` (request_type='batteries'|'repair', device_description, attachment_ids, email_body_notes) and `_send_odsp_submission_email`. SA Mobility email defaults to `samobility@ontario.ca` (`fusion_claims.sa_mobility_email`). + +## 18. Cron jobs (13 total) + +All defined in `data/ir_cron_data.xml` and `data/ir_actions_server_data.xml` (currently empty placeholder). + +| Cron | Time | What it does | +|---|---|---| +| Renew Delivery Reminders | daily | `sale.order._cron_renew_delivery_reminders` — finds `approved`/`approved_deduction` orders with overdue `mail_activity_type_adp_delivery` activities, reschedules to next posting Tuesday | +| Renew Billing Reminders | daily | `account.move._cron_renew_billing_reminders` — for `waiting`-status ADP invoices with overdue billing activities | +| Renew Correction Reminders | daily | `account.move._cron_renew_correction_reminders` — for `need_correction`-status ADP invoices | +| Auto-Close Billed Cases | daily | `billed` → `case_closed` 30 days after `x_fc_billing_date`. Posts chatter with `days_since_billed` | +| Auto-Close ODSP Paid Cases | daily | SA Mobility / ODSP Standard: 7 days after `payment_received` → `case_closed`. **Ontario Works: 7 days after `delivered`** (payment comes BEFORE delivery for OW) | +| First Application Reminder | 9 AM daily | `_cron_send_application_reminders` — `assessment_end_date == today - X` (default 4), status in `waiting_for_application`/`assessment_completed`, `x_fc_application_reminder_sent=False`. Sends to authorizer + sales rep + office | +| Second Application Reminder | 9 AM daily | `_cron_send_application_reminders_2` — `assessment_end_date == today - (X+Y)`, first reminder sent, second not yet. Mentions 90-day assessment validity | +| Acceptance Reminders | 9 AM daily | `_cron_send_acceptance_reminders` — see §18.1 below | +| MOD Follow-up Scheduler | 8 AM daily | Bi-weekly MOD activity creation (capped). See §5.3 | +| MOD Follow-up Escalation | 10 AM daily | Auto-email after activity 3 days overdue. See §5.3 | +| MOD Handoff Follow-up | 9 AM daily | Office call activity for client/authorizer MOD paths. See §5.3 | +| Expire Page 11 Signing Requests | 2 AM daily | Mark unsigned past-expiry requests `expired` | +| ADP Expire Approved Applications | 3 AM daily | `approved` → `expired` 12 months later | +| ADP Hold Expiry Reminders | 9:30 AM daily | Monthly client reminder + 30-day final warning | + +### 18.1 `_cron_send_acceptance_reminders` mechanics + +Sends reminders for orders still `submitted` next business day after submission. Three layers of anti-spam protection (2026-04 update): + +| Layer | Setting | Effect | +|---|---|---| +| 14-day backlog guard | `fusion_claims.acceptance_reminder_max_age_days` (default 14) | Only reminds cases submitted **within the last 14 days** — first cron run after deploy doesn't blast every old stuck `submitted` case | +| 2-day lower bound | hard-coded | `cutoff_date = today - 2 days` — skips weekend-only delays | +| Per-run cap | `fusion_claims.acceptance_reminder_max_per_cron_run` (default 10) | Spreads large backlogs across multiple runs | +| One-shot flag | `x_fc_acceptance_reminder_sent` | Each order gets at most ONE reminder per submission cycle. Reset to False on resubmission (see §4.9) | + +**FIRST/SECOND tier escalation**: +- `days_since_submission ≤ 3` → "FIRST" reminder → **office only** +- `days_since_submission > 3` → "SECOND" reminder → **office + sales rep** + +Subject differs: "Pending Review: Acceptance Status - {SO}" (first) vs "Follow-up Review: Acceptance Status - {SO}" (second). + +`mail_activity_type_*` records (`data/mail_activity_type_data.xml`): `adp_delivery` (sale.order), `adp_billing` (account.move), `adp_correction` (account.move, danger decoration), `mod_followup` (sale.order, 14-day delay). + +## 18.2 MOD VOD request + handoff email mechanics + +**`_send_mod_vod_request_email`** (fired when `submitted_by` first set to `internal`, also manual button): +- TO: authorizer; CC: sales rep +- Subject: "Verification of Disability needed - {client} - {SO}" +- Attaches the **blank VOD form** stored on `res.company.x_fc_mod_vod_form` (via `_mod_company_attachment` helper) +- Stamps `x_fc_mod_vod_requested_date = today` + +**`_send_mod_handoff_email`** (fired by `action_mod_handoff_to_client`, only when `submitted_by ∈ ('client', 'authorizer')`): + +| `submitted_by` | TO | CC | Subject | Tone | +|---|---|---|---|---| +| `client` | client | authorizer + sales rep | "Your March of Dimes Application Package - {SO}" | Direct to client, numbered next-steps list, "Call us once you've submitted" | +| `authorizer` | authorizer | client + sales rep | "MOD Application Package for {client} - {SO}" | Professional, "Please confirm with us once submitted" | + +Attaches: `x_fc_mod_proposal_doc` + `x_fc_mod_drawing` + blank MOD Application Form from `res.company.x_fc_mod_application_form`. + +**`_mod_company_attachment(field_name, filename_field, default_name)`** — reusable helper that pulls a Binary from `res.company` and creates an `ir.attachment` for outbound emails. Returns `[attachment_id]` or `[]` if the company field is blank. + +## 19. PDF reports (15+ templates) + +All declared in `report/report_actions.xml` and bound to their models. Custom paperformat `paperformat_a4_landscape` (A4 Landscape, margins 20/20/7/7, header spacing 20, dpi 90). + +| Report | Model | Type | XML file | +|---|---|---|---| +| Quotation / Order (Portrait - ADP) | sale.order | Portrait | sale_report_portrait.xml | +| Quotation / Order (Landscape - ADP) | sale.order | Landscape | sale_report_landscape.xml | +| Invoice (Portrait) | account.move | Portrait | invoice_report_portrait.xml | +| Invoice (Landscape - ADP) | account.move | Landscape (no menu binding) | invoice_report_landscape.xml | +| ADP Proof of Delivery | sale.order | | report_proof_of_delivery.xml | +| Proof of Delivery (Standard) | sale.order | | report_proof_of_delivery_standard.xml | +| Proof of Pickup | sale.order | | report_proof_of_pickup.xml | +| Approved Items | sale.order | | report_approved_items.xml | +| Grab Bar Waiver | sale.order | | report_grab_bar_waiver.xml | +| Accessibility Contract | sale.order | | report_accessibility_contract.xml | +| MOD Quotation | sale.order | | report_mod_quotation.xml | +| MOD Invoice | account.move | | report_mod_invoice.xml | + +### 19.1 Shared report-template snippets (`report/report_templates.xml`) + +The following QWeb templates are `t-call`-able from any report: + +| Template ID | Purpose | +|---|---| +| `report_header_fusion_claims` | Company logo + name + `x_fc_store_address_1/2` + `x_fc_company_tagline` (3-col / 9-col Bootstrap row) | +| `report_address_boxes` | Bordered billing + delivery address columns (fallback to billing when no shipping address) | +| `report_serial_numbers` | Polymorphic — handles BOTH `sale.order` and `account.move`. Extracts `x_fc_serial_number` from order_line / invoice_line_ids and renders as a bulleted list inside a bordered box. | +| `report_payment_terms` | Outputs `company.x_fc_payment_terms_html`, preceded by "Payment Communication: {doc.name}" | +| `report_refund_policy_page` | Gated by `company.x_fc_include_refund_page` — separate page (`page-break-before: always`) with header + refund policy HTML | +| `report_footer_fusion_claims` | Phone & Fax + email + HST VAT + website (single centered row) | +| `report_styles_fusion_claims` | Inline `
@@ -193,12 +233,13 @@ - -
- Quantities - Quantités -
- + +
@@ -244,9 +285,225 @@
- + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ Fischerscope XRF Thickness Report + Rapport d'épaisseur Fischerscope XRF +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Equipment + Équipement + + + + Calibration Std. + Étalon +
+ Product + Produit + + Operator + Opérateur +
+ Application + Application + + Measured + Mesuré le + + + +
+ Directory + Répertoire + + Measuring Time + Durée de mesure + + + +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#NiP (mils)Ni %P %
+ Mean + Moyenne +
+ Std Dev + Écart-type +
CoV (%)
+ Range + Étendue +
N
+
+ + +
+ Source file: + Fichier source : + + (attached to cert as evidence) +
+
+
+ + - +
Certified By: diff --git a/fusion_plating/fusion_plating_reports/report/report_fp_packing_slip.xml b/fusion_plating/fusion_plating_reports/report/report_fp_packing_slip.xml index 2de1679f..7256b494 100644 --- a/fusion_plating/fusion_plating_reports/report/report_fp_packing_slip.xml +++ b/fusion_plating/fusion_plating_reports/report/report_fp_packing_slip.xml @@ -3,11 +3,150 @@ Copyright 2026 Nexa Systems Inc. License OPL-1 (Odoo Proprietary License v1.0) Fusion Plating — Packing Slip / Shipping Confirmation (Portrait + Landscape). - Binds to stock.picking. Shows parts, quantities, lot/serial tracking, - and a receiver sign-off. + Binds to stock.picking. Bill-To / Ship-To boxes, bilingual column + headers, Received-By signature block and a QR code for scan-to-sign. --> + + + + + + + + + + + + + + @@ -16,6 +155,25 @@ + + + + + + + + + + + + +
@@ -24,92 +182,67 @@ - - - - - - - - + +
FROMSHIP TO
- - - - -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - +
SHIP DATESOURCEOPERATIONCARRIER
- - + + + + + + + + + - -
- - + +
- - - - - + + + - - - - - - - - - - + + + + +
PART NUMBERDESCRIPTIONQTYUOMLOT / SERIAL + Ship ViaMode d'expédition + + Shipping DateDate d'expédition + + Tracking #N° de suivi +
- - - - - - - - -
-
-
-
+ + + + +
+ + + + + + + + + + + +
@@ -118,21 +251,8 @@
- -
-
-
-
-
Shipper (Signature / Date)
-
-
-
-
-
-
Receiver (Signature / Date)
-
-
-
+ +
@@ -149,6 +269,18 @@ + + + + + + + + + + + +
@@ -157,104 +289,67 @@ - - - - - - - - + +
FROMSHIP TO
- -
-
-
+
+ + + + -
-
+
+ + + +
- - + +
- - - - - + + + - - - - + - +
SHIP DATESOURCEOPERATIONCARRIERTRACKING REF + Ship ViaMode d'expédition + + Shipping DateDate d'expédition + + Tracking #N° de suivi +
- - + + + - - - - - - - - +
- - - - - - - - - - - - - - - - - - - - - - - - - - -
PART NUMBERDESCRIPTIONORDEREDDONEUOMLOT / SERIALNOTES
- - - - - - - - - - -
-
-
-
-
+ + + + + + + + + + + @@ -264,21 +359,8 @@
- -
-
-
-
-
Shipper (Signature / Date)
-
-
-
-
-
-
Receiver (Signature / Date)
-
-
-
+ +
diff --git a/fusion_plating/fusion_plating_reports/report/report_fp_sale.xml b/fusion_plating/fusion_plating_reports/report/report_fp_sale.xml index 101b2e80..cb2be321 100644 --- a/fusion_plating/fusion_plating_reports/report/report_fp_sale.xml +++ b/fusion_plating/fusion_plating_reports/report/report_fp_sale.xml @@ -11,28 +11,99 @@ + + +