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

109 lines
5.7 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>
<!-- ============================================================== -->
<!-- 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>