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>
67 lines
2.4 KiB
Python
67 lines
2.4 KiB
Python
# -*- 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}
|