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>
13 KiB
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:
- 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 exceptltc_visit. A repair for a brand-new client (no SO yet) is blocked. - Time fields: Start/End use a 24-hour
float_timewidget while every other view shows 12-hour AM/PM; and the local→UTC conversion is inconsistent (_compute_datetimesresolves company-calendar-tz → user-tz → UTC, but_inverse_datetime_*uses user-tz → UTC only — they disagree, and fall back to UTC when unset). - 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.ratetable (§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_codeLABOR) - 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 standalonerpc()(@web/core/network/rpc). Registered inregistry.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):- Resolve customer — search
res.partnerby email then phone; create if new (name/phone/email/address). For "existing", use the chosen partner/SO's partner. - 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. - Create a draft
sale.ordertagged 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. - Create the
fusion.technician.tasklinked to that SO (reuses existing modelcreate+ address-fill + travel-chain logic). - Return
{task_id, order_id}so the client action can open the task or close.
- Resolve customer — search
- SCSS
fusion_tasks/static/src/scss/_service_booking_tokens.scss+service_booking.scss, branching on$o-webclient-color-scheme(per repo rule), registered inweb.assets_backendandweb.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 seededproduct.product(typeservice,sale_ok, correct UoM — hour/km/unit, HST).noupdate=1means a later-unever 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
priceis the source of truth — the SO line takesprice_unitfrom the rate, not the product'slist_price. One place to edit. - The wizard builds its service-type selector from the active
calloutrows, so a new rate row appears in the wizard automatically.
6.2 fusion_tasks — fusion.technician.task
- Make
_compute_datetimesand_inverse_datetime_start/_enduse 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_endfloats (local); the AM/PM presentation lives in the OWL wizard; the existingtime_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'sproduct_idwithprice_unittaken from the rate row. - Repair-SO identity: boolean
x_fc_is_service_repaironsale.order+ ancrm.tag/SO tag "Service Repair" so these orders are filterable; reuse the standard quotation flow.
7. Pricing engine
- Reads the
fusion.service.ratetable (§6.1) — never hardcoded. _get_callout_rate(category, timing, in_shop)→ the matching activecalloutrow (none if in-shop). Itsprice→ the SO call-out lineprice_unit; itsproduct_id→ the line product.- Per-km: when the call-out row's
adds_per_kmis set, add a line from theper_kmrate 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, wherelabour_rateis read from thelabour_*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_portaldeps) → 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)
fusion.service.ratemodel + seeded rows + products + taxes + Service Rates menu/views.- TZ fix + confirm AM/PM round-trips (time floats).
- Constraint relax +
x_fc_service_call_type+ pricing resolver +_build_service_so+action_book_from_wizard(server). - OWL wizard client action + SCSS (dark/light).
- Entry point (button/menu/tile) +
ir.actions.client. - Clone-verify end-to-end.