Commit Graph

8 Commits

Author SHA1 Message Date
gsinghpal
f463600585 feat(fusion_repairs): Bundle 5 - M5 pre-paid service plans + burn-down
New models
- fusion.repair.service.plan.subscription
  Tracks pre-paid maintenance packages: partner, plan product, optional
  category restriction, visits_included / visits_used / visits_remaining,
  start_date / end_date, computed state (active/exhausted/expired/cancelled),
  burn_history One2many. PLAN-NNNNN sequence.
- fusion.repair.service.plan.burn
  One row per maintenance visit that consumed a plan visit - feeds the
  Burn History tab on the subscription form.

product.template extensions
- x_fc_is_service_plan boolean toggle
- x_fc_plan_visits_included (default 4)
- x_fc_plan_duration_months (default 12)
- x_fc_plan_category_id - if set, only burns for repairs in that category
  (e.g. an Annual Stairlift Maintenance plan does not burn for wheelchair
  repairs)

sale.order.action_confirm() override
- For each order line whose product has x_fc_is_service_plan=True,
  spawns one fusion.repair.service.plan.subscription per qty unit.
- Start date = today; end date = today + plan_duration_months
  (relativedelta - correct month boundaries).

Visit report wizard
- New _burn_service_plan_visit(repair) call from action_confirm() finds
  the matching active subscription and burns one visit + posts a chatter
  note "Visit burned for repair X. N of M remaining." on the subscription.
- Skips quote-only repairs.
- The wizard does NOT zero out the invoice - the burn is informational;
  the office reconciles plan credits in their accounting workflow.

Backend
- Service Plans menu under Fusion Repairs root.
- List view colour-coded by state.
- Form with statusbar + cancel button + Burn History notebook.
- Service Plan tab added to product.template form (manager only).
- ACL: User read; Dispatcher write/create; Manager full + unlink.

Verified end-to-end on local westin-v19:
  Created plan product 'Annual Stairlift Maintenance - 4 Visits'
  Sold it via sale.order -> PLAN-00001 auto-created
  (visits_included=4, end_date=2027-05-21)
  Submitted visit-report on a stairlift repair -> visits_used=1
  remaining=3 (correctly category-matched).

Bumped to 19.0.1.5.0.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 00:19:28 -04:00
gsinghpal
65c4d8801c feat(fusion_repairs): Bundle 4 - M1 compliance inspection certificates
New fusion.repair.inspection.certificate model for the annual safety
inspections required on stairlifts, porch lifts, and power wheelchairs
in many jurisdictions.

Model
- mail.thread chatter-tracked; fields: name (CERT-YYYY-NNNN auto-seq),
  partner_id, product_id (filtered to safety-critical categories), lot_id,
  repair_order_id back-link, inspector_user_id (must be field staff),
  jurisdiction (selection: Ontario / BC / Alberta / Quebec / Other),
  issued_date, valid_for_months (default 12), expiry_date (computed,
  stored, uses relativedelta - correct month boundaries), status
  (non-stored compute: valid / expiring / expired / revoked), revoked,
  notes, last_reminder_band.
- Unique constraint on certificate number (models.Constraint, not
  _sql_constraints, per project rule).
- Sequence 'fusion.repair.inspection.certificate' with use_date_range=True
  so the counter resets each year (CERT-2026-0001 ... CERT-2027-0001).

Visit report integration
- New issue_inspection_cert checkbox on fusion.repair.visit.report.wizard.
- When ticked AND the repair's category is safety_critical, action_confirm()
  creates the certificate via _create_inspection_certificate() and
  redirects to the cert form so the tech can print immediately.
- Non-safety-critical equipment quietly skips with a chatter note
  explaining why.

PDF report
- web.html_container + web.external_layout, model bound so it appears
  as a Print action on the certificate form.
- 'Certificate of Inspection' / 'Safety Inspected' gold-banner layout
  with client name, equipment, serial, jurisdiction, issued + expiry
  dates, inspector signature line, and the certificate number.
- Print Certificate button in form header.

