Files
Odoo-Modules/docs/superpowers/specs/2026-06-03-technician-service-booking-design.md
gsinghpal f0400114f9 docs(service-booking): add spec, plans, mockup, and clone-verify script
Kickoff brief, design spec, both implementation plans (rates foundation +
booking wizard), the UI mockup, and the hands-off Westin clone-verify/deploy
script for the Technician Service Booking feature.

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

173 lines
13 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.
# Technician Service Booking & Auto-Quote — Design Spec
**Date:** 2026-06-03
**Modules:** `fusion_tasks` (booking wizard, task, time/tz), `fusion_claims` (SO link, rate-card products, SO creation)
**Status:** Draft for review
**Mockup:** `docs/superpowers/mockups/technician-booking-wizard.html` (v2)
---
## 1. Problem & Goal
Operators booking a technician service today use the raw `fusion.technician.task` form in a modal. Three problems:
1. **Forced SO:** a hard constraint (`fusion_claims/models/technician_task.py:105 _check_order_link`) requires a Sale Order **or** Purchase Order for every task except `ltc_visit`. A repair for a brand-new client (no SO yet) is blocked.
2. **Time fields:** Start/End use a 24-hour `float_time` widget while every other view shows 12-hour AM/PM; and the local→UTC conversion is inconsistent (`_compute_datetimes` resolves *company-calendar-tz → user-tz → UTC*, but `_inverse_datetime_*` uses *user-tz → UTC* only — they disagree, and fall back to UTC when unset).
3. **No revenue capture at booking:** the booking creates a task but no priced order, even though every service call has a defined call-out fee.
**Goal:** a fast, polished **"Book a Service"** wizard that, from one screen, (a) captures the client — including brand-new clients inline, (b) books the technician task, (c) prices the call-out from the rate card, and (d) auto-creates a **draft repair Sale Order**. Every service call becomes a revenue-tracked order. Works in dark + light.
---
## 2. Scope
**In:** OWL booking wizard (complete design freedom); inline new-client create (name/phone/email/address); rate-card product catalog; service-type → call-out pricing; auto draft repair SO (call-out line + auto per-km); live on-screen estimate; 12-hour AM/PM time entry; timezone-conversion fix; relaxation of the SO constraint.
**Out (phase 2):** deposit/payment capture; multi-technician labour auto-doubling; SMS gateway; maintenance/PM plans; full quote builder (estimated labour & parts written onto the SO at booking — for now the SO carries call-out + per-km only, labour/parts added as actuals).
---
## 3. Pricing model (Westin rate card)
> These values only **seed** the editable `fusion.service.rate` table (§6.1). After install, admins
> change any price and add new rate types from the **Service Rates** menu — nothing here is hardcoded,
> and the wizard reflects edits live.
### 3.1 Call-out fee matrix (the guaranteed charge; includes 30 min labour where noted)
| Category | Normal | Rush (+km) | After-Hours (+km) |
|---|---|---|---|
| **Standard** | $95 | $120 | $140 |
| **Lift & Elevating** | $160 | **$185** ◆ | **$205** ◆ |
-**Suggested fills** (not on the printed card). Derived from the card's own surcharge deltas: Standard Rush = +$25, After-Hours = +$45 over base; same deltas applied to the Lift base ($160) → $185 / $205. *Owner to confirm.*
- **Rush & After-Hours** add **$0.70/km × 2-way** (round trip), computed from the booking's travel distance.
- **In-shop (any device):** no call-out fee; labour billed at $75/hr; no delivery.
### 3.2 Labour (hourly, pro-rated in 30-min increments; per technician)
- On-site (Standard): **$85/hr**
- In-shop: **$75/hr** (already exists as product `LABOR`, default_code `LABOR`)
- Lift & Elevating on-site: **$110/hr**
### 3.3 Travel
- Per-km surcharge: **$0.70/km × 2-way**
### 3.4 Delivery / Pickup
| Item | Price |
|---|---|
| Local (within Brampton) | $35 |
| Outside local area | $60 |
| Rush pickup/delivery | $60 + $0.70/km ×2-way |
| Lift-chair delivery & set-up | $120 |
| Hospital-bed delivery & set-up | $120 |
| Stairlift delivery & set-up | $300 |
| Stairlift removal | $300 |
### 3.5 Footnote rules (from the card)
- A Service Call is an appointment **outside** a Westin location, billed **once per request**, includes **30 min labour**; labour rates apply after.
- Parts are **not** charged when covered under manufacturer warranty (→ "Under warranty" flag on the wizard).
- Multiple technicians → labour applies **per technician** (phase-2 auto-double; for now informational).
---
## 4. UX — wizard layout
Single page (no multi-step), grouped cards, brand-gradient header, dark/light. Sections (see mockup v2):
- **Customer** — segmented `Existing customer | New client`. Existing = search by **phone / name / SO** → prefill. New = **name, phone, email, address (street/unit/buzz/city)** inline; contact find-or-created on save.
- **Service & Pricing** — *device being serviced* (→ auto-suggests category: scooter/chair/bed → Standard; stairlift/lift → Lift & Elevating), *issue/symptom*, *service call type* (category × timing), and the resulting **call-out fee** readout.
- **Schedule** — date, **12-hour AM/PM start picker**, duration → auto end ("Ends at 10:00 AM · local time"), technician + availability hint.
- **Location** — **in-shop toggle** (drives pricing: no call-out, $75 labour, hides address), job address.
- **Job details** — work description, parts to bring, **under-warranty** toggle, POD, send-confirmation, request-review.
- **Estimate** (prominent strip) — *call-out + est. labour + per-km = total*; "a draft repair SO is created."
- **Footer** — Cancel · **Book & Create SO**.
Behaviours: device→category auto-suggest (overridable); in-shop flips pricing & hides address + call-out; live estimate recomputes on every change; AM/PM picker stores local float hours.
---
## 5. Architecture
**Complete UI freedom without duplicating backend logic:**
- **OWL client action** `fusion_tasks.service_booking` — renders the layout; loads reference data (technicians, device types, rate products, customer search) via standalone `rpc()` (`@web/core/network/rpc`). Registered in `registry.category("actions")`. Opened from a "Book a Service" button/menu/dashboard tile (`ir.actions.client`).
- **One server method** `fusion.technician.task.action_book_from_wizard(payload)`:
1. Resolve customer — search `res.partner` by email then phone; create if new (name/phone/email/address). For "existing", use the chosen partner/SO's partner.
2. Compute **travel distance now** (Google Distance Matrix via the existing `_calculate_travel_time`/`_get_google_maps_api_key`) from the shop / previous task to the job — needed for the per-km line.
3. Create a **draft `sale.order`** tagged as a repair (see §6) with the **call-out product line** + an **auto per-km line** (qty = round(distance_km × 2), product = per-km $0.70) when the service type is Rush/After-Hours.
4. Create the `fusion.technician.task` linked to that SO (reuses existing model `create` + address-fill + travel-chain logic).
5. Return `{task_id, order_id}` so the client action can open the task or close.
- **SCSS** `fusion_tasks/static/src/scss/_service_booking_tokens.scss` + `service_booking.scss`, branching on `$o-webclient-color-scheme` (per repo rule), registered in `web.assets_backend` **and** `web.assets_web_dark`. Three-layer contrast tokens (page → card → field), explicit hex.
All validation/workflow/pricing stays server-side; the OWL component is presentation + a single submit call.
---
## 6. Data model changes
### 6.1 New: editable rate table `fusion.service.rate` (the configurable pricing control)
A dedicated model so admins manage **all** pricing from a **Service Rates** menu — no code to change a price or add a service type.
**Fields:** `name`; `code` (unique, e.g. `callout_standard_normal`, `callout_lift_rush`, `labour_onsite`, `labour_lift`, `per_km`, `delivery_local`); `rate_kind` (callout / labour / travel / delivery / other); `category` (standard / lift / na); `timing` (normal / rush / afterhours / na); `in_shop` (bool); `product_id` (the `product.product` used on the SO line — for description, tax, income account); `price` (Monetary — the **editable source of truth**); `unit` (fixed / per_hour / per_km); `adds_per_km` (bool); `included_labour_min` (int, e.g. 30); `active`; `sequence`; `currency_id`.
- **Seed** (`data/service_rate_data.xml`, `noupdate=1`): one row per §3 rate, each linked to a seeded `product.product` (type `service`, `sale_ok`, correct UoM — hour/km/unit, HST). `noupdate=1` means a later `-u` never overwrites admin price edits.
- **Views/menu:** list + form under *Field Service → Configuration → Service Rates* (manager-only) — edit price, add/remove rows, toggle `active`.
- **Products still exist** (SO lines + accounting need a product), but the **rate row's `price` is the source of truth** — the SO line takes `price_unit` from the rate, not the product's `list_price`. One place to edit.
- The **wizard builds its service-type selector from the active `callout` rows**, so a new rate row appears in the wizard automatically.
### 6.2 `fusion_tasks` — `fusion.technician.task`
- Make `_compute_datetimes` and `_inverse_datetime_start/_end` use **one** tz resolver (`_get_local_tz()` everywhere) so compute and inverse agree; document that local float hours ↔ UTC datetime is the single source of truth.
- Time entry stays `time_start`/`time_end` floats (local); the **AM/PM presentation lives in the OWL wizard**; the existing `time_start_display` (12h) already covers list/kanban/calendar.
### 6.3 `fusion_claims` — `fusion.technician.task` + `sale.order`
- **Relax** `_check_order_link`: no longer raise when there is no SO/PO — the wizard now auto-creates the SO, and in-shop/walk-in tasks may legitimately have none. (Keep the helper that auto-fills address from an SO when one *is* linked.)
- Add `x_fc_service_call_type` (Selection: standard/lift × normal/rush/afterhours, + in_shop) on the task, set by the wizard, used to pick the call-out product and for reporting.
- Add a **pricing resolver** that reads `fusion.service.rate`: `_get_callout_rate(category, timing, in_shop)` and `_get_rate(code)` (per-km, labour, delivery) + `_build_service_so(partner, rate, distance_km, ...)` that creates the SO + lines using each rate's `product_id` with `price_unit` taken from the rate row.
- **Repair-SO identity:** boolean `x_fc_is_service_repair` on `sale.order` + an `crm.tag`/SO tag "Service Repair" so these orders are filterable; reuse the standard quotation flow.
---
## 7. Pricing engine
- Reads the **`fusion.service.rate`** table (§6.1) — never hardcoded.
- `_get_callout_rate(category, timing, in_shop)` → the matching active `callout` row (none if in-shop). Its `price` → the SO call-out line `price_unit`; its `product_id` → the line product.
- **Per-km:** when the call-out row's `adds_per_km` is set, add a line from the `per_km` rate row, qty = `round(distance_km × 2)`, `price_unit` = that row's price.
- **On-screen estimate (UI only, not written to SO):** `callout.price + max(0, duration included_labour_min/60) × labour_rate + per-km`, where `labour_rate` is read from the `labour_*` rate rows (in-shop / on-site / lift).
---
## 8. Timezone fix (folds in the audit finding)
Single resolver `_get_local_tz()` (company resource-calendar tz → user tz → UTC) used by **both** `_compute_datetimes` and the inverses, eliminating the compute/inverse mismatch and the silent UTC fallback. Booking writes local float hours; datetime_start/end (UTC) recompute consistently for the calendar/sync.
---
## 9. Open decisions (defaults chosen — confirm at review)
| # | Decision | Default |
|---|---|---|
| 1 | Lift Rush / After-Hours call-out | **$185 / $205** (parallel surcharge) |
| 2 | In-shop pricing | no call-out, labour @ $75/hr, no delivery |
| 3 | Repair-SO identity | boolean `x_fc_is_service_repair` + SO tag "Service Repair" |
| 4 | Estimate labour | on-screen guide only; SO = call-out + per-km; labour/parts as actuals |
| 5 | Per-km distance basis | Distance Matrix, shop/previous-task → job, ×2-way |
| 6 | Rate configurability | editable `fusion.service.rate` table + **Service Rates** menu; the card only seeds it, admin-owned thereafter |
---
## 10. Testing & rollout
- Enterprise-only stack (these modules need `fusion_claims`/`fusion_portal` deps) → **verify on a Westin clone**, not local Community.
- Seed products + taxes; smoke-test: new-client booking → contact + task + draft SO created with the right call-out (+ per-km on rush/after-hours); existing-customer booking; in-shop (no call-out); tz correctness on the task + calendar; dark + light bundles.
---
## 11. Build sequence (for the implementation plan)
1. **`fusion.service.rate` model** + seeded rows + products + taxes + *Service Rates* menu/views.
2. **TZ fix** + confirm AM/PM round-trips (time floats).
3. **Constraint relax** + `x_fc_service_call_type` + pricing resolver + `_build_service_so` + `action_book_from_wizard` (server).
4. **OWL wizard** client action + SCSS (dark/light).
5. **Entry point** (button/menu/tile) + `ir.actions.client`.
6. **Clone-verify** end-to-end.