feat(fusion_helpdesk): owner-approval engagement flow + AI summary + reporting
Ships the design spec at docs/superpowers/specs/2026-05-27-owner-approval-flow-design.md. What's new on central (fusion_helpdesk_central 19.0.1.2.0 -> 19.0.2.0.0): - Engagement model: 8 new fields on helpdesk.ticket (state, snapshotted owner email/name, single-use UUID4 token, sent/reminded/decided timestamps, AI summary, stored-computed turnaround hours). - Wizard: single + bulk modes on one fusion.helpdesk.engagement.wizard TransientModel with a child wizard.line for per-ticket bulk summaries. default_get pulls the OpenAI summary on open; AI fan-out for bulk is parallel via ThreadPoolExecutor (max 5 workers, 30s overall cap). - OpenAI client in utils.py — stdlib urllib, 15s per-call timeout, every failure collapses to '' so the wizard's manual-summary fallback fires. - Public portal: /fusion_helpdesk/engagement/<token>/<decision> GET + POST, four branded standalone QWeb pages (confirm/done/invalid/error). Token is single-use, cleared on confirm. Decision posts a public comment attributed to the resolved owner partner; chatter propagates to the employee's My Tickets thread per the "fully visible" UX choice. - Mail templates (single + bulk) with magic-link buttons. Bulk template renders one card per ticket, each with its own approve/reject URL. - Reminder cron: daily, single-shot per engagement, configurable via fusion_helpdesk_central.engagement_reminder_days ICP (default 3, 0 disables). - Reporting dashboard: pivot/graph/list/kanban over helpdesk.ticket filtered to engaged ones, with avg-turnaround measure. Menu lives under Helpdesk > Reporting > Owner Engagements. - Client_key extended with owner_email/owner_name fields; ticket.create upserts them from the client-side piggyback (no new sync endpoint). - 100% coverage on utils + integration tests on wizard, controllers, re-engagement, cron, computed turnaround. OpenAI mocked in CI. What's new on client (fusion_helpdesk 19.0.1.7.1 -> 19.0.2.0.0): - Two new ICP settings: fusion_helpdesk.owner_email / .owner_name with a new "Owner Approval" block in Settings > Fusion Helpdesk. - controllers/main.py::submit piggybacks both keys on every ticket payload so central keeps client_key.owner_email/name fresh automatically. Verified live end-to-end on entech -> nexa: payload upsert, wizard with mocked AI, action_send, portal GET/POST/GET-again cycle, second click hits the friendly invalid-token page. Token entropy = 122 bits (UUID4).
This commit is contained in:
143
fusion_helpdesk_central/views/engagement_reporting_views.xml
Normal file
143
fusion_helpdesk_central/views/engagement_reporting_views.xml
Normal file
@@ -0,0 +1,143 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1
|
||||
|
||||
Reporting dashboard for owner engagements. No new model — every view
|
||||
is over helpdesk.ticket filtered to records where engagement_state is
|
||||
not 'none'. The stored computed field x_fc_engagement_turnaround_hours
|
||||
makes the pivot's average measure free.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- Pivot: rows = client, columns = state, measure = count + avg turnaround. -->
|
||||
<record id="view_engagement_pivot" model="ir.ui.view">
|
||||
<field name="name">fhc.engagement.pivot</field>
|
||||
<field name="model">helpdesk.ticket</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Owner Engagements" sample="1">
|
||||
<field name="x_fc_client_label" type="row"/>
|
||||
<field name="x_fc_engagement_state" type="col"/>
|
||||
<field name="x_fc_engagement_turnaround_hours" type="measure"/>
|
||||
</pivot>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_engagement_graph" model="ir.ui.view">
|
||||
<field name="name">fhc.engagement.graph</field>
|
||||
<field name="model">helpdesk.ticket</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Owner Engagements" type="bar" sample="1">
|
||||
<field name="x_fc_engagement_sent_at" interval="month"/>
|
||||
<field name="x_fc_client_label" type="col"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_engagement_list" model="ir.ui.view">
|
||||
<field name="name">fhc.engagement.list</field>
|
||||
<field name="model">helpdesk.ticket</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Owner Engagements" create="0" sample="1">
|
||||
<field name="ticket_ref" optional="show"/>
|
||||
<field name="name"/>
|
||||
<field name="x_fc_client_label" string="Client"/>
|
||||
<field name="x_fc_engagement_name" string="Owner"/>
|
||||
<field name="x_fc_engagement_email" string="Owner Email" optional="show"/>
|
||||
<field name="x_fc_engagement_state" widget="badge"
|
||||
decoration-warning="x_fc_engagement_state == 'pending'"
|
||||
decoration-success="x_fc_engagement_state == 'approved'"
|
||||
decoration-danger="x_fc_engagement_state == 'rejected'"/>
|
||||
<field name="x_fc_engagement_sent_at"/>
|
||||
<field name="x_fc_engagement_reminded_at" optional="hide"/>
|
||||
<field name="x_fc_engagement_decided_at" optional="show"/>
|
||||
<field name="x_fc_engagement_turnaround_hours" string="Turnaround (h)"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_engagement_kanban" model="ir.ui.view">
|
||||
<field name="name">fhc.engagement.kanban</field>
|
||||
<field name="model">helpdesk.ticket</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban default_group_by="x_fc_engagement_state" create="0"
|
||||
sample="1" group_create="0" group_edit="0" group_delete="0">
|
||||
<field name="name"/>
|
||||
<field name="x_fc_client_label"/>
|
||||
<field name="x_fc_engagement_name"/>
|
||||
<field name="x_fc_engagement_sent_at"/>
|
||||
<field name="x_fc_engagement_turnaround_hours"/>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<div class="o_kanban_record_top">
|
||||
<strong><field name="name"/></strong>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted"><field name="x_fc_client_label"/></span>
|
||||
·
|
||||
<field name="x_fc_engagement_name"/>
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
sent <field name="x_fc_engagement_sent_at"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_engagement_search" model="ir.ui.view">
|
||||
<field name="name">fhc.engagement.search</field>
|
||||
<field name="model">helpdesk.ticket</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Owner Engagements">
|
||||
<field name="name"/>
|
||||
<field name="x_fc_client_label"/>
|
||||
<field name="x_fc_engagement_email"/>
|
||||
<separator/>
|
||||
<filter string="Pending" name="state_pending"
|
||||
domain="[('x_fc_engagement_state', '=', 'pending')]"/>
|
||||
<filter string="Approved" name="state_approved"
|
||||
domain="[('x_fc_engagement_state', '=', 'approved')]"/>
|
||||
<filter string="Rejected" name="state_rejected"
|
||||
domain="[('x_fc_engagement_state', '=', 'rejected')]"/>
|
||||
<separator/>
|
||||
<filter string="Pending > 7 days" name="stuck"
|
||||
domain="[('x_fc_engagement_state', '=', 'pending'),
|
||||
('x_fc_engagement_sent_at', '<=',
|
||||
(context_today() - relativedelta(days=7)).strftime('%Y-%m-%d %H:%M:%S'))]"/>
|
||||
<separator/>
|
||||
<!-- Odoo 19: <group expand="0">…</group> is no longer valid
|
||||
in search views. Group-By filters work directly. -->
|
||||
<filter string="Group by Client" name="group_client"
|
||||
context="{'group_by': 'x_fc_client_label'}"/>
|
||||
<filter string="Group by State" name="group_state"
|
||||
context="{'group_by': 'x_fc_engagement_state'}"/>
|
||||
<filter string="Group by Sent (month)" name="group_sent_month"
|
||||
context="{'group_by': 'x_fc_engagement_sent_at:month'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_owner_engagements" model="ir.actions.act_window">
|
||||
<field name="name">Owner Engagements</field>
|
||||
<field name="res_model">helpdesk.ticket</field>
|
||||
<field name="view_mode">pivot,graph,list,kanban,form</field>
|
||||
<field name="search_view_id" ref="view_engagement_search"/>
|
||||
<field name="domain">[('x_fc_engagement_state', '!=', 'none')]</field>
|
||||
<field name="context">{
|
||||
'search_default_state_pending': 1
|
||||
}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">No owner engagements yet.</p>
|
||||
<p>Click "Request Owner Approval" on any in-app ticket to start one.</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_owner_engagements"
|
||||
name="Owner Engagements"
|
||||
parent="helpdesk.helpdesk_ticket_report_menu_main"
|
||||
action="action_owner_engagements"
|
||||
sequence="50"/>
|
||||
|
||||
</odoo>
|
||||
67
fusion_helpdesk_central/views/engagement_wizard_views.xml
Normal file
67
fusion_helpdesk_central/views/engagement_wizard_views.xml
Normal file
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1
|
||||
|
||||
The owner-approval engagement wizard, opened from the ticket form
|
||||
button OR the list-view bulk server action. Branches on `mode` to
|
||||
show either the single-ticket layout (one AI summary field) or the
|
||||
bulk layout (an editable line per ticket).
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_engagement_wizard_form" model="ir.ui.view">
|
||||
<field name="name">fusion.helpdesk.engagement.wizard.form</field>
|
||||
<field name="model">fusion.helpdesk.engagement.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Request Owner Approval">
|
||||
<field name="mode" invisible="1"/>
|
||||
<field name="ticket_id" invisible="1"/>
|
||||
|
||||
<group>
|
||||
<group>
|
||||
<field name="owner_name_display" string="Owner"/>
|
||||
<field name="owner_email_display" string="Owner Email" widget="email"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<div class="alert alert-warning" role="alert"
|
||||
invisible="not ai_unavailable">
|
||||
<strong>AI summary unavailable.</strong>
|
||||
OpenAI didn't return a summary (no API key set, rate limit,
|
||||
or network error). Write a quick brief below before sending —
|
||||
everything else still works.
|
||||
</div>
|
||||
|
||||
<!-- Single mode: one summary field for the one ticket. -->
|
||||
<group invisible="mode != 'single'">
|
||||
<field name="personal_note"
|
||||
placeholder="One-line note that appears above the summary in the email…"/>
|
||||
<field name="ai_summary" string="Summary to send"
|
||||
placeholder="Bullet-point summary that the owner will read first." />
|
||||
</group>
|
||||
|
||||
<!-- Bulk mode: per-ticket lines, each with its own summary. -->
|
||||
<group invisible="mode != 'bulk'">
|
||||
<field name="personal_note"
|
||||
placeholder="One-line note that appears once at the top of the combined email…"/>
|
||||
</group>
|
||||
<field name="line_ids" invisible="mode != 'bulk'" nolabel="1">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="ticket_id" readonly="1"/>
|
||||
<field name="ticket_name" readonly="1"/>
|
||||
<field name="ai_summary"/>
|
||||
</list>
|
||||
</field>
|
||||
|
||||
<footer>
|
||||
<button name="action_send" type="object"
|
||||
string="Send Engagement"
|
||||
class="btn-primary"/>
|
||||
<button special="cancel" string="Cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -27,8 +27,100 @@
|
||||
<field name="x_fc_client_label"/>
|
||||
<filter string="Client Deployment" name="group_client_label"
|
||||
context="{'group_by': 'x_fc_client_label'}"/>
|
||||
<separator/>
|
||||
<filter string="Awaiting Owner Approval" name="fhc_pending_engagement"
|
||||
domain="[('x_fc_engagement_state', '=', 'pending')]"/>
|
||||
<filter string="Owner Approved" name="fhc_approved_engagement"
|
||||
domain="[('x_fc_engagement_state', '=', 'approved')]"/>
|
||||
<filter string="Owner Rejected" name="fhc_rejected_engagement"
|
||||
domain="[('x_fc_engagement_state', '=', 'rejected')]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!--
|
||||
Inherited ticket form. Adds:
|
||||
* Header button "Request Owner Approval" (form-button entry into
|
||||
the wizard). Disabled when there's no client_label OR no owner
|
||||
contact on the client_key — see the action_open_engagement_wizard
|
||||
method which raises a UserError with the same explanation.
|
||||
* State pill in the title row showing pending / approved / rejected.
|
||||
* Collapsible "Owner Engagement" group with the audit fields.
|
||||
Inherited views in Odoo 19 cannot carry `groups`/`group_ids` on the
|
||||
record (raises ParseError); per-node `groups=` attributes are fine.
|
||||
-->
|
||||
<record id="fhc_ticket_form_engagement" model="ir.ui.view">
|
||||
<field name="name">fhc.helpdesk.ticket.form.engagement</field>
|
||||
<field name="model">helpdesk.ticket</field>
|
||||
<field name="inherit_id" ref="helpdesk.helpdesk_ticket_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<!-- Header button + state badge. Placed at the top of the
|
||||
header so it's the first thing support sees. -->
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="action_open_engagement_wizard"
|
||||
type="object"
|
||||
string="Request Owner Approval"
|
||||
class="oe_highlight"
|
||||
invisible="not x_fc_client_label or x_fc_engagement_state == 'pending'"
|
||||
groups="base.group_user"/>
|
||||
<button name="action_open_engagement_wizard"
|
||||
type="object"
|
||||
string="Re-engage Owner"
|
||||
invisible="x_fc_engagement_state != 'pending'"
|
||||
groups="base.group_user"/>
|
||||
<field name="x_fc_engagement_state" widget="statusbar"
|
||||
invisible="x_fc_engagement_state == 'none'"
|
||||
statusbar_visible="pending,approved,rejected"/>
|
||||
</xpath>
|
||||
|
||||
<!-- Collapsible Owner Engagement page on the notebook. -->
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="Owner Engagement"
|
||||
name="fhc_engagement_page"
|
||||
invisible="x_fc_engagement_state == 'none'">
|
||||
<group>
|
||||
<group>
|
||||
<field name="x_fc_engagement_state" readonly="1"/>
|
||||
<field name="x_fc_engagement_name" readonly="1"/>
|
||||
<field name="x_fc_engagement_email" readonly="1" widget="email"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="x_fc_engagement_sent_at" readonly="1"/>
|
||||
<field name="x_fc_engagement_reminded_at" readonly="1"/>
|
||||
<field name="x_fc_engagement_decided_at" readonly="1"/>
|
||||
<field name="x_fc_engagement_turnaround_hours" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<separator string="AI Summary (snapshotted at engagement)"/>
|
||||
<field name="x_fc_ai_summary" readonly="1" nolabel="1"/>
|
||||
</page>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!--
|
||||
Kanban dot on the main Helpdesk kanban deferred — the underlying
|
||||
view's structure varies between Helpdesk versions and reaching into
|
||||
it with xpath is fragile. The Reporting → Owner Engagements kanban
|
||||
(grouped by state) gives the same at-a-glance signal until we have
|
||||
a stable hook in helpdesk's main kanban.
|
||||
-->
|
||||
|
||||
<!--
|
||||
Server action — bulk "Request Owner Approval" available from the
|
||||
helpdesk.ticket list view. Validation happens in the wizard's
|
||||
default_get; the server action just opens the wizard with the
|
||||
selection in context.
|
||||
-->
|
||||
<record id="action_engagement_bulk" model="ir.actions.server">
|
||||
<field name="name">Request Owner Approval (Bulk)</field>
|
||||
<field name="model_id" ref="helpdesk.model_helpdesk_ticket"/>
|
||||
<field name="binding_model_id" ref="helpdesk.model_helpdesk_ticket"/>
|
||||
<field name="binding_view_types">list,kanban</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">action = model.action_open_engagement_wizard_bulk()</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
132
fusion_helpdesk_central/views/portal_templates.xml
Normal file
132
fusion_helpdesk_central/views/portal_templates.xml
Normal file
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1
|
||||
|
||||
Minimal branded portal pages for the owner-approval flow. NOT using the
|
||||
heavy portal.frontend_layout — the owner sees this once on their phone
|
||||
and we want sub-200ms render, so each page is a self-contained HTML doc
|
||||
with inline CSS. Branding kept consistent with the ack email's button
|
||||
style (same blue, same border-radius).
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- Shared shell: page wrapper, header strip, inline CSS reset. The
|
||||
DOCTYPE is omitted — Odoo's QWeb XML parser rejects it inside
|
||||
<template>, and modern browsers handle no-doctype pages fine for
|
||||
a minimal confirmation page like this. Inline CSS keeps the
|
||||
page self-contained, no external bundles to load. -->
|
||||
<template id="engagement_layout" name="Owner Engagement Layout">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>Nexa Systems Support</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: Arial, Helvetica, sans-serif; color: #21252b; background: #f3f4f6; -webkit-font-smoothing: antialiased; }
|
||||
.fhc-header { background: #1e3a5f; color: #fff; padding: 14px 22px; font-weight: 700; letter-spacing: 0.02em; }
|
||||
.fhc-card { max-width: 640px; margin: 28px auto; background: #fff; border: 1px solid #d8dadd; border-radius: 8px; padding: 22px 26px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
|
||||
.fhc-title { font-size: 1.15rem; font-weight: 700; margin: 0 0 6px 0; }
|
||||
.fhc-meta { color: #6c757d; font-size: 0.85rem; margin-bottom: 14px; }
|
||||
.fhc-section-h { font-size: 0.78rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #6c757d; margin: 16px 0 6px 0; }
|
||||
.fhc-summary { white-space: pre-wrap; line-height: 1.5; background: #f9fafb; padding: 12px 14px; border-radius: 6px; border: 1px solid #e5e7eb; font-size: 0.92rem; }
|
||||
.fhc-comment { width: 100%; min-height: 90px; padding: 8px 10px; font-family: inherit; font-size: 0.95rem; border: 1px solid #d8dadd; border-radius: 6px; resize: vertical; }
|
||||
.fhc-btn { display: inline-block; padding: 12px 26px; font-size: 1rem; font-weight: 700; border-radius: 6px; border: 0; color: #fff; cursor: pointer; }
|
||||
.fhc-btn-approve { background: linear-gradient(135deg, #5cc66f 0%, #28a745 100%); }
|
||||
.fhc-btn-reject { background: linear-gradient(135deg, #e85d68 0%, #dc3545 100%); }
|
||||
.fhc-actions { display: flex; gap: 10px; margin-top: 14px; }
|
||||
.fhc-foot { color: #9aa3ad; font-size: 0.78rem; text-align: center; margin-top: 18px; }
|
||||
.fhc-bad { color: #b02a37; }
|
||||
.fhc-good { color: #1e7e34; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="fhc-header">Nexa Systems Support</div>
|
||||
<div class="fhc-card">
|
||||
<t t-out="0"/>
|
||||
</div>
|
||||
<div class="fhc-foot">If this link doesn't look right, contact <a href="mailto:support@nexasystems.ca">support@nexasystems.ca</a>.</div>
|
||||
</body>
|
||||
</html>
|
||||
</template>
|
||||
|
||||
<!-- Confirmation page: owner has clicked the magic link and we want
|
||||
one last confirmation + optional comment before recording the
|
||||
decision. POST goes back to the same URL. -->
|
||||
<template id="engagement_confirm" name="Engagement Confirm">
|
||||
<t t-call="fusion_helpdesk_central.engagement_layout">
|
||||
<div class="fhc-title" t-esc="ticket.name"/>
|
||||
<div class="fhc-meta">
|
||||
Confirm your decision on this request — your name and any
|
||||
comment below will be posted on the ticket so the team sees
|
||||
it immediately.
|
||||
</div>
|
||||
|
||||
<div class="fhc-section-h">Summary</div>
|
||||
<div class="fhc-summary" t-esc="ticket.x_fc_ai_summary or 'No AI summary was attached to this request.'"/>
|
||||
|
||||
<form method="POST" action="">
|
||||
<div class="fhc-section-h">Comment (optional)</div>
|
||||
<textarea class="fhc-comment" name="comment"
|
||||
placeholder="Add a note for the team (e.g. 'go ahead, this fits Q2 budget')"/>
|
||||
<div class="fhc-actions">
|
||||
<t t-if="decision_state == 'approved'">
|
||||
<button class="fhc-btn fhc-btn-approve" type="submit">✓ Confirm Approval</button>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<button class="fhc-btn fhc-btn-reject" type="submit">✗ Confirm Rejection</button>
|
||||
</t>
|
||||
</div>
|
||||
</form>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Done: decision recorded. Owner sees a friendly receipt. -->
|
||||
<template id="engagement_done" name="Engagement Done">
|
||||
<t t-call="fusion_helpdesk_central.engagement_layout">
|
||||
<t t-if="decision_state == 'approved'">
|
||||
<div class="fhc-title fhc-good">✓ Approval recorded</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="fhc-title fhc-bad">✗ Rejection recorded</div>
|
||||
</t>
|
||||
<div class="fhc-meta">
|
||||
Thanks — your decision on
|
||||
"<b><t t-esc="ticket.name"/></b>"
|
||||
has been posted to the ticket and the team has been notified.
|
||||
You can safely close this tab. If you also received a bulk
|
||||
email with multiple requests, come back to it and decide on
|
||||
the others independently.
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Invalid: token unknown, already used, or wrong state. -->
|
||||
<template id="engagement_invalid" name="Engagement Invalid">
|
||||
<t t-call="fusion_helpdesk_central.engagement_layout">
|
||||
<div class="fhc-title fhc-bad">Link no longer valid</div>
|
||||
<div class="fhc-meta">
|
||||
This approval link has already been used, was replaced by a
|
||||
newer request, or is otherwise no longer active.
|
||||
If you think this is a mistake, please contact
|
||||
<a href="mailto:support@nexasystems.ca">support@nexasystems.ca</a>
|
||||
and we'll sort it out.
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Server-side error -->
|
||||
<template id="engagement_error" name="Engagement Error">
|
||||
<t t-call="fusion_helpdesk_central.engagement_layout">
|
||||
<div class="fhc-title fhc-bad">Something went wrong</div>
|
||||
<div class="fhc-meta">
|
||||
We couldn't record your decision because of an unexpected
|
||||
error on our side. Please try the link again in a minute —
|
||||
if it still fails, reply to the original email or contact
|
||||
<a href="mailto:support@nexasystems.ca">support@nexasystems.ca</a>.
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user