Files
Odoo-Modules/fusion_helpdesk_central/views/engagement_reporting_views.xml
gsinghpal 396170b438 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).
2026-05-27 13:03:23 -04:00

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 &gt; 7 days" name="stuck"
domain="[('x_fc_engagement_state', '=', 'pending'),
('x_fc_engagement_sent_at', '&lt;=',
(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>