Daily cron
- cron_send_expiry_reminders runs at 09:00, sends two band-tracked
  reminders (30 days + 7 days before expiry) to the client.
- New mail.template email_template_inspection_expiry_reminder with
  4px amber accent, certificate ref, equipment, expiry date, and a
  CTA to call to book the re-inspection visit.
- last_reminder_band on the cert prevents re-sending the same band.

Backend wiring
- New menu entry 'Fusion Repairs > Inspection Certificates'.
- ACL: User read, Dispatcher write, Manager unlink. Field technicians
  can create (they need to issue from the field).
- List view with red/amber/green status decoration.
- Form with statusbar, header buttons (Print, Revoke with confirm),
  chatter.

Verified end-to-end on local westin-v19:
  Stairlift repair RO-202605-15 -> visit-report with issue_inspection_cert=True
  -> CERT-2026-0001 issued (status=valid, expires 2027-05-21)
  Cert CERT-2026-0002 expiring in 30 days -> cron flagged
  last_reminder_band='30' (would email client).

Bumped to 19.0.1.4.0 (minor bump for the new public-facing capability).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 00:11:59 -04:00
gsinghpal
5c8768c556 feat(fusion_repairs): Bundle 2 - weekend self-service (CL6/CL7 + CL15 + CL17)
CL6/CL7 AI self-check engine
- New fusion.repair.ai.service AbstractModel with single guardrailed
  suggest_self_check(category_id, symptoms, urgency) entry point.
- Hard-escalation FIRST (before any AI call): stairlift / porch lift +
  safety symptoms (smoke / burning / spark / stuck / motor), OR any
  mention of fire / injury / hurt / bleeding / trapped, OR urgency=safety
  -> escalate immediately regardless of AI availability.
- AI call via fusion.api.service.call_openai() (consumer='fusion_repairs',
  feature='client_self_triage') with try/fallback per project rule -
  no hard fusion_api dep, no install error if it's missing.
- Strict response validation: JSON schema check, max 3 steps, max 200
  chars per field, forbidden-phrase regex (diagnose, you have, medical
  condition, stop using, consult doctor, price patterns) - on any
  failure falls back to deterministic rules.
- 24h in-memory cache keyed by (category, symptom_hash) so repeat calls
  during AI cost-cap incidents come from cache.
- System prompt + JSON schema published as ir.config_parameter so office
  can refine without code changes (default prompt + schema in spec
  Appendix A).
- New fusion.repair.self.check.rule model + 17 seeded rules across all
  7 product categories (data/self_check_data.xml) - these are the
  deterministic fallback AND the canonical seed if AI is disabled.
- New /repair/self_check jsonrpc route (auth=public) gated by the
  per-IP rate-limit; defensive input bounds (max 5 symptoms, 500 chars
  each) defend against prompt-injection bloat.

CL15 weekend safety escalation + on-call paging
- New fusion.repair.on.call.service AbstractModel with:
  * find_next_on_call(exclude=...) -> lowest x_fc_on_call_priority
  * page_on_call(repair) -> sends mail to next available + writes
    x_fc_on_call_token / x_fc_on_call_paged_user_id / paged_at on the
    repair, posts chatter
  * acknowledge(repair, user) -> records ack, posts chatter
  * cron_escalate_unacknowledged() -> every 5 min, re-pages the next
    priority for repairs paged >15 min ago without ack
- Auto-fires from intake service whenever x_fc_urgency='safety' is
  submitted. _is_business_hours() defaults to "page" when no calendar
  is set or after working hours.
- New email_template_on_call_page with 4px red accent + acknowledge
  CTA button linking to /repair/on-call/ack/<token>.
- /repair/on-call/ack/<token> http route (auth=user, must be the paged
  manager OR any internal user) records the ack and renders confirmation.
- 5-minute cron 'Fusion Repairs: Escalate unacknowledged on-call pages'
  with configurable window via fusion_repairs.on_call_escalate_minutes
  (default 15).
- New repair.order fields x_fc_on_call_token, x_fc_on_call_paged_user_id,
  x_fc_on_call_paged_at, x_fc_on_call_acknowledged_user_ids,
  x_fc_on_call_acknowledged_at - all copy=False so duplicates start fresh.

