Files
Odoo-Modules/docs/superpowers/plans/2026-05-27-fusion-helpdesk-customer-followup.md
gsinghpal 6c15a7b1cf 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>
2026-05-27 09:23:33 -04:00

25 KiB

Fusion Helpdesk — Customer Follow-up & Embedded Inbox Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Attach real customer identity to every helpdesk ticket and give client-deployment staff an in-app ticket inbox (read replies + follow up without leaving their Odoo), while external customers use the native Enterprise portal + magic link.

Architecture: Keystone = pass partner_email/partner_name/x_fc_client_label in the ticket-create payload; native helpdesk then creates the partner + subscribes the follower. Client module (fusion_helpdesk) gains read/reply RPC endpoints + a tabbed dialog + unread badge, all scoped server-side by the logged-in user's identity. Central module (fusion_helpdesk_central) adds the x_fc_client_label field + a branded acknowledgement email.

Tech Stack: Odoo 19 (Enterprise on central, Community on client deployments), Python 3.11, OWL 2, XML-RPC client→central, helpdesk (Enterprise), portal.mixin, mail.thread.cc.

Spec: docs/superpowers/specs/2026-05-27-fusion-helpdesk-customer-followup-design.md

Testability note: fusion_helpdesk depends only on base/web/mail → installable + testable on local Community (odoo-modsdev-app, DB modsdev). Pure logic (scope-domain, message filtering, vals builder, unread math) is extracted into fusion_helpdesk/utils.py and unit-tested with no live remote. fusion_helpdesk_central depends on helpdesk (Enterprise) → install/test on the deploy target (odoo-nexa) or odoo-trial.


File Structure

fusion_helpdesk (client)

  • utils.py (new) — pure helpers: build_scope_domain, is_public_message, build_ticket_vals, compute_unread_count. No Odoo env needed → trivially unit-testable.
  • controllers/main.py (modify) — keystone payload in submit(); new endpoints my_tickets, ticket_detail, ticket_reply, unread_count; a mockable _rpc(model, method, args, kw) seam.
  • models/__init__.py, models/fusion_helpdesk_ticket_seen.py (new)fusion.helpdesk.ticket.seen read-tracking model.
  • security/ir.model.access.csv (modify) — ACL for the seen model.
  • security/fusion_helpdesk_groups.xml (new)group_reporter_admin.
  • static/src/js/fusion_helpdesk_dialog.js (modify) — tabs (New / My Tickets), list, thread, reply.
  • static/src/xml/fusion_helpdesk_dialog.xml (modify) — tab markup + list/thread/reply templates + confirmed-email field.
  • static/src/js/fusion_helpdesk_systray.js (modify) — unread badge.
  • static/src/xml/fusion_helpdesk_systray.xml (modify) — badge markup.
  • static/src/scss/fusion_helpdesk.scss (modify) — tab/list/thread/badge styles.
  • tests/__init__.py, tests/test_utils.py, tests/test_seen.py (new).
  • __manifest__.py (modify) — version bump, register groups XML + tests dir + new model.

fusion_helpdesk_central (central)

  • models/__init__.py, models/helpdesk_ticket.py (new) — inherit helpdesk.ticket, add x_fc_client_label.
  • views/helpdesk_ticket_views.xml (new) — list column + search filter for x_fc_client_label.
  • data/mail_template_ack.xml (new) — branded acknowledgement template.
  • data/helpdesk_ack_automation.xml (new) OR create-override in helpdesk_ticket.py — send ack on create.
  • tests/__init__.py, tests/test_identity.py (new) — partner resolution + follower + label.
  • __manifest__.py (modify) — version bump, register models/views/data/tests.

Phase 1 — Keystone identity

Task 1: Pure build_ticket_vals helper (client)

Files: Create fusion_helpdesk/utils.py; Test fusion_helpdesk/tests/test_utils.py

  • Step 1: Write failing test
# fusion_helpdesk/tests/test_utils.py
from odoo.tests import TransactionCase, tagged
from odoo.addons.fusion_helpdesk.utils import build_ticket_vals

