docs(fusion_portal): spec for assessment Visit + funding-routed sale orders
Brainstormed design: bundle a home visit's assessments (ADP + accessibility), measurement-first with client/funding deferred, add-as-you-go workspace, per-item funding selector (fixes the March-of-Dimes routing gap), and on completion group items into ONE draft sale order per funding workflow (ADP / MOD / ODSP / Hardship / private) reusing the existing pipelines. Adds ADP multi-device + combination rules, a new mobility-scooter type, and a power-mobility home-accessibility rule that feeds the accessibility upsell. v1 keeps manual quotation (no auto-pricing); MOD $15k cap is a reminder only. Phased 1-3; risks + file map included. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
# Assessment Visit — bundled, funding-routed assessments
|
||||
|
||||
**Date:** 2026-06-02
|
||||
**Module:** `fusion_portal` (depends on `fusion_claims`, `fusion_tasks`); live on `odoo-westin` (DB `westin-v19`)
|
||||
**Status:** Draft for review
|
||||
**Author:** Brainstormed with Gurpreet (Fusion / Westin Healthcare)
|
||||
|
||||
---
|
||||
|
||||
## 1. Problem & goals
|
||||
|
||||
A sales rep visits a client's home **with an occupational therapist (OT) and the client present for only 30–45 minutes**, and the OT's time is the scarcest resource. In that window the team often does more than one assessment — a wheelchair (ADP) plus, opportunistically, accessibility products the rep spots (a ramp at the front steps, a stair lift inside, a tub cutout, a patient lift for transfers). Today each assessment is a **separate, standalone web form** that re-collects the client's details and creates its own sale order, and the front-end forms give the rep **no way to mark a case's funding source** — so March-of-Dimes work silently defaults to private pay and never reaches the MOD pipeline.
|
||||
|
||||
**Goals**
|
||||
|
||||
1. **One visit, many assessments, entered once.** Bundle every assessment from one home visit; capture the client + funding details a single time.
|
||||
2. **Measurement-first.** Capture measurements while the OT is present; defer client/health-card data to after they leave; let the OT sign the ADP application on the spot.
|
||||
3. **Add as you go.** The rep adds an assessment/product the instant they spot it — repeatable, with a location tag (Front / Back / Inside).
|
||||
4. **Route by funding workflow.** On completion the visit emits **one sale order per funding workflow** (ADP, March of Dimes, ODSP, WSIB, private, …) — never one combined SO, and never a separate SO per item within the same funding.
|
||||
5. **Let the rep set funding at assessment time** (the real MOD "tracking" gap).
|
||||
6. **ADP multi-device** with valid-combination rules, including a new **mobility scooter** type and a **home-accessibility hard rule** for power mobility that feeds the accessibility upsell.
|
||||
|
||||
**Non-goals (v1):** voice/dictated entry; rebuilding the measurement math; a new MOD/ADP claim model (the pipelines already exist — we reuse them).
|
||||
|
||||
---
|
||||
|
||||
## 2. Current state (verified against source)
|
||||
|
||||
- **Two assessment models, already two separate SO lineages.** `fusion.assessment` (ADP: rollator/wheelchair/powerchair) and `fusion.accessibility.assessment` (the 7 lift/mod types) each have their own `_create_draft_sale_order` (`assessment.py:587`, `accessibility_assessment.py:751`), their own `x_fc_sale_type`, and their own state machine — ADP's 24-state `x_fc_adp_application_status` vs MOD's 16-state `x_fc_mod_status`. Each guards against a second SO (`accessibility_assessment.py:503-511`). SO back-links are **scalar** Many2one: `assessment_id`, `accessibility_assessment_id` (`fusion_portal/models/sale_order.py:37,48`).
|
||||
- **SOs are born with no order lines.** Specs become a **chatter HTML note** (`_format_assessment_html_table`, `accessibility_assessment.py:815`); a human prices the draft afterward. **No per-type product mapping exists.**
|
||||
- **Funding is modelled but not on the measurement forms.** `x_fc_funding_source` (required, default `direct_private`) on the accessibility model — values `march_of_dimes`, `odsp`, `wsib`, `insurance`, `direct_private`, `other` (`accessibility_assessment.py:71-87`) — is present on the public booking form but **absent from all 7 measurement forms**, so they default to private. Canonical billing type `sale.order.x_fc_sale_type` (`fusion_claims/models/sale_order.py:320`) carries the full set incl. `adp`, `adp_odsp`, `march_of_dimes`, etc.
|
||||
- **MOD tracking already exists** as `x_fc_mod_status` (16 states) + ~60 `x_fc_mod_*` fields (HVMP reference #, vendor code, drawings, PCA, POD, approved/payment amounts, dated audit trail) + MOD views + ~7 wizards + ~40 MOD/ODSP stage emails (`fusion_claims/models/sale_order.py:438,877`). An accessibility assessment funded `march_of_dimes` already lands its SO in this pipeline at `need_to_schedule`. **The gap is purely that the rep can't choose `march_of_dimes` on the form.**
|
||||
- **Emails** are mostly Python-built via the shared `fusion.email.builder.mixin._email_build` (`fusion_tasks/models/email_builder_mixin.py:8`), gated by `ir.config_parameter` `fusion_claims.enable_email_notifications`. Completion email fires from inside `_create_draft_sale_order` (`assessment.py:847`; `accessibility_assessment.py:624`). Stage emails (`_adp_send_stage_email`, `_mod_email_build`, `_odsp_email_build`) are keyed off the SO's funding type + status, so **they keep working per-SO unchanged**.
|
||||
- **Known bug:** backend ADP `action_complete()` sends the authorizer **two** completion emails (template pair at `assessment.py:494` + inline report via `:847`). Must consolidate before fanning out across a visit.
|
||||
|
||||
---
|
||||
|
||||
## 3. The design
|
||||
|
||||
### 3.1 The Visit aggregate (only net-new model)
|
||||
|
||||
`fusion.assessment.visit` — the hub for one home visit.
|
||||
|
||||
- **Client/context, entered once:** `partner_id`, address fields, `visit_date`, `sales_rep_id`, `authorizer_id` (OT), `x_fc_funding_source`-style default, `state` (`measuring` → `client_pending` → `done`).
|
||||
- **Links to its assessments:** `adp_assessment_ids` (One2many → `fusion.assessment`) and `accessibility_assessment_ids` (One2many → `fusion.accessibility.assessment`). Each assessment gains `visit_id`.
|
||||
- **Links to its sale orders:** `sale_order_ids` (One2many → `sale.order`) — one per funding workflow it produced.
|
||||
- On the SO side, add `visit_id`. Each assessment already carries `sale_order_id` (Many2one — `accessibility_assessment.py:153`, `assessment.py:422`), so several same-funding assessments can already point at one SO; the redundant **scalar** `assessment_id` / `accessibility_assessment_id` on the SO (`fusion_portal/models/sale_order.py:37,48`) become **One2many** (or are dropped in favour of the `sale_order_id` reverse) so an SO no longer assumes a single source assessment.
|
||||
|
||||
Client info moves to the Visit as the single source of truth; the per-assessment `client_name`-required gate is relaxed (the model keeps the field for back-compat / standalone use but the Visit flow fills it from `partner_id`).
|
||||
|
||||
### 3.2 Add-as-you-go workspace (portal UX)
|
||||
|
||||
A portal "visit workspace" (reps are portal users, tablet-first):
|
||||
|
||||
- Always-present **"+ Add"** → pick a type + location tag (Front / Back / Inside / custom) → drop **straight into the existing measurement form** for that type. No client paperwork required to start.
|
||||
- Each added assessment is a **card** showing type, location, status (To measure / Measured / Signed), and — once priced — its amount.
|
||||
- **Measurement-first:** the forms render with client fields hidden/optional; a **deferred "Client + funding" step** is completed after the OT leaves and is shared by every item.
|
||||
- The **OT signs the ADP application (Page 11)** inline on the wheelchair/ADP item, on-site, independent of client demographics (reuse `portal_assessment_express` Page-11 section + signature pad).
|
||||
- Mockups (for reference, in repo `docs/mockups/` if committed): `fusion_portal_new_approach_mockup.html`.
|
||||
|
||||
### 3.3 Multi-instance + location tags
|
||||
|
||||
Any type can be added **more than once**, each its own assessment record with a **location label** ("Main stairs", "Basement", "Front porch"). Two stair lifts = two assessment records (→ two lines on the same funding SO; see §3.6). A **"Same as the previous"** action copies shared options so the rep only re-enters the differing measurements.
|
||||
|
||||
### 3.4 Per-item funding selector — the MOD gap fix
|
||||
|
||||
Expose `x_fc_funding_source` on **each accessibility assessment** in the flow: **Private Pay / March of Dimes / ODSP / WSIB / Hardship / Insurance / Other**. This one field drives the existing `sale_type_map` → `x_fc_sale_type` → correct pipeline (MOD 16-state tracker, ODSP, hardship, …). Defaults to the previous item's funding so an all-MOD visit isn't re-picked each time. **ADP/wheelchair items are fixed to ADP** (no picker). This is the minimal change that closes the "can't mark a case as March of Dimes" gap — no new tracking model.
|
||||
|
||||
> **Patient lift** is an accessibility/equipment item that uses this same picker — funded by March of Dimes, **ODSP**, or **Hardship** (e.g. Toronto residents), so its funding is chosen per case, not fixed.
|
||||
> **`sale_type_map` gap:** `x_fc_funding_source` currently lacks `hardship` while `x_fc_sale_type` already has it (`sale_order.py:320`) — add `hardship` to the picker + a `sale_type_map` entry (`accessibility_assessment.py:771`), and review the map so every offered funding routes to a real `x_fc_sale_type`.
|
||||
> **MOD funding cap** applies to MOD items — see Resolved decision 1 (§4).
|
||||
|
||||
### 3.5 ADP multi-device + combinations + scooter + home-access rule
|
||||
|
||||
**Multi-device ADP order.** Today one ADP device per order; the visit allows a **valid combination** of ADP devices for one client, all landing on the **one ADP SO**. Each ADP device is an item; the combination check runs across the visit's ADP items.
|
||||
|
||||
**Device categories:** Walker/Rollator · Manual Wheelchair · Power Wheelchair · **Scooter (new)**.
|
||||
|
||||
**Combination rules (confirmed):**
|
||||
|
||||
| Combination | Allowed? |
|
||||
|---|---|
|
||||
| Any single device | ✓ |
|
||||
| Walker + Manual Wheelchair | ✓ |
|
||||
| Walker + Power Wheelchair | ✓ |
|
||||
| Walker + Scooter | ✓ |
|
||||
| Manual + Power Wheelchair | ✗ |
|
||||
| Power Wheelchair + Scooter | ✗ |
|
||||
| Manual Wheelchair + Scooter | ✗ |
|
||||
| Two walkers / any duplicate | ✗ |
|
||||
|
||||
Rule in words: **at most one "seated-mobility" device** {manual wheelchair, power wheelchair, scooter}, **optionally one walker/rollator alongside, no duplicates.** Enforced when adding/saving an ADP device.
|
||||
|
||||
**Scooter (new ADP type) fields:** `client_weight` (exists), scooter type, **maximum travel range**, and the home-accessibility check (below). Gets its own measurement section in the ADP form, mirroring the rollator/wheelchair/powerchair sections.
|
||||
|
||||
**Power-mobility home-accessibility hard rule.** For **scooter and power wheelchair**, a required check: *"Is the home accessible enough for the device to be used **inside and outside** the home independently — no lifting, not left outside/in the garage?"* ADP will not fund power mobility a home can't accommodate. If the answer is **No**, the visit **flags an accessibility need** and prompts the rep to add an accessibility item (ramp / porch lift, typically March of Dimes) to remediate. This is the explicit bridge between the ADP power-mobility item and the accessibility/MOD upsell.
|
||||
|
||||
> **The power-wheelchair form is already well-optimized — do NOT change its fields.** The *only* addition there is this home-accessibility warning. The new **scooter** type gets its own section (fields above); the manual-wheelchair and rollator sections are unchanged.
|
||||
|
||||
### 3.6 Funding-workflow grouping → one SO per workflow
|
||||
|
||||
On visit completion, group its assessments by **funding workflow** (`x_fc_sale_type`) and create **one SO per group**:
|
||||
|
||||
- All `march_of_dimes` items (stair lift + porch lift + tub cutout, or two stair lifts) → **one MOD SO, multiple lines** (funding permitting).
|
||||
- All ADP devices (the valid combination) → **one ADP SO**.
|
||||
- Private / ODSP / WSIB / insurance → their own SO each.
|
||||
- A separate SO appears **only when the case type changes**, never per-item within a funding.
|
||||
|
||||
Refactor the two per-model `_create_draft_sale_order` routines into a **shared, group-aware builder** that takes a set of same-funding assessments and produces one SO, branching on funding type to stamp the right starting status field (`x_fc_adp_application_status` for ADP, `x_fc_mod_status` for MOD, etc. — mirroring `assessment.py:600-622`) and the right links. **Reuse the existing MOD/ADP/ODSP pipelines unchanged.**
|
||||
|
||||
### 3.7 Emails
|
||||
|
||||
- Reuse `fusion.email.builder.mixin` and the existing per-funding stage emails (they're keyed off SO type + status, so per-SO they keep working).
|
||||
- **Move the completion send to per-SO** inside the new builder (not per-assessment), and **dedupe recipients**, so a 3-item visit doesn't emit 3–6 completion emails.
|
||||
- **Fix the existing duplicate** (authorizer gets two completion emails on backend ADP completion) as part of this.
|
||||
- Make `enable_email_notifications` gating consistent across the sends the visit touches.
|
||||
|
||||
### 3.8 Reused vs net-new
|
||||
|
||||
- **Reused, largely untouched:** the 7 accessibility measurement forms + their JS/Python calc; the ADP Express form + Page-11 signature; the MOD/ADP/ODSP pipelines, views, wizards, and stage emails; the email branding mixin.
|
||||
- **Net-new:** the `fusion.assessment.visit` model + workspace UI; per-item funding selector on the accessibility forms; the group-aware SO builder + link-cardinality change; ADP multi-device + combination validation; scooter type + fields; power-mobility home-access rule + cross-sell flag; completion-email consolidation.
|
||||
|
||||
---
|
||||
|
||||
## 4. Resolved decisions
|
||||
|
||||
1. **MOD funding cap — documented rule, light-touch in v1.** March of Dimes covers **up to $15,000 per person, lifetime**, income-gated: if the client's income is **under** that year's threshold (the threshold changes annually), MOD funds the full $15k; if **over**, MOD may **deny or partially approve**. **v1:** surface this cap as a reminder on MOD items and capture an *"income under MOD threshold? (yes / no / unknown)"* flag so the rep can judge — **do not** auto-compute lifetime used-vs-remaining across the client's prior MOD orders (the SO's existing `x_fc_mod_*` approved/payment fields already record per-order amounts). **Future:** yearly-threshold config + automatic lifetime-remaining tracking + a hard warning.
|
||||
2. **No auto pricing / products in v1.** The visit creates a **draft** SO per funding workflow and appends each assessment's specs to that SO's chatter (today's pattern); **the sales rep builds the quotation lines manually.** One SO can hold many items. No per-assessment-type product mapping. (Auto-pricing is a future expansion.)
|
||||
3. **Patient-lift funding is chosen per case** via the funding picker — March of Dimes, **ODSP**, or **Hardship** (e.g. Toronto residents) all fund it; it is not fixed (see §3.4).
|
||||
4. **Power-wheelchair form unchanged** — already well-optimized; the only addition is the **home-accessibility warning** (device usable **inside and outside** the home). The home-access rule applies to **scooter (new type, new section) and power wheelchair (warning only)**.
|
||||
|
||||
---
|
||||
|
||||
## 5. Phasing
|
||||
|
||||
- **Phase 1 — Funding correctness + visit backbone:** `fusion.assessment.visit`, link-cardinality change, **funding selector on the accessibility forms** (incl. Hardship; patient-lift routing), **MOD $15k-cap reminder + income-threshold flag** (informational), group-and-route to per-workflow **draft** SOs (specs to chatter, manual pricing) reusing existing pipelines, completion-email consolidation + duplicate fix. *(Delivers the MOD-routing fix and the multi-SO split.)*
|
||||
- **Phase 2 — ADP expansion:** multi-device ADP order + combination validation, **scooter** type + fields, power-mobility **home-access hard rule** + accessibility cross-sell prompt.
|
||||
- **Phase 3 — Seamless field UX:** the full add-as-you-go workspace, measurement-first deferral, location tags, "same as previous", OT on-site sign-off polish.
|
||||
- **Later:** product-line auto-pricing, MOD funding-cap tracking, voice/quick entry.
|
||||
|
||||
---
|
||||
|
||||
## 6. Risks (from investigation)
|
||||
|
||||
- **Duplicate completion emails** already live on the ADP backend path — fix before fan-out (§3.7).
|
||||
- **Scalar back-links + double-SO guards** assume one SO per assessment; grouping breaks them — must move to `visit_id` / One2many and make the guard visit-aware.
|
||||
- **Inconsistent `enable_email_notifications`** — template sends ignore the kill-switch; don't route new traffic through templates without honoring it.
|
||||
- **Label drift** `x_fc_funding_source` vs `x_fc_sale_type` (`insurance`="Private Insurance" vs "Insurance"; `direct_private`="Private Pay (Direct)" vs "Direct/Private") — keys match so routing works; align labels in any shared UI.
|
||||
- **Unreachable funding types from accessibility:** `sale_type_map` (`accessibility_assessment.py:771`) covers 6 values; decide which funding types each assessment type may emit.
|
||||
|
||||
---
|
||||
|
||||
## 7. Files in scope
|
||||
|
||||
- `fusion_portal/models/assessment.py` — ADP `_create_draft_sale_order` (:587), completion email (:847), multi-device + scooter + home-access.
|
||||
- `fusion_portal/models/accessibility_assessment.py` — accessibility `_create_draft_sale_order` (:751), `action_complete` (:493), completion email (:624), funding routing.
|
||||
- `fusion_portal/models/sale_order.py` — back-links (:37,:48) → `visit_id` / One2many.
|
||||
- `fusion_portal/models/visit.py` — **new** `fusion.assessment.visit`.
|
||||
- `fusion_portal/views/portal_accessibility_forms.xml` + `portal_assessment_express.xml` — funding selector, scooter section, home-access check; workspace shell.
|
||||
- `fusion_portal/controllers/portal_main.py` (`/my/accessibility/save` :2482) + `portal_assessment.py` — visit-aware save/group/route.
|
||||
- `fusion_claims/models/sale_order.py` — reuse `x_fc_sale_type` (:320), `x_fc_mod_status` (:438), stage emails (:6876,:9038,:10063); no pipeline rebuild.
|
||||
- `fusion_tasks/models/email_builder_mixin.py` — reuse for any new visit emails.
|
||||
|
||||
**Deployment note:** `fusion_portal` is live on `odoo-westin` (`westin-v19`, container `odoo-dev-app`). Ship per the rename/deploy procedure (backup → code sync → `-u fusion_portal` → cache-bust → restart → verify).
|
||||
Reference in New Issue
Block a user