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>
This commit is contained in:
gsinghpal
2026-05-20 23:40:52 -04:00
parent 3a15164605
commit 5c8768c556
18 changed files with 1146 additions and 1 deletions

View File

@@ -15,5 +15,20 @@
<field name="active" eval="True"/>
</record>
<!-- CL15: Escalate unacknowledged on-call pages every 5 minutes.
Pages older than fusion_repairs.on_call_escalate_minutes
(default 15) get re-paged to the next priority. -->
<record id="cron_on_call_escalate" model="ir.cron">
<field name="name">Fusion Repairs: Escalate unacknowledged on-call pages</field>
<field name="model_id" ref="model_fusion_repair_on_call_service"/>
<field name="state">code</field>
<field name="code">model.cron_escalate_unacknowledged()</field>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">5</field>
<field name="interval_type">minutes</field>
<field name="nextcall" eval="(DateTime.now() + timedelta(minutes=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="active" eval="True"/>
</record>
</data>
</odoo>

View File

@@ -55,6 +55,53 @@
<field name="auto_delete" eval="True"/>
</record>
<!-- ============================================================== -->
<!-- On-Call Safety Page -->
<!-- ============================================================== -->
<record id="email_template_on_call_page" model="mail.template">
<field name="name">Repair: On-Call Safety Page</field>
<field name="model_id" ref="repair.model_repair_order"/>
<field name="subject">[SAFETY PAGE] {{ object.partner_id.name or 'Unknown' }} - {{ object.name or 'n/a' }}</field>
<field name="email_from">{{ (object.company_id.email_formatted or user.email_formatted) }}</field>
<field name="body_html" type="html">
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;max-width:600px;margin:0 auto;">
<div style="height:4px;background-color:#c53030;"></div>
<div style="padding:32px 28px;">
<p style="color:#c53030;font-size:13px;font-weight:600;letter-spacing:0.5px;text-transform:uppercase;margin:0 0 24px 0;">
URGENT - SAFETY PAGE
</p>
<h2 style="font-size:22px;font-weight:700;margin:0 0 6px 0;line-height:1.3;">Safety service call requires response</h2>
<p style="opacity:0.65;font-size:15px;line-height:1.5;margin:0 0 24px 0;">
A client just submitted a safety-flagged service request via the
<t t-out="dict(object._fields['x_fc_intake_source'].selection).get(object.x_fc_intake_source) or 'intake'"/>.
You have been paged as the on-call manager.
</p>
<table style="width:100%;border-collapse:collapse;margin:0 0 24px 0;">
<tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);width:35%;">Reference</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.name"/></td></tr>
<tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);">Client</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.partner_id.name or '?'"/></td></tr>
<t t-if="object.partner_id.phone">
<tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);">Phone</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.partner_id.phone"/></td></tr>
</t>
<t t-if="object.x_fc_repair_category_id">
<tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);">Equipment</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.x_fc_repair_category_id.name"/></td></tr>
</t>
</table>
<div style="text-align:center;margin:0 0 24px 0;">
<a t-attf-href="/repair/on-call/ack/{{ object.x_fc_on_call_token or '' }}"
style="display:inline-block;padding:14px 28px;background-color:#c53030;color:#ffffff;text-decoration:none;border-radius:6px;font-size:16px;font-weight:600;">
Acknowledge - I will respond
</a>
</div>
<p style="opacity:0.55;font-size:12px;margin:0;">
If you do not acknowledge within 15 minutes, the next on-call
priority will be paged automatically.
</p>
</div>
</div>
</field>
<field name="auto_delete" eval="True"/>
</record>
<!-- ============================================================== -->
<!-- Maintenance Due Reminder -->
<!-- ============================================================== -->

View File