@tagged('post_install', '-at_install', 'fusion_helpdesk')
class TestBuildTicketVals(TransactionCase):
    def test_identity_fields_present(self):
        vals = build_ticket_vals(
            kind='bug', subject='X', body_html='<p>b</p>',
            team_id=1, client_label='ENTECH',
            reporter_name='John Doe', reporter_email='john@entech.com',
            company_name='ENTECH Inc',
        )
        self.assertEqual(vals['partner_email'], 'john@entech.com')
        self.assertEqual(vals['partner_name'], 'John Doe')
        self.assertEqual(vals['x_fc_client_label'], 'ENTECH')
        self.assertEqual(vals['partner_company_name'], 'ENTECH Inc')
        self.assertEqual(vals['team_id'], 1)
        self.assertIn('X', vals['name'])

    def test_no_email_omits_partner_email(self):
        vals = build_ticket_vals(
            kind='feature', subject='Y', body_html='<p>b</p>',
            team_id=False, client_label='', reporter_name='Jane',
            reporter_email='', company_name='',
        )
        self.assertNotIn('partner_email', vals)       # never send empty email
        self.assertNotIn('team_id', vals)             # omit falsy team
        self.assertEqual(vals['partner_name'], 'Jane')
  • Step 2: Run — expect ImportError/FAIL Run: docker exec odoo-modsdev-app odoo -d modsdev --test-enable --test-tags /fusion_helpdesk -u fusion_helpdesk --stop-after-init --http-port=0 --gevent-port=0 2>&1 | tail -30

  • Step 3: Implement build_ticket_vals

# fusion_helpdesk/utils.py
"""Pure helpers for fusion_helpdesk — no Odoo env, unit-testable in isolation."""

def build_ticket_vals(kind, subject, body_html, team_id, client_label,
                      reporter_name, reporter_email, company_name):
    """Construct helpdesk.ticket create vals. Identity fields drive native
    partner find-or-create + follower subscription on the central Odoo."""
    kind_label = 'Bug Report' if kind == 'bug' else 'Feature Request'
    prefix = ('[%s] ' % client_label) if client_label else ''
    vals = {
        'name': '%s%s: %s' % (prefix, kind_label, subject or '(untitled)'),
        'description': body_html,
        'partner_name': reporter_name or '',
    }
    if team_id:
        vals['team_id'] = team_id
    if reporter_email:
        vals['partner_email'] = reporter_email
    if company_name:
        vals['partner_company_name'] = company_name
    if client_label:
        vals['x_fc_client_label'] = client_label
    return vals
  • Step 4: Run — expect PASS (same command as Step 2)
  • Step 5: Commit git add fusion_helpdesk/utils.py fusion_helpdesk/tests/ && git commit -m "feat(fusion_helpdesk): pure build_ticket_vals helper (identity keystone)"

Task 2: Wire keystone into submit() (client)

Files: Modify fusion_helpdesk/controllers/main.py

  • Step 1: In submit(), accept new arg reply_email=None. Replace the inline ticket_vals block with:
from odoo.addons.fusion_helpdesk.utils import build_ticket_vals
# ...
user = request.env.user
reporter_email = (reply_email or user.email or user.login or '').strip()
body_html = '\n'.join(body_parts)
ticket_vals = build_ticket_vals(
    kind=kind, subject=subject, body_html=body_html,
    team_id=cfg['team_id'], client_label=cfg['client_label'],
    reporter_name=user.name, reporter_email=reporter_email,
    company_name=request.env.company.name,
)
  • Step 2: Keep the existing create + attachment + return logic. Verify _build_diag_block still appends.
  • Step 3: Manual sanitydocker exec odoo-modsdev-app odoo -d modsdev -u fusion_helpdesk --stop-after-init 2>&1 | tail -20 (module upgrades clean).
  • Step 4: Commit git commit -am "feat(fusion_helpdesk): send partner identity in ticket payload"

Task 3: x_fc_client_label field on central

Files: Create fusion_helpdesk_central/models/__init__.py, models/helpdesk_ticket.py; Modify __init__.py, __manifest__.py

  • Step 1: Write failing test (runs on Enterprise env)
# fusion_helpdesk_central/tests/test_identity.py
from odoo.tests import TransactionCase, tagged