CL17 QR sticker generator
- New fusion.repair.qr.sticker.wizard TransientModel takes a Many2many
  of stock.lot records (optionally filtered by product).
- QWeb PDF report fusion_repairs.report_qr_stickers prints a 4-up
  sticker sheet on letter paper: 80mm x 50mm per sticker with the
  QR code (38mm), product name, serial number, and the canonical
  portal URL (from web.base.url + fusion_repairs.client_portal_url).
- QR encodes /repair?sn=<serial> which the public client portal
  already pre-fills via the ?sn= query param.
- Uses the qrcode library if available; renders 'QR lib missing'
  placeholder otherwise so the PDF still prints.
- New menu Configuration > Generate QR Stickers + standalone wizard.

Verified end-to-end on local westin-v19:
  CL6 stairlift+smoke -> escalate=True source=escalated reason=safety
  CL6 bed (no AI) -> fallback returned escalate=True (safe default)
  CL15 admin paged for RO-202605-10 with 27-char token
  CL17 sticker URL: /repair?sn=001124032521528404
       QR data URI: data:image/png;base64,iVBORw... (PNG OK)

Bumped to 19.0.1.2.0 (minor bump - new public-facing capabilities).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 23:40:52 -04:00
gsinghpal
38a79a4b04 feat(fusion_repairs): OWL dashboard - quick actions, KPIs, portal share
A real landing dashboard for the Fusion Repairs app so users see at a
glance what is open, what is urgent, and where to click. Built as an
OWL client action, theme-aware (light AND dark) at SCSS compile time,
zero hardcoded user-facing colours.

What's on it
- Hero banner with gradient accent
- 4 quick-action tiles (New Service Call, Service Calls, Maintenance
  Contracts, Repair Warranties)
- 6 KPI stat tiles (Open / Urgent+Safety / Awaiting Dispatch /
  Needs Re-Quote / New This Month / Maintenance Due 30d) - each is
  clickable and lands in the right filtered list
- Self-service portal cards with copy-to-clipboard for the public
  client portal URL and the sales rep portal URL (so office can
  share them on voicemail / printed materials / training)
- Recent Service Calls list (last 5) - click jumps to repair form
- Upcoming Maintenance list (next 5 due) - red pill when <=7 days out
- Configuration tiles (Equipment Categories / Intake Templates /
  Service Catalogue)
- Refresh button

Architecture
- fusion.repair.dashboard AbstractModel exposes get_dashboard_data():
  returns stats + urgency_breakdown + source_breakdown + recent[5] +
  upcoming[5] + portals (URLs resolved via web.base.url +
  fusion_repairs.client_portal_url)
- FusionRepairsDashboard OWL component (registry actions
  'fusion_repairs.dashboard') uses standalone rpc() per project rule
  #3, useService('action') for navigation, useService('notification')
  for copy feedback. static props = ['*'] to accept the client-action
  props envelope.
- _fr_tokens.scss registered FIRST in web.assets_backend so its
  variables are in scope when dashboard.scss compiles. NO @import (per
  project rule). Branches on $o-webclient-color-scheme at compile time
  so the dark bundle (web.assets_web_dark) gets dark hex values
  automatically - per project CLAUDE.md rule on dark mode.
- All visible colours come from CSS-variable-wrapped SCSS tokens
  (--fr-page-bg, --fr-card-bg, --fr-border, --fr-accent, ...) which
  fall back to the SCSS hex value. Three-layer contrast: page (grayest)
  -> card (mid) -> elevated (brightest).
- New ir.actions.client action_fusion_repairs_home_dashboard with
  tag='fusion_repairs.dashboard'.
- Top-level menu now lands on this dashboard. 'Dashboard' added as
  the first sub-menu; 'Service Calls' (the kanban) is still right
  below it.

