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

13 KiB
Raw Blame History

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 & Pricingdevice 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.
  • Locationin-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_tasksfusion.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_claimsfusion.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.