# 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.