Files
Odoo-Modules/fusion_repairs/views/portal_maintenance_templates.xml
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

153 lines
8.0 KiB
XML

<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================== -->
<!-- Booking landing page -->
<!-- ============================================================== -->
<template id="portal_maintenance_book" name="Maintenance - Book Visit">
<t t-call="website.layout">
<div id="wrap" class="o_fusion_repairs_client">
<section class="container py-5">
<div class="row justify-content-center">
<div class="col-12 col-lg-7">
<h1 class="mb-3">Book your maintenance visit</h1>
<p class="lead text-muted">
Hello <strong t-out="contract.partner_id.name"/>!
Your <strong t-out="contract.product_id.display_name"/> is due for service.
</p>
<t t-if="already_booked">
<div class="alert alert-info">
<strong>Already booked.</strong>
Your maintenance visit reference is
<strong t-out="contract.booking_repair_id.name"/>. We will be in touch shortly.
</div>
</t>
<t t-if="not already_booked">
<form t-attf-action="/repairs/maintenance/book/{{ contract.booking_token }}/confirm"
method="POST" class="card shadow-sm">
<input type="hidden" name="csrf_token"
t-att-value="request.csrf_token()"/>
<div class="card-body p-4">
<div class="mb-3">
<label class="form-label">Preferred date</label>
<input type="date" name="preferred_date"
class="form-control form-control-lg"
t-att-value="default_date"/>
<small class="text-muted">
A team member will call to confirm the exact time.
</small>
</div>
<p class="small text-muted mb-0">
By submitting, you confirm you want this maintenance visit.
Contract reference <strong t-out="contract.name"/>.
</p>
</div>
<div class="card-footer text-end">
<button type="submit" class="btn btn-success btn-lg">
Yes, book my visit
</button>
</div>
</form>
</t>
</div>
</div>
</section>
</div>
</t>
</template>
<!-- ============================================================== -->
<!-- Thanks -->
<!-- ============================================================== -->
<template id="portal_maintenance_thanks" name="Maintenance - Booked">
<t t-call="website.layout">
<div id="wrap" class="o_fusion_repairs_client">
<section class="container py-5">
<div class="row justify-content-center text-center">
<div class="col-12 col-lg-7">
<i class="fa fa-calendar-check fa-4x text-success mb-3"/>
<h1>Booking received</h1>
<p class="lead text-muted">
Your maintenance visit <strong t-out="repair.name"/> has been scheduled
and our office will reach out to confirm the exact time.
</p>
</div>
</div>
</section>
</div>
</t>
</template>
<!-- ============================================================== -->
<!-- On-call ack pages (CL15) -->
<!-- ============================================================== -->
<template id="portal_on_call_ack_ok" name="On-Call Page Acknowledged">
<t t-call="website.layout">
<div id="wrap" class="o_fusion_repairs_client">
<section class="container py-5">
<div class="row justify-content-center text-center">
<div class="col-12 col-lg-7">
<i class="fa fa-check-circle fa-4x text-success mb-3"/>
<h1>Page acknowledged</h1>
<p class="lead text-muted">
Thank you. <strong t-out="repair_name"/> is now your responsibility -
no further pages will be sent for this request.
</p>
<a href="/web" class="btn btn-outline-secondary mt-3">Open Odoo</a>
</div>
</div>
</section>
</div>
</t>
</template>
<template id="portal_on_call_ack_invalid" name="On-Call Page Invalid">
<t t-call="website.layout">
<div id="wrap" class="o_fusion_repairs_client">
<section class="container py-5">
<div class="row justify-content-center text-center">
<div class="col-12 col-lg-7">
<i class="fa fa-exclamation-triangle fa-3x text-warning mb-3"/>
<h1>Link not valid</h1>
<p class="lead text-muted">
This on-call acknowledgement link is no longer valid. It may have been
acknowledged already or the page expired. Open Odoo and look up the
repair in the Fusion Repairs dashboard.
</p>
<a href="/web" class="btn btn-outline-secondary mt-3">Open Odoo</a>
</div>
</div>
</section>
</div>
</t>
</template>
<!-- ============================================================== -->
<!-- Invalid token -->
<!-- ============================================================== -->
<template id="portal_maintenance_invalid_token" name="Maintenance - Invalid Link">
<t t-call="website.layout">
<div id="wrap" class="o_fusion_repairs_client">
<section class="container py-5">
<div class="row justify-content-center text-center">
<div class="col-12 col-lg-7">
<i class="fa fa-exclamation-triangle fa-3x text-warning mb-3"/>
<h1>Link not valid</h1>
<p class="lead text-muted">
This booking link is no longer valid or has been used. Please call our
office directly to schedule your maintenance visit.
</p>
<a href="/repair" class="btn btn-outline-secondary">Submit a service request</a>
</div>
</div>
</section>
</div>
</t>
</template>
</odoo>