Files
Odoo-Modules/docs/plans/fusion_maintenance_brainstorm.md
gsinghpal 12d383a8c2 docs(fusion_maintenance): add Westin install-base sizing to Step 0 results
Sized the real serial-tracked install base on sale.order.line: ~138 units / ~136 customers across all funders (walkers 68, wheelchairs 45, power bases 7, scooters 4, +14 with no ADP device_type). Serial# is captured ~only on equipment, so it doubles as a trackable-unit marker. ADP-only gating misses ~28 units (direct_private/adp_odsp/march_of_dimes) -> bridge should key on serial, funder-agnostic. Flags two data gaps (no-device_type units; non-ADP units lacking delivery_date) and reframes the MVP open question as volume (walkers/chairs) vs margin (powered units).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 00:33:12 -04:00

195 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# fusion_maintenance — Brainstorm & Handoff Brief
> Status: **research/brainstorm only — no code, no final decisions.** Written from a
> Claude Code *web* session that could **not** reach the private network (no Tailscale,
> no docker daemon, Supabase KB unreachable). Resume from a **Tailscale-connected env**
> (dev box or a host that can reach Westin production) and do the live inspection in
> Step 0 **before** committing to the design.
## Goal (user's words, paraphrased)
Automated maintenance follow-ups for mobility/accessibility equipment we've sold, to turn
service into **recurring revenue**. Reminder emails → client books maintenance → booking
happens in **real time** and **lands in our calendar**. Leverage Odoo Enterprise's
appointment system. Decide whether this lives in `fusion_repairs` or a new module — the
result must be **seamless and production-ready**.
## Decisions locked with the user (this session)
- **Same DB**: `fusion_claims` + `fusion_repairs` run on one database → new module may depend on both.
- **Enterprise `appointment` is available** → build real-time booking ON it (`appointment.type` /
`appointment.slot` / `calendar.event`), do **not** hand-roll a calendar.
- **Public self-serve booking** → reminder email carries a token link to a no-login slot picker
(extend the existing `/repairs/maintenance/book/<token>` pattern). Elderly clients shouldn't log in.
- **Target box for grounding = Westin production** (where `fusion_claims` runs day-to-day).
## Key findings from repo exploration
### `fusion_repairs` (v19.0.2.2.6) ALREADY has a maintenance engine — reuse it, don't fork
- `fusion.repair.maintenance.contract`: interval, due/last-service dates, state machine.
Auto-spawned on SO confirm when `product.template.x_fc_maintenance_interval_months > 0`.
- Daily reminder cron `cron_maintenance_due_reminders` → 30/7/1-day bands → branded email
`email_template_maintenance_due_reminder` with tokenized link `/repairs/maintenance/book/<token>`.
- Booking controller: `controllers/portal_maintenance_booking.py` — **single date-confirm form,
NO slot availability, NO conflict check, NO calendar event.** ← this is the real gap.
- Contract **roll-forward** on technician-task completion (`next_due_date += interval`).
- `fusion.repair.service.plan.subscription`: pre-paid visit plans (recurring-revenue primitive).
- Deps: `repair, maintenance, sale_management, stock, purchase, website, portal, fusion_tasks,
fusion_poynt, fusion_authorizer_portal`. ~8.3k LOC, 25+ models.
### `fusion_claims` (v19.0.9.2.0) is the ideal trigger source
- Claim container = `sale.order` (`x_fc_sale_type`: adp, odsp, wsib, insurance, march_of_dimes, …).
- **Equipment unit** = `sale.order.line.x_fc_serial_number` + `product_id`.
- **Equipment category** = `fusion.adp.device.code.device_type` (wheelchair, walker, hospital bed,
stair lift, porch lift, custom ramp, …) — matches the user's "sale groups".
- **Schedule anchors**: `x_fc_adp_delivery_date`, `x_fc_service_start_date`; gate on `x_fc_adp_approved`.
- Customer = `sale.order.partner_id`; prescriber = `x_fc_authorizer_id`.
- Already depends on `calendar, fusion_tasks, ai, fusion_ringcentral`.
## Proposed architecture (PENDING live verification)
**New module `fusion_maintenance`** depending on `fusion_repairs`, `fusion_claims`, `appointment`.
Reuses the existing contract/reminder/roll-forward engine; adds the 3 genuinely-missing pieces:
1. **`fusion.maintenance.policy`** (ops-configurable, no code per category):
`device_type` → `interval_months`, reminder bands, `service_product_id` (priced visit),
`appointment_type_id`, required technician skill. Turns "stair lift = 6 mo, $X" into data.
2. **Claims bridge** (daily cron): scan `fusion_claims` `sale.order.line` for delivered+approved
devices whose `device_type` matches an active policy → ensure a maintenance contract exists,
anchored at `delivery_date + interval`. Idempotent (key on serial / sale-line). Extend the
reused contract with `x_fc_source_claim_line_id`, `x_fc_device_type`, `x_fc_policy_id` so the
repairs path and claims path both feed **one** contract model.
3. **Real-time booking on `appointment`**: token link → slot picker backed by `appointment.type`
(partner pre-resolved from token, no login). Slot pick → real `calendar.event` → hook spawns
`repair.order` + technician task, assigns by skill/zone, advances reminder band, rolls contract
forward.
**Recurring revenue**: each policy carries `service_product_id` → booked visit drafts a priced
SO/invoice; optional pre-paid annual plan via existing `service.plan.subscription`; optional
door payment via existing `fusion_poynt`.
## STEP 0 — run on Westin production FIRST (grounding before any decision)
> Replace `APP`/`DB` with the real Westin container + database. CLAUDE.md rule #1: never code
> from memory — read the real Enterprise `appointment` source before building the booking layer.
```bash
# RESOLVED 2026-06-02 — Westin Odoo prod migrated OFF Digital Ocean onto the on-prem Proxmox
# cluster. Old DO IPs (152.42.146.204 / 178.128.229.92) are DEAD (:22 timeout). Live box:
# host `odoo-westin` = 192.168.1.40 via the `supabase-prod` Tailscale jump (Windows OpenSSH
# ProxyCommand → run `ssh odoo-westin ...` from PowerShell). App container `odoo-dev-app`
# (odoo:19, Enterprise); DB container `odoo-dev-db`; DB `westin-v19`; user `odoo` (local-socket
# trust inside odoo-dev-db). Enterprise addons → /mnt/enterprise-addons, custom → /mnt/extra-addons.
# SQL: ssh odoo-westin 'docker exec odoo-dev-db psql -U odoo -d westin-v19 -c "..."'
# FS read: ssh odoo-westin 'docker exec odoo-dev-app sed -n 1,160p /mnt/enterprise-addons/...'
APP=odoo-dev-app ; DB=westin-v19 ; DBC=odoo-dev-db
# 1) Install matrix — confirm same-DB + Enterprise appointment present + versions
docker exec "$APP" psql -U odoo -d "$DB" -c \
"SELECT name,state,latest_version FROM ir_module_module \
WHERE name IN ('fusion_claims','fusion_repairs','fusion_maintenance','calendar','maintenance','repair') \
OR name LIKE 'appointment%' ORDER BY name;"
# 2) Real device_type distribution (drives per-category policies)
docker exec "$APP" psql -U odoo -d "$DB" -c \
"SELECT device_type, count(*) FROM fusion_adp_device_code GROUP BY device_type ORDER BY 2 DESC;"
# 3) Locate the Enterprise appointment source (read, don't guess the API)
docker exec "$APP" bash -lc 'ls -d /mnt/enterprise-addons/appointment 2>/dev/null || \
find / -maxdepth 6 -type d -name appointment 2>/dev/null | grep -i addons | head'
# 4) Appointment model surface to build booking on (adjust path from #3)
docker exec "$APP" cat <appointment_path>/models/appointment_type.py | head -160
docker exec "$APP" ls <appointment_path>/controllers/ # find the public booking controller
# 5) How fusion_repairs maintenance contracts already look in live data
docker exec "$APP" psql -U odoo -d "$DB" -c \
"SELECT state, count(*) FROM fusion_repair_maintenance_contract GROUP BY state;"
```
## STEP 0 — RESULTS (ran 2026-06-02 against Westin prod `westin-v19`)
> Grounding facts only — **no design decisions made**. These correct several assumptions above.
**Connection (resolved):** host `odoo-westin` (192.168.1.40) via the `supabase-prod` Tailscale jump.
App container `odoo-dev-app` (odoo:19, Enterprise), DB container `odoo-dev-db`, DB `westin-v19`,
user `odoo`. Old Digital Ocean boxes are DEAD — Westin migrated on-prem.
**1) Install matrix** — `appointment` **19.0.1.3 installed** (+ `appointment_account_payment`,
`_crm`, `_hr`, `_microsoft_calendar`, `_sms`). All deps present: `calendar`, `maintenance`, `repair`,
`sale_management`, `portal`, `website`, `resource`, `phone_validation`, `web_gantt`. `fusion_claims`
**19.0.9.2.0 installed**. `fusion_repairs` and `fusion_maintenance` are **absent entirely** (no
records). → a module depending on `appointment` installs cleanly; "reuse the fusion_repairs engine"
means *deploy fusion_repairs to Westin first* (heavy) **or** own a lean contract model here. Note
Odoo's native `maintenance` (CMMS) is installed — an under-considered third reuse option.
**2) device_type** — 119 distinct values, but `fusion.adp.device.code` is the ADP billing-code
**CATALOG** (`_order='device_type, device_code'`), so counts are catalog codes per type, **NOT units
installed**. Top entries are seating COMPONENTS (Seat Cushion 564, Back Support 375, Headrest 193).
The maintainable **equipment classes** ≈ wheelchairs (manual + power tilt), power bases, power
scooters, wheeled walkers / walking frames, paediatric standing frames, specialty strollers (~6-8
clean categories). → `device_type` can't be a 1:1 policy key (119 values, mostly parts); needs a
grouping/whitelist. **Real install base sized on `sale.order.line`** (`x_fc_adp_device_type` [stored compute from
product's `x_fc_adp_device_code_id.device_type`], `x_fc_serial_number`, `x_fc_adp_approved`; delivery
dates `x_fc_adp_delivery_date` / `x_fc_service_start_date`) — **see the Install-base sizing block below.**
**3) + 4) Enterprise appointment source** — `/mnt/enterprise-addons/appointment`. The no-login token
slot-picker is **mostly NATIVE — don't hand-roll it**: public booking (`auth="public"`), invite
tokens (`appointment.invite`, `/appointment/<id>?…invite_token`), live availability
(`/appointment/<id>/update_available_slots`, jsonrpc/public), slot submit → real `calendar.event`
(`/appointment/<id>/submit`), auto/manual staff+resource assignment, capacity, booked/cancelled mail
templates. Model `appointment.type`; controller `controllers/appointment.py`. → the module mainly
needs to: seed an `appointment.type` per category, drop a partner-bound invite link into the reminder
email, and hook `calendar.event` create → spawn the service task + advance the contract.
`appointment_account_payment` is installed → native pay-to-book is on the table for the revenue mechanic.
**5) Maintenance-contract state** — `relation "fusion_repair_maintenance_contract" does not exist`
→ confirms the fusion_repairs maintenance engine is **not** on Westin.
**Headline correction:** Westin's ADP data has **zero** stair lifts / porch lifts / ramps / hospital
beds — those belong to the fusion_repairs / EN-Tech (mobility) domain. Westin's recurring-revenue
play is **wheelchairs / power bases / scooters / walkers / seating**. Open questions updated below.
**Install-base sizing (ran 2026-06-02 — the REAL units, complementing #2's catalog counts).** Big tell:
serial numbers are captured **~only on actual equipment** (every part/option/mod device_type shows 0
serials), so `x_fc_serial_number` is already a de-facto "trackable unit" marker — convenient, because the
bridge's idempotency key is the serial.
- **Addressable base ≈ 138 serial-tracked units across ~136 customers** (all funders). By equipment
family (serial-tracked / of which delivered): **Walkers & walking frames 68 (55)**, **Wheelchairs 45
(40)**, **Power bases 7 (6)**, **Scooters 4 (3)**, plus **14 units with no ADP device_type** (likely
private-pay) and 1 misc.
- **Funder split** (serial-tracked): adp 109, direct_private 13, adp_odsp 10, march_of_dimes 7;
wsib / insurance / standalone-odsp / rental / regular = **0 serials**. → an ADP-only gate
(`x_fc_adp_approved`) captures ~110 and **misses ~28** real units. The bridge should likely key on
**serial (funder-agnostic)**, not approval.
- **Two data gaps the design must absorb:** (a) the 14 serial units with no ADP device_type can't be
classified by a device_type→policy map → need a product-level or manual category override; (b) non-ADP
units have no `x_fc_adp_delivery_date` → the contract anchor (`delivery_date + interval`) needs a
fallback (invoice/order date).
- Deliveries span **2022-10 → 2026-05** (active program) — history to anchor intervals + a live pipeline.
- Top serial-tracked device_types: Adult Wheeled Walker Type 3 (47), Adult Manual Dynamic Tilt Type 5
Wheelchair (23), Adult Lightweight Performance Type 3 (11), Adult Lightweight Standard Type 1 (10),
Adult Wheeled Walker Type 2 (9), Adult Power Base Type 3 (5), Power Scooter (3). (1 line ≈ 1 unit;
equipment device_types are 1 base line each.)
## Open questions to resolve with the user (in the connected session)
- **MVP cut**: which categories first? Sizing surfaces a real tension: **by volume** it's walkers (68) +
wheelchairs (45) ≈ 82% of the base, but rollators/walkers are mechanically low-service; **by
service-revenue-per-unit** the targets are the powered units (power bases 7 + scooters 4 + power
wheelchairs) — high maintenance value but only ~1115 units today. Volume vs. margin — or phase it
(powered units first to prove the booking loop, then walkers/manual chairs for reach)?
- **Revenue mechanic**: auto-draft a priced SO/invoice per booking, vs. pre-paid annual plan, vs.
pay-at-door via Poynt — which is the default?
- **Technician assignment**: auto-assign by skill+zone at booking time, or leave dispatch manual
(fusion_tasks) and only reserve the calendar slot?
- **Booking-portal strategy**: Step 0 shows Enterprise `appointment` already ships public,
token-based real-time booking (`appointment.invite` + `/appointment/<id>/...`, `auth="public"`).
Ride on that (generate an invite per reminder, partner pre-bound, no login) vs. a custom
`/maintenance/book/<token>` route? (The `/repairs/...` route is moot — fusion_repairs isn't on Westin.)
## Applicable CLAUDE.md rules (don't relearn the hard way)
- Rule #1: read reference files from the running instance before coding (esp. the appointment source).
- Odoo 19: `res.users.group_ids` (not `groups_id`); `ir.cron` has no `numbercall`; declarative
`models.Constraint`/`models.Index`; HTTP routes `type="jsonrpc"`; OWL uses standalone `rpc()`.
- No `sale.subscription` model exists — a subscription is a `sale.order` with `is_subscription=True`.
- New fields use `x_fc_` prefix; Canadian English; `$` Monetary + `currency_id`.
- Route attachment opens through `fusion_pdf_preview` (`att.action_fusion_preview(...)`).
- Tests need `--http-port=0 --gevent-port=0`. Westin prod is Enterprise; local dev is Community
(so the appointment-dependent module can't be installed/tested on `odoo-modsdev-app`).