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>
This commit is contained in:
67
fusion_repairs/views/maintenance_contract_views.xml
Normal file
67
fusion_repairs/views/maintenance_contract_views.xml
Normal file
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_maintenance_contract_list" model="ir.ui.view">
|
||||
<field name="name">fusion.repair.maintenance.contract.list</field>
|
||||
<field name="model">fusion.repair.maintenance.contract</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Maintenance Contracts" decoration-warning="next_due_date and next_due_date <= context_today().strftime('%Y-%m-%d')">
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="product_id"/>
|
||||
<field name="interval_months"/>
|
||||
<field name="last_service_date"/>
|
||||
<field name="next_due_date"/>
|
||||
<field name="last_reminder_band" optional="show"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-success="state == 'active'"
|
||||
decoration-muted="state == 'cancelled'"
|
||||
decoration-warning="state == 'paused'"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_maintenance_contract_form" model="ir.ui.view">
|
||||
<field name="name">fusion.repair.maintenance.contract.form</field>
|
||||
<field name="model">fusion.repair.maintenance.contract</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Maintenance Contract">
|
||||
<header>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="draft,active,paused,cancelled"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" readonly="1"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="partner_id" options="{'no_create': True}"/>
|
||||
<field name="product_id"/>
|
||||
<field name="lot_id"/>
|
||||
<field name="original_sale_order_id" readonly="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="interval_months"/>
|
||||
<field name="last_service_date"/>
|
||||
<field name="next_due_date"/>
|
||||
<field name="last_reminder_band" readonly="1"/>
|
||||
<field name="booking_repair_id" readonly="1"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_maintenance_contract" model="ir.actions.act_window">
|
||||
<field name="name">Maintenance Contracts</field>
|
||||
<field name="res_model">fusion.repair.maintenance.contract</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -26,6 +26,12 @@
|
||||
action="repair.action_repair_order_tree"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem id="menu_fusion_repairs_maintenance_contracts"
|
||||
name="Maintenance Contracts"
|
||||
parent="menu_fusion_repairs_operations"
|
||||
action="action_maintenance_contract"
|
||||
sequence="30"/>
|
||||
|
||||
<!-- Configuration -->
|
||||
<menuitem id="menu_fusion_repairs_configuration"
|
||||
name="Configuration"
|
||||
|
||||
108
fusion_repairs/views/portal_maintenance_templates.xml
Normal file
108
fusion_repairs/views/portal_maintenance_templates.xml
Normal file
@@ -0,0 +1,108 @@
|
||||
<?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>
|
||||
Reference in New Issue
Block a user