4f1b7c2df609bc25e98ccc3df3f678f6b6f27c56
17 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4f1b7c2df6 |
fix(fusion_repairs): persona-driven workflow audit - 6 real bugs
Full end-to-end walk acting as customer, CS rep, dispatcher, technician,
and manager surfaced 6 real bugs (1 critical state-machine, 4 missing UX
wires, 1 docstring). Server endpoints existed for everything but several
were not wired into the templates.
B1 (HIGH) - Visit-report wizard never closed the repair
Tech submitted visit -> state stayed 'draft' -> x_fc_done_at never
stamped -> NPS cron never fired -> the whole post-visit flow died
silently. Customers never got their NPS email.
Fix: action_confirm() now drives the Odoo native state machine
draft -> action_validate (with _action_repair_confirm fallback) ->
action_repair_start -> action_repair_end. Each step guarded by the
current state and exception-logged. Leaves the repair open if:
- requires_requote=True (variance flag - office must re-quote)
- no_show=True (office reschedules)
- x_fc_is_quote_only (still a quote)
- found_another_issue spawned a stub
Posts a clear chatter line on success or failure.
Verified: e2e walk now shows state=done + x_fc_done_at stamped +
NPS cron fires + flags x_fc_nps_email_sent=True.
B2 (HIGH) - /repair/new form never called /repair/self_check
The AI self-check engine was the headline weekend feature but it was
invisible to the client. The endpoint worked server-side, just had
no frontend.
Fix: new portal_client_repair.js (Interaction class, registered on
registry.category('public.interactions')). 'Try 1-3 safe self-check
steps first' button POSTs to /repair/self_check, renders steps via
createElement + textContent (no innerHTML - all server output is
treated as untrusted text). Shows the AI's safety disclaimer on
every result. On escalate_immediately, shows a clear 'submit the
form, we'll come to you' message instead of the steps.
Verified: HTTP POST returns full JSON with instruction +
expected_result + disclaimer; new button + result panel appear in
rendered HTML.
B3 (HIGH) - No phone-lookup UI for returning clients
Same problem - endpoint existed but no UI. Returning clients had to
retype everything from scratch.
Fix:
- lookup_phone now returns a 'partners' array (id, name, email,
street, city) - cap of 3 results, rate-limited, every match logged
at INFO level for audit. Privacy compromise: a phone holder
deserves to see their own pre-fill; rate limit caps harvesting.
- JS lookup widget at the top of the form posts to /repair/lookup_phone
and pre-fills the 5 contact fields + writes the partner_id to a
hidden #fr_known_partner_id input.
- controller /repair/submit now trusts known_partner_id if present
(skips the phone re-match) so we don't create duplicate partners
when the lookup widget already identified the right one.
Verified: HTTP POST returns the 2 partner records we have for
+19055551234 with full id/name/email/street/city.
B4 (MEDIUM) - /repair?sn=<serial> from QR sticker did nothing
Spec: 'Client scans QR sticker - portal pre-fills the unit info.'
Reality: the form had no serial field; ?sn= was ignored.
Fix: new _resolve_serial_info(serial) on the controller resolves
the lot via stock.lot.search([('name','=',sn)]) and returns
{serial, lot_id, product_id, product_name, category_id}. Both
/repair (landing) and /repair/new pass it as serial_info template
context. Templates show 'Recognized X (Serial: Y)' + auto-select
the matching category in the dropdown. Hidden #fr_serial_number
carries it through to /repair/submit, which attaches the lot_id +
uses the QR category as fallback if user didn't pick one.
Verified: ?sn=stella23-20040164 produces 'Pre-filled from QR scan:'
banner + hidden input populated.
B5 (MEDIUM) - No upsell after submit
Spec required an upsell - 'reduce future calls'. Page was a bare
'Got it'.
Fix: /repair/thanks now shows a 2-card layout:
- 'Want to avoid this next time?' with 4 bullets (priority booking,
free inspection cert, discounted parts, annual reminder) +
'See our maintenance plans' CTA to /shop?category=maintenance
- 'What happens next' 4-step bulleted explanation
Verified: both cards render.
B6 (LOW) - SyntaxWarning '\-->' in repair_service_plan.py
Made the module docstring a raw string (r''') so the ASCII flowchart
arrows don't trigger Python's invalid-escape-sequence warning.
Bumped to 19.0.1.8.0.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
b4b59cc3c9 |
feat(fusion_repairs): Bundle 7 - tech mobile (T3 + T4 + T6 + T7)
T3 Labour timer on technician task - Two new fields on fusion.technician.task: x_fc_timer_running_since (Datetime) + x_fc_timer_accumulated_minutes (Float). - action_timer_start / action_timer_stop methods, idempotent (start when already running is a no-op, stop when not running is a no-op). - Multiple start/stop cycles accumulate into the same total. - Two header buttons (Start Timer green / Stop Timer amber), invisible based on the running_since field so the right one shows at any time. - Stop posts a chatter line 'Labour timer stopped. Added X.X min, total Y.Y min.' so audit history shows every shift. T4 Client signature on visit report - New client_signature Binary field on the visit-report wizard with Odoo native widget='signature' that draws on canvas + base64-encodes the PNG. - client_signature_name Char for typed name (audit). - Persisted as an ir.attachment on the repair.order via the new _persist_mobile_artefacts helper. - Chatter post 'Client signature captured (Jane Smith).'. T6 Replaced parts - serial capture - parts_serial_capture Text on the wizard (one per line per the spec). - On confirm, posted to chatter wrapped in <pre> so line breaks survive. - Used by OEM warranty filing in future M8. T7 Client no-show photo proof - no_show Boolean + no_show_photo Binary with widget='image' (visible only when no_show=True via Odoo 19 invisible= conditional). - Photo saved as ir.attachment on the repair when present. - Chatter post 'Visit recorded as client no-show (photo attached)'. Verified end-to-end on local westin-v19: T3 timer started -> 2s sleep -> stopped -> 0.0357 min recorded T4 attachment 'signature-RO-202605-17.png' created on repair T6 chatter shows 'SN-AAA-111 / SN-BBB-222' T4 chatter shows 'Client signature captured (Jane Smith)' Bumped to 19.0.1.7.0. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
638b223d3b |
feat(fusion_repairs): Bundle 6 - M7 failure analytics + M9 margin per repair
M9 margin per repair
- New non-stored computes on repair.order: x_fc_revenue, x_fc_labour_cost,
x_fc_parts_cost, x_fc_margin, x_fc_margin_pct.
- Revenue: sum of posted out_invoice.amount_untaxed on the repair's sale
order (handles partial / multi invoice scenarios).
- Labour: sum of (task.duration_hours x technician.x_fc_tech_cost_rate)
over COMPLETED visits only - avoids counting scheduled-but-not-done time.
- Parts: sum of standard_price x qty for stock moves where
repair_line_type='add' (parts consumed, not removed).
- New 'Margin' notebook tab on repair.order form, manager-group gated.
M7 failure analytics on the dashboard
- Three new keys in get_dashboard_data():
* failures_by_product - top 8 products by repair_count in last 90 days
via _read_group (efficient - no record load)
* failures_by_symptom - top 8 x_fc_issue_category values
* margin_summary - revenue/labour/parts/margin/margin_pct + sample_size
over the same 90-day window
- Three new tiles on the OWL dashboard 'Last 90 Days' section:
Margin Summary (revenue/labour/parts/margin breakdown),
Failure Rate by Product, Failure Rate by Symptom.
- New formatMoney + formatPercent helpers on the dashboard JS so values
display as 'CAD 12,345' rather than raw floats.
Verified end-to-end on local westin-v19:
Dashboard returned all 9 expected keys.
Top product: 'M6 X 27 THREADED BARREL' (2 repairs) - actual test data.
Margin summary over 26 repairs (dev has $0 invoices so values 0.0,
but the compute path is exercised and shapes are correct).
Bumped to 19.0.1.6.0.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
c506b53dec |
feat(fusion_repairs): Bundle 3 - reminders + upsells (X2 + X4 + M3)
X2 Day-before visit reminder email
- New cron 'Fusion Repairs: Day-before visit reminders' (daily at 08:00)
walks repair.order records with at least one linked
fusion.technician.task scheduled for tomorrow and not yet reminded.
- Sends mail.template email_template_visit_day_before to the client.
- New x_fc_day_before_reminder_sent flag (copy=False) so the cron
never re-sends the same reminder.
- Template uses 4px blue accent, 600px max-width, shows the scheduled
date + technician name + equipment, with a 'reply to reschedule' note.
- Verified: cron flagged the test repair x_fc_day_before_reminder_sent=True
after running.
X4 Post-visit NPS / Google review email
- New cron 'Fusion Repairs: Send post-visit NPS emails' (hourly)
finds repairs in state='done' with write_date >= 24h ago and no NPS
email sent. Sends mail.template email_template_post_visit_nps.
- New x_fc_nps_email_sent flag so we never re-pester clients.
- Template uses 4px green accent + 'Leave a Google review' CTA button
linking to res.company.x_fc_google_review_url (or a sensible Google
search fallback when the company hasn't configured a review URL).
M3 Loaner auto-offer for long-running repairs
- Soft-bridges fusion_loaners_management without a hard dep -
cron_offer_loaner_for_long_repairs returns immediately if the
fusion.loaner.checkout model isn't installed.
- Walks repair.order records open longer than
fusion_repairs.loaner_offer_threshold_days (ICP, default 3 days)
with no existing loaner-offer activity.
- Posts a 'Repair: Offer Loaner' activity (new mail.activity.type)
assigned to the repair responsible.
- New x_fc_loaner_offered flag to prevent daily re-posting.
- Manual 'Offer Loaner' button on repair header opens the
fusion.loaner.checkout wizard pre-filled with partner + SO.
- Daily cron runs at 08:30.
Email + ICP + cron wiring:
- 2 new mail.template records (visit_day_before, post_visit_nps)
- 1 new mail.activity.type (loaner_offer)
- 3 new ir.cron records (day-before, NPS, loaner)
- 1 new ir.config_parameter (loaner_offer_threshold_days)
- 1 new header button (Offer Loaner) on repair.order
Verified end-to-end on local westin-v19:
X2 setup repair: RO-202605-12 task: TASK-00045
day-before flag after cron: True (expected True)
M3 loaner model not installed - cron correctly no-op'd
(no flag set, no activity posted, no error - the soft-dep guard works)
Bumped to 19.0.1.3.0.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
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>
|
||
|
|
3a15164605 |
fix(fusion_repairs): Bundle 1 code-review fixes (H1-H5 + M1-M6)
H1 Float -> Monetary for outstanding_balance
Added currency_id companion field on the wizard so widget="monetary"
renders properly. Currency defaults to env.company.currency_id.
H2 Maps URL address duplication
fusion_tasks address_street often contains the full Google-Places-
formatted address. Concatenating address_street + address_city + zip
was producing "15 Fisherman Dr, Brampton, ON L7A 1B7, Canada, Brampton,
L7A 1B7". Now uses the existing address_display field (fusion_tasks
computes it correctly for both Google Places and manual entries), with
a partner-based fallback that includes street, street2, city,
state_id.name, zip, country_id.name.
H3 Banner copy hardcoded "14 days"
Added duplicate_window_days compute field; banner now reads
"in last <N> days" from the ir.config_parameter.
H4 Outstanding-balance multi-company + child_of direction
- Dropped .sudo() (CS users already have access to their own company's
invoices via standard groups + the Repairs Office rule)
- Replaced child_of (which only walks descendants) with
commercial_partner_id (the canonical Odoo "billed-to root" - covers
child contacts AND walks up from a child if the caller IS a child)
- Added ('company_id', 'in', env.companies.ids) filter to both the
invoice search AND the duplicate-repair search so a CS rep in
Westin Healthcare doesn't see NEXA Systems balances
H5 duplicate_count capped at 5 (false reassurance)
Now uses search_count for the true total + search(limit=5) for the
display list. Earlier verification showed count=5 was actually
capped; running again shows 15 for the same partner.
M1 Function-level imports
Moved urllib.parse.quote_plus and odoo.exceptions.UserError to module
top in technician_task.py.
M2 Many2many 'in' with scalar
Changed ('x_fc_repair_skills', 'in', category.id) to
('x_fc_repair_skills', 'in', [category.id]) - safer against future
ORM tightening.
M4 C6 - added x_fc_is_quote_only field + filter + form indicator
Boolean tracked field on repair.order (was previously discoverable
only via chatter text). Indexed. Visible on the form's intake metadata
row and filterable on the dashboard search view as "Quote Only".
M5 Account-move read perf
Replaced Move.search() + Python sum with _read_group(
aggregates=['amount_residual:sum', '__count']) - pushes the SUM to
Postgres; O(1) record load vs O(N).
M6 Hide Maps button when no address
Added invisible="not address_display and not partner_id" on the
Open in Maps button so it doesn't appear on in-store tasks.
Plus the dispatch-task cutoff is now a datetime (was a date) so the
create_date >= cutoff comparison is type-correct.
Verified end-to-end on local westin-v19 after fixes:
C1 count: 15 (was capped at 5) window_days: 14
C5 balance: 0.0 currency: CAD warning: False (correct)
C6 x_fc_is_quote_only: True tech_tasks: 0 (urgent intake, NOT dispatched)
T1 URL: https://www.google.com/maps?q=15+Fisherman+Dr%2C+Brampton%2C+ON+L7A+1B7%2C+Canada%2C+Unit+7
(no duplicated city/zip)
Bumped to 19.0.1.1.1.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
194850e3cf |
feat(fusion_repairs): Bundle 1 - wizard polish (C1 + C5 + C6 + D2 + T1)
C1 duplicate-call detection - Wizard computes duplicate_count + duplicate_repair_ids when partner is picked (open repairs from the configurable window, default 14 days). - Yellow banner with "Open Existing Repair" button to jump to the most recent duplicate so CS can add a note instead of creating a new repair. C5 outstanding-balance warning - Wizard sums posted unpaid account.move.amount_residual across all invoices of the partner. - Red banner shown when balance >= fusion_repairs.outstanding_balance_threshold (default $100) with a "View Invoices" button. C6 quote-only mode - New quote_only boolean on the wizard; passed through the shared intake service. Skips dispatch-task creation for urgent/safety AND for catalogue auto_schedule. Chatter note "Created in Quote Only mode" posted on the resulting repair.order. D2 skills filter on dispatch picker - _pick_dispatch_technician(repair) prefers users whose x_fc_repair_skills Many2many contains the repair's product category. Three-tier preference: 1) intake user if field staff AND has the skill 2) any active field-staff user with the skill 3) any active field-staff user (no skill filter) - last-resort - Logs a warning + skips task creation if no field-staff user exists at all. T1 Open in Maps on technician task - action_open_in_maps() returns ir.actions.act_url to https://www.google.com/maps?q=<URL-encoded address>. Deep-links into Apple Maps / Google Maps native apps on iOS / Android, browser otherwise. - Header button added on the fusion.technician.task form (after the existing buttons) plus a "View Repair" button when x_fc_repair_order_id is set. Verified end-to-end on local westin-v19: Existing repair: RO-202605-06 C1 duplicate_count = 5 (>=1 expected) - last duplicate: RO-202605-06 C5 balance check ran without error (target partner had $0) C6 quote-only repair: RO-202605-07 tech_tasks = 0 (expected 0) D2 picked the only stairlift-skilled field-staff user T1 Maps URL: https://www.google.com/maps?q=15+Fisherman+Dr%2C+Brampton%2C+ON+L7A+1B7%2C+Canad... Bumped to 19.0.1.1.0. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
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> |
||
|
|
750c7068e2 |
fix(fusion_repairs): activity-create access error + dashboard landing
Two complaints from the first hands-on test: 1) Submit button raised "Access Error (Document type: Activity, Operation: create)" - the wizard called the intake service WITHOUT sudo so the mail.activity records the service schedules tripped on the activity ACL (admin's group chain does not auto-grant activity create on repair.order without sudo). Both portal controllers already sudo'd; the wizard now does too. x_fc_intake_user_id preserves audit identity regardless. Verified end-to-end as gsingh@westinhealthcare.com (admin): Created: BR-WA/RO/00025 Activities: 2 Source: backend_wizard Intake user: gsingh@westinhealthcare.com 2) "Real dashboard with dedicated pages would have been nice" - the main menu opened the wizard directly as a modal. Restructured so the menu lands on a proper kanban dashboard of service calls, matching the standard Odoo app pattern: Fusion Repairs (app icon) - Service Calls <- dashboard kanban (default landing) - New Service Call <- wizard (still a modal, accessed from menu OR kanban's New button) - All Repair Orders <- native Odoo repair list (full backend) - Maintenance Contracts - Configuration - Equipment Categories / Intake Templates / Service Catalogue / Repair Warranties New view_fusion_repair_dashboard_kanban shows urgency badges (red / amber / grey), category, scheduled date, intake source pill, and a 3rd-party warning. Default group_by=state. New view_fusion_repair_dashboard_search adds quick filters: Today, This Week, Safety/Urgent, Third-Party, Open, plus per-source filters and Group By (Status / Urgency / Category / Intake Source). Wizard remains target='new' (modal) so submitting drops the user back to the kanban they came from with the new repair visible. Bumped version to 19.0.1.0.2 to bust the asset bundle hash. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
c86f1bbbe5 |
fix(fusion_repairs): code-review batch - 4 critical + 8 high + 8 medium/low
Critical - C1: _sql_constraints -> models.Constraint (Odoo 19 deprecation rule violation) - C2: variance threshold no longer uses abs() - under-cost is good news, must not block invoicing. Now only OVER-cost triggers requires_requote. - C3: roll_next_due_date() was dead code - now wired from fusion.technician.task.write() when a maintenance task transitions to 'completed', so the whole maintenance lifecycle actually advances. - C4: warranty.is_active was store=True but time-dependent (became stale). Dropped store=True; find_active_for() now filters by expiry_date directly. High - H1: added x_fc_maintenance_contract_id back-link on repair.order and populated it from create_repair_from_booking(). - H2: find_active_for() returns empty when neither lot nor product is supplied - prevents cross-product false warranty matches. - H3: visit-report wizard now creates stock.move records of repair_line_type 'add' for each part line, so Odoo's native action_create_sale_order() chain has lines to invoice and stock gets consumed properly. - H4: office intake email template now carries a fallback email_to header computed from res.company.x_fc_office_notification_ids (or company email), so it does not silently send with no recipient. - H5: maintenance reminder cron nextcall now always rolls to tomorrow at 07:00 local time, so installing/upgrading after 07:00 does not immediately fire all the day's reminders. - H6: public portal no longer hardcodes UID 1 as the intake user fallback (which in Odoo 19 is OdooBot). Prefers base.user_admin, else the lowest-id non-share user, else SUPERUSER_ID. - H7: public portal validates client_email via tools.email_normalize before partner creation; malformed addresses redirect with error=email. - H8: find_best_match() returns empty when no symptom keywords match (no silent first-catalog guess) and uses word-boundary regex to avoid matching 'battery' inside 'no battery problem'. Medium - M1: _inherit moved next to _name on maintenance_contract (cosmetic but brittle if Odoo refactors model class detection) - M2: relativedelta(months=N) instead of timedelta(days=N*30) for warranty and maintenance intervals (correct month boundaries) - M3: unique constraint on fusion.repair.maintenance.contract.booking_token - M6: dispatch task fallback now searches for an actual x_fc_is_field_staff user; gracefully skips and logs if no field staff exists (instead of silently failing the constraint check) - M7: maintenance contract list view date decoration uses context_today() (date) instead of strftime(string) - the str comparison would TypeError - M9: Visit Report button hidden on draft repairs and when no technician task is linked yet Low - L2: portal-created partners get default lang + company_id so mail templates render in the right language - L3: dropped unused exception variable in sales rep portal controller - L4: visit-report wizard 'found another issue' now redirects to the spawned stub repair so the tech can fill it in immediately - L5: dropped unrecognized data-string from <app> in settings view Public portal also: rate-limit check moved BEFORE the counter increment so blocked attempts do not keep inflating the bucket. All fixes verified end-to-end on local westin-v19: - variance one-sided: 0.5h labour vs $500 est -> requires_requote=False; 2h x $250 + $200 parts vs $100 est -> requires_requote=True - maintenance roll-forward: created MC/00006 due 2026-05-31, completed linked maintenance task -> contract rolled to 2026-11-21 with last_reminder_band reset - warranty find_active_for(partner only) -> empty recordset - service catalog find_best_match with unrelated text -> empty recordset - pg_constraint shows fusion_repair_maintenance_contract_booking_token_unique - /repair landing still 200 after restart Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
ad553b1082 |
feat(fusion_repairs): Phase 1 sales rep + public client portals
Both portals share the existing fusion.repair.intake.service so behaviour
stays identical across all three intake surfaces (backend wizard,
sales rep portal, public client portal).
Sales rep portal
- Hard depends on fusion_authorizer_portal (reuses is_sales_rep_portal
flag + group_sales_rep_portal scaffolding)
- /my/repair/new - mobile-friendly intake form with phone-first
partner search (jsonrpc lookup), category select, third-party flag,
urgency, photo capture
- /my/repairs - list of repairs the rep submitted (paginated)
- /my/repair/<id> - read-only detail with status, equipment, scheduled
visit
- Interaction-class JS (Odoo 19 public.interactions), safe DOM construction
- Mobile SCSS with 44px tap targets, sticky CTA on small screens
- Record rule scopes portal users to repairs where
x_fc_intake_user_id = user.id
Public client portal
- auth='public' - voicemail-ready /repair URL
- /repair - landing page with 911 disclaimer and Start CTA
- /repair/new - single-page form: contact, equipment, issue, urgency,
optional photos. QR pre-fill via ?sn=<serial>
- /repair/submit - CSRF + honeypot + per-IP rate limit (configurable);
finds or creates partner; calls intake service with sudo
- /repair/thanks - confirmation with reference number
- /repair/lookup_phone (jsonrpc) - safe partner match returning ONLY
masked name (first + last initial) + city (no other PII leakage)
Security fix: technician record rule on repair.order now uses STORED
fields (technician_id + additional_technician_ids) instead of the
non-stored all_technician_ids compute, which was failing SQL generation.
Verified end-to-end on local westin-v19:
- Sales rep create via intake service with the rep user context creates
the repair with x_fc_intake_source='sales_rep_portal' and proper
activities
- /repair/submit posts urlencoded data -> creates partner + repair
('BR-WA/RO/00010', source='client_portal', urgency='urgent') ->
redirects to /repair/thanks with the reference
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
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> |