@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Seed deterministic self-check rules per equipment category.
These are used by fusion.repair.ai.service when AI is unavailable,
when AI returns malformed/unsafe content, or when AI is disabled.
noupdate=1 so admins can customise per site without losing changes.
-->
<odoo>
<data noupdate="1">
<!-- Hospital Bed -->
<record id="self_check_bed_no_power" model="fusion.repair.self.check.rule">
<field name="name">Hospital Bed - No Power</field>
<field name="category_id" ref="category_hospital_bed"/>
<field name="sequence">10</field>
<field name="symptom_keywords">won't move,dead,no power,no response</field>
<field name="instruction">Check the bed is plugged in and the outlet has power - try plugging a phone charger into the same outlet to confirm.</field>
<field name="expected_result">Bed responds when controls are pressed.</field>
</record>
<record id="self_check_bed_slow" model="fusion.repair.self.check.rule">
<field name="name">Hospital Bed - Slow / Sluggish</field>
<field name="category_id" ref="category_hospital_bed"/>
<field name="sequence">20</field>
<field name="symptom_keywords">slow,sluggish</field>
<field name="instruction">Unplug the bed for 30 seconds then plug it back in.</field>
<field name="expected_result">Movement returns to normal speed.</field>
</record>
<record id="self_check_bed_remote" model="fusion.repair.self.check.rule">
<field name="name">Hospital Bed - Remote Unresponsive</field>
<field name="category_id" ref="category_hospital_bed"/>
<field name="sequence">30</field>
<field name="symptom_keywords">remote,controller</field>
<field name="instruction">Replace the remote batteries with fresh AAA batteries.</field>
<field name="expected_result">Remote lights up and bed responds.</field>
</record>
<record id="self_check_bed_alarm" model="fusion.repair.self.check.rule">
<field name="name">Hospital Bed - Alarm</field>
<field name="category_id" ref="category_hospital_bed"/>
<field name="sequence">40</field>
<field name="symptom_keywords">beep,alarm,alert</field>
<field name="instruction">Check both side rails are fully locked in the raised position.</field>
<field name="expected_result">Alarm stops.</field>
</record>
<!-- Wheelchair (manual) -->
<record id="self_check_wheelchair_brake" model="fusion.repair.self.check.rule">
<field name="name">Wheelchair - Brake</field>
<field name="category_id" ref="category_wheelchair_manual"/>
<field name="sequence">10</field>
<field name="symptom_keywords">brake,stop</field>
<field name="instruction">Push the brake lever fully to the locked position and listen for a click.</field>
<field name="expected_result">Brake holds wheel firmly.</field>
</record>
<record id="self_check_wheelchair_tire" model="fusion.repair.self.check.rule">
<field name="name">Wheelchair - Hard to Push</field>
<field name="category_id" ref="category_wheelchair_manual"/>
<field name="sequence">20</field>
<field name="symptom_keywords">hard to push,drag,slow</field>
<field name="instruction">Check both tires for full inflation - firm to thumb pressure.</field>
<field name="expected_result">Wheelchair rolls freely.</field>
</record>
<!-- Wheelchair (power) -->
<record id="self_check_powerchair_battery" model="fusion.repair.self.check.rule">
<field name="name">Power Wheelchair - No Power</field>
<field name="category_id" ref="category_wheelchair_power"/>
<field name="sequence">10</field>
<field name="symptom_keywords">won't turn on,dead,no power,battery</field>
<field name="instruction">Confirm the battery indicator shows charge and the key switch is in the ON position.</field>
<field name="expected_result">Display lights up.</field>
</record>
<record id="self_check_powerchair_error" model="fusion.repair.self.check.rule">
<field name="name">Power Wheelchair - Error Code</field>
<field name="category_id" ref="category_wheelchair_power"/>
<field name="sequence">20</field>
<field name="symptom_keywords">error,flashing,code</field>
<field name="instruction">Note the error code shown on the joystick display, then turn off and back on after 30 seconds.</field>
<field name="expected_result">Error clears or a specific code is captured.</field>
</record>
<!-- Stairlift (safety-critical) -->
<record id="self_check_stairlift_seat" model="fusion.repair.self.check.rule">
<field name="name">Stairlift - Won't Move</field>
<field name="category_id" ref="category_stairlift"/>
<field name="sequence">10</field>
<field name="symptom_keywords">won't move,stuck</field>
<field name="instruction">Check the seat is fully rotated to the forward position and the seatbelt is fastened.</field>
<field name="expected_result">Stairlift responds.</field>
</record>
<record id="self_check_stairlift_track" model="fusion.repair.self.check.rule">
<field name="name">Stairlift - Stops Midway</field>
<field name="category_id" ref="category_stairlift"/>
<field name="sequence">20</field>
<field name="symptom_keywords">stops midway,halts</field>
<field name="instruction">Check the track for items blocking the sensors - toys, slippers, debris.</field>
<field name="expected_result">Stairlift completes its travel.</field>
</record>
<record id="self_check_stairlift_remote" model="fusion.repair.self.check.rule">
<field name="name">Stairlift - Call Station Unresponsive</field>
<field name="category_id" ref="category_stairlift"/>
<field name="sequence">30</field>
<field name="symptom_keywords">remote,call station</field>
<field name="instruction">Replace the remote / call-station batteries with fresh batteries.</field>
<field name="expected_result">Call station responds.</field>
</record>
<!-- Porch Lift (safety-critical) -->
<record id="self_check_porch_gate" model="fusion.repair.self.check.rule">
<field name="name">Porch Lift - Won't Move</field>
<field name="category_id" ref="category_porch_lift"/>
<field name="sequence">10</field>
<field name="symptom_keywords">won't move,dead</field>
<field name="instruction">Check all gate and door safety switches are fully closed.</field>
<field name="expected_result">Lift responds.</field>
</record>
<record id="self_check_porch_controls" model="fusion.repair.self.check.rule">
<field name="name">Porch Lift - Sticky Controls</field>
<field name="category_id" ref="category_porch_lift"/>
<field name="sequence">20</field>
<field name="symptom_keywords">sticky,stuck button</field>
<field name="instruction">If outdoors, gently wipe the controls with a dry cloth and let dry.</field>
<field name="expected_result">Controls respond.</field>
</record>
<!-- Walker / Rollator -->
<record id="self_check_walker_wheel" model="fusion.repair.self.check.rule">
<field name="name">Walker - Wheel Stuck</field>
<field name="category_id" ref="category_walker"/>
<field name="sequence">10</field>
<field name="symptom_keywords">wheel stick,won't roll</field>
<field name="instruction">Check for hair or debris wrapped around the wheel axle.</field>
<field name="expected_result">Wheel spins freely.</field>
</record>
<record id="self_check_rollator_brake" model="fusion.repair.self.check.rule">
<field name="name">Rollator - Brake Won't Lock</field>
<field name="category_id" ref="category_rollator"/>
<field name="sequence">10</field>
<field name="symptom_keywords">brake won't lock,brake loose</field>
<field name="instruction">Push the brake lever fully down until you feel a click.</field>
<field name="expected_result">Brake holds.</field>
</record>
<!-- Medical Mattress -->
<record id="self_check_mattress_pump" model="fusion.repair.self.check.rule">
<field name="name">Mattress - Deflated</field>
<field name="category_id" ref="category_mattress"/>
<field name="sequence">10</field>
<field name="symptom_keywords">deflated,flat,soft</field>
<field name="instruction">Confirm the pump is plugged in, powered on, and the hose is firmly attached.</field>
<field name="expected_result">Mattress inflates.</field>
</record>
<record id="self_check_mattress_alarm" model="fusion.repair.self.check.rule">
<field name="name">Mattress - Alarm</field>
<field name="category_id" ref="category_mattress"/>
<field name="sequence">20</field>
<field name="symptom_keywords">alarm,beep</field>
<field name="instruction">Check the pump display for the error code shown, then restart the pump by unplugging for 30 seconds.</field>
<field name="expected_result">Alarm clears.</field>
</record>
</data>
</odoo>