Ran Step 0 against Westin prod (westin-v19 on odoo-westin). Resolved the APP/DB placeholders (DO boxes dead; migrated on-prem to odoo-dev-app), added a dated STEP 0 RESULTS section, and corrected the open questions the live inspection disproved: no stair/porch lifts in Westin ADP data; Enterprise appointment already ships native token booking; fusion_repairs contract engine not deployed; device_type is the ADP billing-code catalog taxonomy, not the install base. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
12 KiB
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_repairsrun on one database → new module may depend on both. - Enterprise
appointmentis 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_claimsruns 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 whenproduct.template.x_fc_maintenance_interval_months > 0.- Daily reminder cron
cron_maintenance_due_reminders→ 30/7/1-day bands → branded emailemail_template_maintenance_due_reminderwith 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 onx_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:
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.- Claims bridge (daily cron): scan
fusion_claimssale.order.linefor delivered+approved devices whosedevice_typematches an active policy → ensure a maintenance contract exists, anchored atdelivery_date + interval. Idempotent (key on serial / sale-line). Extend the reused contract withx_fc_source_claim_line_id,x_fc_device_type,x_fc_policy_idso the repairs path and claims path both feed one contract model. - Real-time booking on
appointment: token link → slot picker backed byappointment.type(partner pre-resolved from token, no login). Slot pick → realcalendar.event→ hook spawnsrepair.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/DBwith the real Westin container + database. CLAUDE.md rule #1: never code from memory — read the real Enterpriseappointmentsource before building the booking layer.
# 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 must be sized on sale.order.line (x_fc_adp_device_type
[computed 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) — NOT yet
run; this is the next grounding step.
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.
Open questions to resolve with the user (in the connected session)
- MVP cut: reminders+booking for which device categories first? (Step 0 update: stair/porch lifts do NOT exist in Westin's ADP data. Candidates are the powered units most likely to need recurring service — power wheelchairs, power bases, power scooters — then manual wheelchairs / walkers.)
- 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
appointmentalready 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(notgroups_id);ir.cronhas nonumbercall; declarativemodels.Constraint/models.Index; HTTP routestype="jsonrpc"; OWL uses standalonerpc(). - No
sale.subscriptionmodel exists — a subscription is asale.orderwithis_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 onodoo-modsdev-app).