@tagged('post_install', '-at_install', 'fusion_helpdesk_central')
class TestTicketIdentity(TransactionCase):
    def test_label_field_and_partner_resolution(self):
        team = self.env['helpdesk.team'].search([], limit=1)
        t = self.env['helpdesk.ticket'].create({
            'name': 'T1', 'team_id': team.id,
            'partner_email': 'newperson@example.com',
            'partner_name': 'New Person',
            'x_fc_client_label': 'ENTECH',
        })
        self.assertEqual(t.x_fc_client_label, 'ENTECH')
        self.assertTrue(t.partner_id, "native create should resolve partner from email")
        self.assertIn(t.partner_id, t.message_partner_ids, "customer should be a follower")
  • Step 2: Implement field
# fusion_helpdesk_central/models/helpdesk_ticket.py
from odoo import fields, models

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 in-app reporter. '
             'Scopes the embedded "My Tickets" inbox per client.',
    )
# fusion_helpdesk_central/models/__init__.py
from . import helpdesk_ticket
  • Step 3: fusion_helpdesk_central/__init__.py → add from . import models. __manifest__.pyversion bump to 19.0.1.1.0, add 'models' import is implicit; add views/helpdesk_ticket_views.xml to data, add tests discovery (automatic).
  • Step 4: Run on Enterprise (deferred to Phase 6 deploy; can't run on local Community).
  • Step 5: Commit git commit -am "feat(fusion_helpdesk_central): x_fc_client_label on helpdesk.ticket"

Task 4: Backend list/search exposure (central)

Files: Create fusion_helpdesk_central/views/helpdesk_ticket_views.xml

  • Step 1: Inherit the helpdesk ticket list + search to add x_fc_client_label (column optional="show", search field + a group-by). Use group_ids not groups_id if gating (none needed here).
<odoo>
  <record id="fhc_ticket_list_label" model="ir.ui.view">
    <field name="name">fhc.helpdesk.ticket.list.label</field>
    <field name="model">helpdesk.ticket</field>
    <field name="inherit_id" ref="helpdesk.helpdesk_ticket_view_tree"/>
    <field name="arch" type="xml">
      <field name="partner_id" position="after">
        <field name="x_fc_client_label" optional="show"/>
      </field>
    </field>
  </record>
  <record id="fhc_ticket_search_label" model="ir.ui.view">
    <field name="name">fhc.helpdesk.ticket.search.label</field>
    <field name="model">helpdesk.ticket</field>
    <field name="inherit_id" ref="helpdesk.helpdesk_tickets_view_search"/>
    <field name="arch" type="xml">
      <field name="partner_id" position="after">
        <field name="x_fc_client_label"/>
        <filter string="Client Deployment" name="group_client_label"
                context="{'group_by': 'x_fc_client_label'}"/>
      </field>
    </field>
  </record>
</odoo>

NOTE at execution: verify the exact inherit_id external IDs by reading the live views (helpdesk.helpdesk_ticket_view_tree, helpdesk.helpdesk_tickets_view_search) on odoo-nexa — names differ across versions. Adjust before install.

  • Step 2: Commit git commit -am "feat(fusion_helpdesk_central): expose client label in ticket views"

Phase 2 — Read APIs + scoping (client)

Task 5: Pure scoping + message-filter + unread helpers

Files: Modify fusion_helpdesk/utils.py; Modify fusion_helpdesk/tests/test_utils.py

  • Step 1: Write failing tests
from odoo.addons.fusion_helpdesk.utils import (
    build_scope_domain, is_public_message, compute_unread_count)

def test_regular_scope_binds_email_and_label(self):
    dom = build_scope_domain(label='ENTECH', email='john@entech.com', is_admin=False)
    self.assertIn(('x_fc_client_label', '=', 'ENTECH'), dom)
    self.assertIn(('partner_email', '=ilike', 'john@entech.com'), dom)

def test_admin_scope_binds_label_only(self):
    dom = build_scope_domain(label='ENTECH', email='a@entech.com', is_admin=True)
    self.assertIn(('x_fc_client_label', '=', 'ENTECH'), dom)
    self.assertFalse(any(t[0] == 'partner_email' for t in dom))

def test_admin_still_bounded_by_label(self):
    # label is ALWAYS present — no cross-deployment leakage
    self.assertTrue(build_scope_domain('ENTECH', 'a@x', True))

def test_internal_note_is_not_public(self):
    self.assertFalse(is_public_message({'subtype_is_internal': True}))
    self.assertTrue(is_public_message({'subtype_is_internal': False}))

def test_unread_count(self):
    tickets = [{'id': 1, 'last_support_msg_id': 10},
               {'id': 2, 'last_support_msg_id': 5},
               {'id': 3, 'last_support_msg_id': 0}]
    seen = {1: 10, 2: 3}   # ticket 2 has newer support msg; 1 is read; 3 none
    self.assertEqual(compute_unread_count(tickets, seen), 1)
  • Step 2: Run — FAIL
  • Step 3: Implement
def build_scope_domain(label, email, is_admin):
    """Server-side ticket scope. label is ALWAYS bound (defense in depth)."""
    domain = [('x_fc_client_label', '=', label or '__none__')]
    if not is_admin:
        domain.append(('partner_email', '=ilike', email or '__none__'))
    return domain

def is_public_message(msg):
    """True when a message is customer-visible (not an internal note)."""
    return not msg.get('subtype_is_internal', False)

def compute_unread_count(tickets, seen_by_id):
    """Count tickets whose latest support message id exceeds the user's
    last-seen id for that ticket (0/absent = unseen baseline)."""
    n = 0
    for t in tickets:
        last = t.get('last_support_msg_id') or 0
        if last and last > (seen_by_id.get(t['id']) or 0):
            n += 1
    return n
  • Step 4: Run — PASS; Step 5: Commit

Task 6: fusion.helpdesk.ticket.seen model + ACL

Files: Create fusion_helpdesk/models/__init__.py, models/fusion_helpdesk_ticket_seen.py; Modify __init__.py, security/ir.model.access.csv, __manifest__.py; Test fusion_helpdesk/tests/test_seen.py

  • Step 1: Failing test
# tests/test_seen.py
from odoo.tests import TransactionCase, tagged

@tagged('post_install', '-at_install', 'fusion_helpdesk')
class TestSeen(TransactionCase):
    def test_mark_seen_upserts(self):
        Seen = self.env['fusion.helpdesk.ticket.seen']
        Seen._mark_seen(central_ticket_id=42, last_message_id=100)
        Seen._mark_seen(central_ticket_id=42, last_message_id=120)
        rec = Seen.search([('user_id', '=', self.env.uid),
                           ('central_ticket_id', '=', 42)])
        self.assertEqual(len(rec), 1)
        self.assertEqual(rec.last_seen_message_id, 120)

    def test_seen_map(self):
        Seen = self.env['fusion.helpdesk.ticket.seen']
        Seen._mark_seen(1, 10); Seen._mark_seen(2, 20)
        self.assertEqual(Seen._seen_map([1, 2, 3]), {1: 10, 2: 20})
  • Step 2: Run — FAIL
  • Step 3: Implement model
# models/fusion_helpdesk_ticket_seen.py
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,
                              default=lambda s: s.env.uid, ondelete='cascade')
    central_ticket_id = fields.Integer(required=True, index=True)
    last_seen_message_id = fields.Integer(default=0)

    _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):
        rec = self.search([('user_id', '=', self.env.uid),
                           ('central_ticket_id', '=', central_ticket_id)], limit=1)
        if rec:
            if last_message_id > 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})
        return True

    @api.model
    def _seen_map(self, central_ticket_ids):
        rows = self.search([('user_id', '=', self.env.uid),
                           ('central_ticket_id', 'in', central_ticket_ids)])
        return {r.central_ticket_id: r.last_seen_message_id for r in rows}
  • Step 4: ACL CSV row:
access_fhd_seen_user,fusion.helpdesk.ticket.seen.user,model_fusion_helpdesk_ticket_seen,base.group_user,1,1,1,1

models/__init__.pyfrom . import fusion_helpdesk_ticket_seen; __init__.pyfrom . import models; manifest registers nothing extra (models auto).

  • Step 5: Run — PASS; Step 6: Commit

Task 7: Admin group

Files: Create fusion_helpdesk/security/fusion_helpdesk_groups.xml; Modify __manifest__.py (add to data, FIRST so the group exists before ACLs reference it if needed)

  • Step 1:
<odoo>
  <record id="group_reporter_admin" model="res.groups">
    <field name="name">Helpdesk Reporter Admin</field>
    <field name="comment">Can view all tickets filed from this deployment in the in-app inbox.</field>
  </record>
</odoo>

Odoo 19: NO users/category_id fields on res.groups. Keep the record minimal.

  • Step 2: Upgrade clean; Step 3: Commit

Task 8: Read endpoints (my_tickets, ticket_detail, unread_count)

Files: Modify fusion_helpdesk/controllers/main.py

  • Step 1: Add a mockable RPC seam + identity helper:
def _identity(self):
    user = request.env.user
    cfg = self._read_config()
    return {
        'email': (user.email or user.login or '').strip(),
        'label': cfg['client_label'],
        'is_admin': user.has_group('fusion_helpdesk.group_reporter_admin'),
        'cfg': cfg,
    }