Verified on local westin-v19:
  STATS: open=15, urgent=4, new_this_month=13, awaiting_dispatch=9,
         requires_requote=1, maintenance_due_30d=1, active_total=2
  PORTALS: client=http://192.168.139.165:8069/repair
           sales_rep=http://192.168.139.165:8069/my/repair/new
  RECENT count: 5
  UPCOMING count: 2
  SOURCE breakdown: backend_wizard 9, client_portal 3, manual 2, sales_rep_portal 1
  Web /web/login: 200, no SCSS compile errors in logs.

Bumped to 19.0.1.0.5 so the asset bundle hash refreshes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 22:58:06 -04:00
gsinghpal
afe19f2105 feat(fusion_repairs): sale.order smart buttons - repairs + maintenance
On the original purchase sale.order:
- Repairs button (fa-wrench) lists all repair.order records where
  x_fc_original_sale_order_id = this SO
- Maintenance button (fa-calendar-check-o) lists all
  fusion.repair.maintenance.contract records spawned from this SO
- Both auto-hide when count is zero
- Both gated by fusion_repairs.group_fusion_repairs_user

Follows the count + action_view_* + oe_stat_button / statinfo pattern
from fusion_claims/views/sale_order_views.xml line ~1176.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 22:02:12 -04:00
gsinghpal
73ee48e7c9 feat(fusion_repairs): Phase 3 - maintenance contracts + client self-booking
Maintenance contracts
- New fusion.repair.maintenance.contract model: one per partner +
  product + lot. Fields: interval_months, last_service_date,
  next_due_date, state, booking_token (secrets.token_urlsafe),
  last_reminder_band (30 / 7 / 1), booking_repair_id
- roll_next_due_date() advances the cycle by interval_months and resets
  the band / booked-repair so the next cycle starts fresh
- sale.order._spawn_maintenance_contracts() creates contracts for
  delivered SOs whose product has x_fc_maintenance_interval_months > 0
  (called from Phase 3 hooks; ready for cron / on-state change wiring)

Reminder cron
- Daily ir.cron at 07:00 -> cron_send_due_reminders()
- Sends email at 30 / 7 / 1 day bands before next_due_date; tracks
  last_reminder_band so we never re-send the same band in one cycle
- Master toggle via ir.config_parameter fusion_repairs.enable_email_notifications

Public client booking portal
- /repairs/maintenance/book/<token>  GET landing page with a date input
- /repairs/maintenance/book/<token>/confirm  POST creates a repair.order
  via contract.create_repair_from_booking() (source='client_portal')
- Idempotent: existing booking shows "already booked" instead of
  spawning a duplicate
- Invalid / expired tokens render a friendly "link not valid" page

Mail template
- email_template_maintenance_due_reminder with 4px green accent bar,
  600px max-width, dark/light safe; renders the tokenized booking CTA
  button directly to /repairs/maintenance/book/<token>

Backend
- Maintenance Contracts list / form with statusbar + chatter
- Menu under Operations -> Maintenance Contracts
- Sequence MC/##### for contract reference
- Access rules: User read, Dispatcher write, Manager full

Verified end-to-end on local westin-v19:
- Contract MC/00003 created due in 7 days
- cron_send_due_reminders() fires the 7-day band; second invocation
  skips (idempotent)
- create_repair_from_booking() spawns BR-WA/RO/00014 with
  x_fc_intake_source='client_portal' and links it back to the contract
- HTTP GET /repairs/maintenance/book/<token> -> 200 with the date input
  and contract reference visible in the page

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 22:01:30 -04:00
gsinghpal
7727745b73 feat(fusion_repairs): Phase 2 - service catalogue, visit report, warranty, Poynt
Service catalogue
- New fusion.repair.service.catalog model: named service entries per
  equipment category with symptom keywords, estimated hours / cost,
  default parts, auto_schedule flag, optional pricelist override
- find_best_match() scores candidates by symptom-keyword overlap against
  intake text hints (issue summary + category + notes)
- Intake service wires it in: on submit, the matcher sets
  x_fc_service_catalog_id + x_fc_estimated_duration + x_fc_estimated_cost
  and (when auto_schedule=True) creates a draft dispatch task
- Double-task guard: if catalogue match already created a task, the
  urgency-based dispatch skips so we never duplicate

