feat(fusion_helpdesk_central): client-label field, views, branded ack email

x_fc_client_label on helpdesk.ticket (+ list/search exposure), create() override sends a branded acknowledgement (magic link via get_portal_url) for in-app-channel tickets only. 3 Enterprise tests (run on nexa).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-27 03:38:39 -04:00
parent b6e7ad3485
commit 9dceff5baf
7 changed files with 220 additions and 1 deletions

View File

@@ -3,7 +3,7 @@
# License OPL-1
{
'name': 'Fusion Helpdesk Central — Client API Keys',
'version': '19.0.1.0.2',
'version': '19.0.1.1.0',
'category': 'Productivity',
'summary': 'Admin UI on the central Odoo for issuing per-client API '
'keys used by fusion_helpdesk client deployments.',
@@ -28,7 +28,9 @@ Depends only on `helpdesk`. No client-side install needed.
'data': [
'security/ir.model.access.csv',
'data/ir_config_parameter_data.xml',
'data/mail_template_ack.xml',
'views/fusion_helpdesk_client_key_views.xml',
'views/helpdesk_ticket_views.xml',
],
'installable': True,
'auto_install': False,

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1
Branded acknowledgement sent to the customer when an in-app-channel
ticket is created. Carries the portal magic link (object.get_portal_url()
embeds the access token) so the customer can track + reply without an
account. Button colours follow the company email branding, like Odoo's
own helpdesk templates.
-->
<odoo>
<data noupdate="1">
<record id="mail_template_ticket_ack" model="mail.template">
<field name="name">Helpdesk: Ticket Acknowledgement (Fusion)</field>
<field name="model_id" ref="helpdesk.model_helpdesk_ticket"/>
<field name="subject">We received your request [{{ object.ticket_ref or object.id }}]</field>
<field name="partner_to">{{ object.partner_id.id }}</field>
<field name="lang">{{ object.partner_id.lang }}</field>
<field name="auto_delete" eval="True"/>
<field name="body_html" type="html">
<div style="margin:0; padding:0; font-family:Arial, Helvetica, sans-serif; color:#21252b; font-size:14px;">
<p>Hello <t t-out="object.partner_name or 'there'"/>,</p>
<p>
Thanks for reaching out — we've received your request and our
support team will be in touch. Here are the details:
</p>
<table style="margin:12px 0; font-size:14px;">
<tr>
<td style="padding:2px 12px 2px 0; color:#6c757d;">Reference</td>
<td style="padding:2px 0;"><strong t-out="object.ticket_ref or object.id"/></td>
</tr>
<tr>
<td style="padding:2px 12px 2px 0; color:#6c757d;">Subject</td>
<td style="padding:2px 0;"><t t-out="object.name or ''"/></td>
</tr>
</table>
<p style="margin:18px 0;">
<a t-att-href="object.get_base_url() + object.get_portal_url()"
target="_blank"
t-attf-style="background-color: {{ object.company_id.email_secondary_color or '#2c89e9' }}; padding:10px 18px; text-decoration:none; color: {{ object.company_id.email_primary_color or '#ffffff' }}; border-radius:5px; font-size:14px; display:inline-block;">
View &amp; track your ticket
</a>
</p>
<p style="color:#6c757d; font-size:13px;">
You can reply directly to this email to add information, follow up
from the link above, or sign up for an account from that page to
manage all of your requests in one place.
</p>
<p style="color:#6c757d; font-size:13px;"><t t-out="object.company_id.name or 'Support'"/></p>
</div>
</field>
</record>
</data>
</odoo>

View File

@@ -1,2 +1,3 @@
# -*- coding: utf-8 -*-
from . import fusion_helpdesk_client_key
from . import helpdesk_ticket

View File

@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1
"""Central-side helpdesk.ticket extensions for the customer follow-up flow.
Adds the `x_fc_client_label` deployment tag (set by the in-app reporter so
the embedded inbox can scope per client) and sends a branded acknowledgement
email — carrying the portal magic link — when an in-app ticket is created.
"""
import logging
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
class HelpdeskTicket(models.Model):
_inherit = 'helpdesk.ticket'
x_fc_client_label = fields.Char(
string='Client Deployment', index=True, copy=False,
help='Deployment tag (e.g. ENTECH) set by the fusion_helpdesk in-app '
'reporter. Scopes the embedded "My Tickets" inbox per client and '
'lets support filter tickets by originating deployment.',
)
@api.model_create_multi
def create(self, vals_list):
tickets = super().create(vals_list)
tickets._fc_send_ack_email()
return tickets
def _fc_send_ack_email(self):
"""Send the branded acknowledgement (with magic link) to the customer.
Only fires for in-app-channel tickets (those tagged with a client
label) that have a customer email — external web-form submissions
rely on the native website confirmation, so this won't double-send.
The whole thing is best-effort: a template/mail failure must never
block ticket creation, so we log and move on.
"""
template = self.env.ref(
'fusion_helpdesk_central.mail_template_ticket_ack',
raise_if_not_found=False,
)
if not template:
return
for ticket in self:
if not (ticket.x_fc_client_label and ticket.partner_email):
continue
try:
template.send_mail(ticket.id, force_send=False)
except Exception: # noqa: BLE001 — ack must never block create
_logger.exception(
'fusion_helpdesk_central: acknowledgement email failed '
'for ticket %s (%s)', ticket.id, ticket.x_fc_client_label,
)

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import test_identity

View File

@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1
"""Identity-keystone tests for the central helpdesk extensions.
Runs on an Enterprise environment (helpdesk installed) — e.g. odoo-nexa or
odoo-trial. Validates that passing partner_email resolves the customer +
follower (native), that the client label is stored, and that the branded
acknowledgement only fires for in-app-channel tickets.
"""
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install', 'fusion_helpdesk_central')
class TestTicketIdentity(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.team = cls.env['helpdesk.team'].search([], limit=1)
def _ack_mails(self, ticket):
return self.env['mail.mail'].search([
('model', '=', 'helpdesk.ticket'),
('res_id', '=', ticket.id),
]).filtered(lambda m: 'received your request' in (m.subject or ''))
def test_partner_resolution_follower_and_label(self):
ticket = self.env['helpdesk.ticket'].create({
'name': 'Keystone test',
'team_id': self.team.id,
'partner_email': 'keystone.newperson@example.com',
'partner_name': 'Key Stone',
'x_fc_client_label': 'ENTECH',
})
self.assertEqual(ticket.x_fc_client_label, 'ENTECH')
self.assertTrue(
ticket.partner_id,
"native create() should find-or-create a partner from partner_email")
self.assertEqual(ticket.partner_id.email, 'keystone.newperson@example.com')
self.assertIn(
ticket.partner_id, ticket.message_partner_ids,
"the customer must be subscribed as a follower")
def test_ack_email_for_inapp_ticket(self):
ticket = self.env['helpdesk.ticket'].create({
'name': 'Ack test',
'team_id': self.team.id,
'partner_email': 'ack.person@example.com',
'partner_name': 'Ack Person',
'x_fc_client_label': 'ENTECH',
})
self.assertTrue(
self._ack_mails(ticket),
"an in-app ticket with a customer email should get our ack email")
def test_no_ack_without_client_label(self):
# Simulates an external web-form ticket — no client label, so our
# acknowledgement must NOT fire (avoids double-acknowledgement).
ticket = self.env['helpdesk.ticket'].create({
'name': 'External web ticket',
'team_id': self.team.id,
'partner_email': 'external.web@example.com',
'partner_name': 'External Web',
})
self.assertFalse(
self._ack_mails(ticket),
"no client label => our acknowledgement should not send")

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1
Surface the client-deployment tag in the agent backend so support can
see + filter tickets by the deployment they came from.
-->
<odoo>
<record id="fhc_ticket_list_label" model="ir.ui.view">
<field name="name">fhc.helpdesk.ticket.list.client_label</field>
<field name="model">helpdesk.ticket</field>
<field name="inherit_id" ref="helpdesk.helpdesk_tickets_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//list" position="inside">
<field name="x_fc_client_label" optional="show"/>
</xpath>
</field>
</record>
<record id="fhc_ticket_search_label" model="ir.ui.view">
<field name="name">fhc.helpdesk.ticket.search.client_label</field>
<field name="model">helpdesk.ticket</field>
<field name="inherit_id" ref="helpdesk.helpdesk_tickets_view_search"/>
<field name="arch" type="xml">
<xpath expr="//search" position="inside">
<field name="x_fc_client_label"/>
<filter string="Client Deployment" name="group_client_label"
context="{'group_by': 'x_fc_client_label'}"/>
</xpath>
</field>
</record>
</odoo>