def _rpc(self, cfg, model, method, args, kw=None):
    uid, proxy = self._authenticate(cfg)   # existing
    return proxy.execute_kw(cfg['db'], uid, cfg['password'], model, method, args, kw or {})
  • Step 2: Implement endpoints (all type='jsonrpc', auth='user'). my_tickets builds the scoped domain via build_scope_domain, search_read fields [id, name, stage_id, write_date], plus a per-ticket latest public support message id (read message_ids or a dedicated query), then computes has_unread via the seen map. ticket_detail re-resolves the ticket through the scoped domain (reject if absent), reads public messages only (filter via is_public_message using each message's subtype internal flag fetched from central), and calls _mark_seen. unread_count returns compute_unread_count(...).

Execution detail: fetch message subtype "internal" flag from central by reading mail.message fields [author_id, date, body, message_type, subtype_id] and resolving subtype_id.internal via a second read or by filtering message_type='comment' + excluding notes. Confirm the cleanest field set against the live mail.message model during execution.

  • Step 3: Manual: upgrade module; Step 4: Commit

Phase 3 — Reply endpoint (client)

Task 9: ticket_reply

Files: Modify fusion_helpdesk/controllers/main.py

  • Step 1: Endpoint /fusion_helpdesk/ticket/<int:ticket_id>/reply, auth='user'. Re-resolve ticket via scoped domain (reject if not in scope). Resolve author partner on central by the replier's email (find-or-create via res.partner search/create through bot, or pass author_id resolved from partner_email). Post:
self._rpc(cfg, 'helpdesk.ticket', 'message_post', [ticket_id], {
    'body': body_html,            # already-safe HTML (escape user text)
    'message_type': 'comment',
    'subtype_xmlid': 'mail.mt_comment',
    'author_id': author_partner_id,
})
  • Step 2: Escape the user's text to HTML server-side (reuse _html_escape). Mark seen after posting.
  • Step 3: Manual upgrade; Step 4: Commit

Phase 4 — Client UI (dialog tabs, thread, badge)

Task 10: Dialog tabs + My Tickets list + thread + reply + confirmed email

Files: Modify static/src/js/fusion_helpdesk_dialog.js, static/src/xml/fusion_helpdesk_dialog.xml, static/src/scss/fusion_helpdesk.scss

  • Step 1: Add to state: tab:'new'|'list'|'thread', tickets:[], loadingList, current:{id,subject,messages,canReply}, replyBody, replyEmail (default from a new /fusion_helpdesk/whoami or seeded via session user email — read user.email via useService('user')/session), scope:'mine'|'all', isAdmin.
  • Step 2: Methods: openList() → rpc /fusion_helpdesk/my_tickets (with scope); openTicket(id) → rpc detail, switch to thread, refresh list badge; sendReply() → rpc reply then reload thread; setScope() (admin toggle). Add confirmed Your email input on the New tab bound to state.replyEmail, passed as reply_email in submit payload.
  • Step 3: Template: a tab header (New | My Tickets); New pane = existing form + email field; List pane = table (ref, subject, stage chip, unread dot) + admin Mine/All toggle; Thread pane = messages (author, date, body, attachments) + reply box + Back. Use Markup-safe rendering: render message bodies with t-out (OWL) since central returns sanitized HTML.
  • Step 4: SCSS for tabs/list/thread (follow Odoo kanban hex pattern + dark-mode $o-webclient-color-scheme branch per CLAUDE.md).
  • Step 5: Manual QA locally (dialog opens, tabs switch). Step 6: Commit

Task 11: Systray unread badge

Files: Modify static/src/js/fusion_helpdesk_systray.js, static/src/xml/fusion_helpdesk_systray.xml, SCSS

  • Step 1: On setup, call /fusion_helpdesk/unread_count; store state.unread. Poll on an interval (e.g. 120s) and on dialog close. Show a badge bubble when unread > 0.
  • Step 2: Badge markup over the icon. Step 3: Commit

Phase 5 — Central acknowledgement email

Task 12: Branded acknowledgement template + send-on-create

Files: Create fusion_helpdesk_central/data/mail_template_ack.xml; Modify models/helpdesk_ticket.py, __manifest__.py

  • Step 1: mail.template on helpdesk.ticket with subject "We received your request [{{ object.ticket_ref }}]" and a body using the company email layout + a prominent button to {{ object.get_base_url() }}{{ object.access_url }} (magic link). Canadian English.
  • Step 2: Send on create via a create-override (central inherit), gated:
@api.model_create_multi
def create(self, vals_list):
    tickets = super().create(vals_list)
    tmpl = self.env.ref('fusion_helpdesk_central.mail_template_ticket_ack', raise_if_not_found=False)
    for t in tickets:
        if tmpl and t.partner_email and t.x_fc_client_label:   # in-app channel only → avoid double-ack with native web form
            tmpl.send_mail(t.id, force_send=False)
    return tickets

Decision: gate on x_fc_client_label so only in-app-channel tickets get OUR ack; external web/email customers rely on native confirmation (verify native behavior during deploy; widen the gate if native sends nothing).

  • Step 3: Register template data in manifest; Step 4: Commit

Phase 6 — Review, fix, deploy, smoke test

Task 13: Code review + fix

  • Run the code-review skill / pr-review-toolkit code-reviewer + silent-failure-hunter over the diff. Fix HIGH/MEDIUM findings. Re-run client tests locally. Commit fixes.

Task 14: Deploy + test central on odoo-nexa

  • Copy/confirm fusion_helpdesk_central source is visible to odoo-nexa (/opt/odoo/custom-addons).
  • Run module tests on nexa: -u fusion_helpdesk_central --test-enable --test-tags /fusion_helpdesk_central --stop-after-init (ephemeral http port). Fix failures.
  • Upgrade live: -u fusion_helpdesk_central --stop-after-init then restart odoo-nexa-app.

Task 15: Deploy client on odoo-entech

  • Look up entech access (memory: DB admin; confirm container/SSH via Supabase quick_commands). Confirm entech's fusion_helpdesk.client_label (e.g. ENTECH) + remote config points at nexa.
  • Ensure fusion_helpdesk source present on entech; upgrade -u fusion_helpdesk --stop-after-init; restart.

Task 16: Smoke test (one ticket)

  • From entech: file ONE test ticket via the dialog (or simulate the controller path).
  • On nexa: confirm the new ticket has partner_id resolved, partner_email/partner_name/x_fc_client_label set, customer is a follower, ack email queued/sent.
  • Reply as agent on nexa → confirm notification email to the reporter w/ magic link; confirm the entech dialog "My Tickets" shows the ticket + reply and the badge increments.
  • Confirm pre-existing identity-less tickets are untouched (the "lots already submitted" set) and do NOT leak across deployments in the inbox query.

Self-Review (run before execution)

  • Spec coverage: keystone (T1-3), label field+views (T3-4), scoping (T5,8,9), seen/badge (T6,10,11), admin group (T7), ack email (T12), portal/native (config — verified live, no code), tests (T1,5,6 local + T3 enterprise), deploy+smoke (T14-16). ✓
  • Placeholders: none — code shown for all Python/XML; JS tasks specify state/methods/markup concretely. JS is manually QA'd (OWL unit tests out of scope).
  • Type consistency: build_scope_domain(label,email,is_admin), is_public_message(msg), compute_unread_count(tickets,seen), _mark_seen(central_ticket_id,last_message_id), _seen_map(ids), x_fc_client_label — names consistent across tasks. ✓