Visit report wizard
- fusion.repair.visit.report.wizard with labour hours + parts lines +
  technician notes + 'found another issue' branch
- Computes actual cost = (labour x service_product.list_price) + parts
- Compares against estimate -> sets requires_requote when variance
  exceeds configured threshold (% or $); shows warning banner inline
- On confirm: writes actuals back to repair, posts notes to chatter,
  optionally spawns a follow-up repair (T5 'found another issue')

Repair warranty
- New fusion.repair.warranty.coverage model (start/expiry, partner,
  product, lot, active flag)
- find_active_for(partner, product, lot) returns the most-recent active
  coverage
- Intake service auto-checks: when a new repair lands on an equipment
  that has active warranty coverage, posts a chatter banner so the
  office knows the work may be free under our 30/90-day re-do policy
  (manager review still required; never auto-zeros pricing)

Repair form
- Header: Visit Report + Collect Payment buttons (gated by group)
- action_collect_payment looks up the linked posted unpaid invoice on
  the repair SO and opens the Poynt wizard (action_open_poynt_payment_wizard)

AI intake summary
- _generate_ai_summary calls self.env['fusion.api.service'].call_openai
  with consumer='fusion_repairs', feature='intake_triage'
- Strict system prompt: no medical advice, no diagnoses, no recommending
  stop equipment use; ~80 words; plain English
- Try/fallback per fusion-api-integration.mdc: if fusion_api not
  installed or call fails -> silently skip; intake never blocked

Verified end-to-end on local westin-v19:
- Stairlift motor intake -> catalogue match -> estimated $500/2h -> auto
  dispatch task (count=1, not duplicated)
- Visit report: 2.5h x $250 + $100 parts = $725 actual vs $500 estimated
  = 45% variance -> requires_requote=True
- Warranty: 30-day coverage on the completed repair; second repair on
  same partner triggers warranty banner in chatter

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 21:57:33 -04:00
gsinghpal
429084e0bf feat(fusion_repairs): Phase 1 MVP - backend intake wizard + core models
Scaffolds the fusion_repairs module that extends Odoo 19 repair.order with
a guided medical-equipment intake workflow.

Models
- fusion.repair.product.category (8 medical equipment categories seeded)
- fusion.repair.intake.template / .question / .answer (7 templates,
  32 questions seeded across hospital bed, stairlift, porch lift,
  wheelchair, walker/rollator, mattress)
- fusion.repair.intake.service (AbstractModel) - single entry point used
  by backend wizard, sales rep portal, and public client portal so all
  three surfaces produce identical outcomes
- repair.order extensions (x_fc_intake_*, x_fc_third_party_equipment,
  x_fc_photo_ids, x_fc_urgency, x_fc_estimated/actual_cost, AI summary)
- fusion.technician.task back-link (x_fc_repair_order_id)
- res.partner service preferences (preferred tech, time window, access notes)
- res.users repair extensions (skills, cost rate, on-call rotation fields)
- res.config.settings for variance thresholds, portal URL, rate limit

UI
- Backend intake wizard with multi-equipment loop, third-party flag, photos
- repair.order form: Intake tab, Photos, Pricing tab, AI tab, smart buttons
  (technician tasks, intake answers, original SO)
- Kanban + list view urgency badges
- Fusion Repairs app menu (New Service Call, Repair Orders, Config)

Activities & Email
- 4 follow-up activity types (CS callback, tech dispatch, visit follow-up,
  manager review) with urgency-tiered deadlines
- 2 mail templates (client confirmation + office notification) with the
  same dark/light-safe styling as fusion_claims ADP templates

Security
- New res.groups.privilege + 3 groups (User, Dispatcher, Manager)
- Reuses fusion_tasks.group_field_technician (do NOT recreate)
- Reuses fusion_authorizer_portal.group_sales_rep_portal
- Multi-company global rule + technician scoping rule on repair.order

Verified end-to-end on local westin-v19 dev DB via odoo-shell - creates
multiple repairs in one session, auto-creates dispatch task for urgent,
attaches 4 activity types correctly per urgency tier and third-party flag.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 21:35:52 -04:00