feat(fusion_helpdesk): customer follow-up + embedded ticket inbox
Squash-merge of feat/helpdesk-customer-followup. The billing and fusion_login_audit work from that branch is already on main (landed separately); this lands only the helpdesk feature. - Identity keystone: submit() forwards partner_email/partner_name/ x_fc_client_label so the central Helpdesk find-or-creates the customer partner and subscribes them as a follower (enables reply emails + magic link). - Embedded in-app 'My Tickets' inbox: server-side scoped read/reply RPC endpoints, per-user seen tracking (fusion.helpdesk.ticket.seen), systray unread badge. Defense-in-depth scope domain + _norm_email normalisation (wildcard emails cannot widen scope). - fusion_helpdesk_central: x_fc_client_label field + list/search views + branded acknowledgement email template. - Deployed and smoke-tested live: nexa central 19.0.1.1.0, entech client 19.0.1.4.1 (requires Contact Creation on the central service account). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import res_config_settings
|
||||
from . import fusion_helpdesk_ticket_seen
|
||||
|
||||
66
fusion_helpdesk/models/fusion_helpdesk_ticket_seen.py
Normal file
66
fusion_helpdesk/models/fusion_helpdesk_ticket_seen.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1
|
||||
"""Per-user read-tracking for the embedded ticket inbox.
|
||||
|
||||
Stores ONLY metadata — which central ticket a user has seen and up to
|
||||
which message id. No ticket content is replicated locally; this exists
|
||||
purely so the systray unread badge can work without re-fetching the
|
||||
whole inbox on every page load. Tickets themselves remain a live RPC
|
||||
view of the central Odoo.
|
||||
"""
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class FusionHelpdeskTicketSeen(models.Model):
|
||||
_name = 'fusion.helpdesk.ticket.seen'
|
||||
_description = 'Fusion Helpdesk — per-user read tracking (metadata only)'
|
||||
|
||||
user_id = fields.Many2one(
|
||||
'res.users', required=True, index=True, ondelete='cascade',
|
||||
default=lambda self: self.env.uid,
|
||||
)
|
||||
central_ticket_id = fields.Integer(
|
||||
string='Central Ticket ID', required=True, index=True,
|
||||
help='helpdesk.ticket id on the central Odoo.',
|
||||
)
|
||||
last_seen_message_id = fields.Integer(
|
||||
string='Last Seen Message ID', default=0,
|
||||
help='Highest central mail.message id this user has viewed for '
|
||||
'the ticket. Drives the unread badge.',
|
||||
)
|
||||
|
||||
_user_ticket_uniq = models.Constraint(
|
||||
'UNIQUE(user_id, central_ticket_id)',
|
||||
'One seen-row per user per ticket.',
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _mark_seen(self, central_ticket_id, last_message_id):
|
||||
"""Upsert the current user's last-seen marker for a ticket.
|
||||
|
||||
Monotonic — never moves the marker backwards (a stale client
|
||||
reporting an older id can't resurrect an unread badge)."""
|
||||
rec = self.search([
|
||||
('user_id', '=', self.env.uid),
|
||||
('central_ticket_id', '=', central_ticket_id),
|
||||
], limit=1)
|
||||
if rec:
|
||||
if (last_message_id or 0) > rec.last_seen_message_id:
|
||||
rec.last_seen_message_id = last_message_id
|
||||
else:
|
||||
self.create({
|
||||
'central_ticket_id': central_ticket_id,
|
||||
'last_seen_message_id': last_message_id or 0,
|
||||
})
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def _seen_map(self, central_ticket_ids):
|
||||
"""Return {central_ticket_id: last_seen_message_id} for the
|
||||
current user across the given ticket ids."""
|
||||
rows = self.search([
|
||||
('user_id', '=', self.env.uid),
|
||||
('central_ticket_id', 'in', list(central_ticket_ids)),
|
||||
])
|
||||
return {r.central_ticket_id: r.last_seen_message_id for r in rows}
|
||||
Reference in New Issue
Block a user