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).
144 lines
6.7 KiB
XML
144 lines
6.7 KiB
XML
<?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>
|