Sending an engagement triggered template.send_mail(), which logged the
outbound email to the chatter as a `notification` message with the
internal `Note` subtype. That's correct for nexa-side bookkeeping (we
don't want the raw email body propagating to the customer), but it
meant nothing public was posted — so the entech-side My Tickets inbox
showed no activity. The employee couldn't tell their request had been
escalated for approval.
_fc_reset_engagement now posts a follow-up public message via
message_post (subtype mail.mt_comment, message_type='comment') with:
⏳ Awaiting owner approval from <owner_name>.
Their decision will appear here when they reply.
Our reply:
> <findings text>
This survives the entech _public_messages filter (comment +
non-internal subtype) and propagates to the employee's My Tickets
thread, giving them context AND the engineer's reply without exposing
the raw outbound email or the owner's email address.
Smoke-tested live on ticket #54: re-engaged with the same owner, the
new mail.message (id=348213) is subtype=Discussions / internal=False /
message_type=comment, and contains both the awaiting-approval notice
and the findings text. _public_messages would surface it.
Bumps fusion_helpdesk_central to 19.0.2.4.1.
The owner only saw the AI summary, which was a paraphrase of the user
report — they couldn't see the actual request OR what we said back.
Restructure the engagement email into three sections so the owner can
read the conversation and not just the AI's take:
1. Original Request (from the reporter) — ticket.description, no
longer buried in a <details> collapsible at the bottom
2. Our Reply — the wizard's "Your Findings" text, now persisted on
the ticket so the email template can render it directly. This is
the engineer's analysis / response to the request.
3. Summary for the Decision — the AI-generated brief
Approve / Reject buttons stay below all three. Bulk email mirrors the
same per-card structure.
New ticket field x_fc_engagement_findings (Text, copy=False) stores
the findings at send-time so they survive as audit history. Wizard's
_action_send_single / _action_send_bulk pass findings into
_fc_reset_engagement; bulk uses per-line findings + per-line summary.
Mail templates are in <data noupdate="1"> so a plain -u doesn't
re-import them. Pre-migration in migrations/19.0.2.4.0/pre-migration.py
deletes the existing template records + ir_model_data so the upgrade's
data load re-creates them with the new body_html. Pre- (not post-)
because data load happens between the two phases.
Smoke-tested live on nexa: rendered template HTML contains all three
section headers at the expected positions with their expected content
markers (ORIGINAL FROM RIYA in Original Request, REPLY-FROM-GURPREET
in Our Reply, the summary text in Summary for the Decision).
Bumps fusion_helpdesk_central to 19.0.2.4.0.
Previous fix (return True from action_generate_summary) prevented the
self-id crash but introduced a worse regression: Odoo's web client
auto-closes target='new' modals on any non-action return — the
"wizard done" convention. So Generate Summary updated the field and
then immediately killed the dialog, leaving the user with no chance
to click Send Engagement.
The only Odoo-19 idiom that reliably keeps a wizard dialog open
across a button click is to return an act_window dict that re-opens
the same wizard record (res_id=self.id). That was the original
approach — it crashed because of the active_id self-id collision in
default_get. With the active_model='helpdesk.ticket' guard now in
place (from 0104e877), the re-open is safe.
Belt-and-suspenders: the re-open action passes context={} explicitly,
so even if a future change to default_get drops the active_model
guard, there's no parent-form active_id leaking in to confuse the
ticket lookup. The wizard record is loaded by res_id directly; Odoo
19 doesn't call default_get for record loads, only for new-record
creation.
Centralised the re-open logic in _reopen_action so single + bulk
modes share the same code path.
Bumps fusion_helpdesk_central to 19.0.2.3.4.
Repro: open the engagement wizard on a ticket, write findings, click
'Generate Summary from Findings'. Notification: "Ticket N no longer
exists" and the whole dialog closes — even though the ticket clearly
exists in the DB.
Root cause was two compounding bugs:
1. action_generate_summary returned an act_window dict with
res_id=self.id to "stay open after writing the summary field". The
web client honoured that by opening a NEW act_window — and the new
action's context inherited active_id=<wizard_id> (because that's
the res_id of the action being opened). Wizard ids are not ticket
ids, but our default_get didn't know the difference.
2. default_get read ctx.get('active_id') unconditionally, without
first checking ctx.get('active_model') == 'helpdesk.ticket'. So
when active_id pointed at the wizard's own id, default_get fed
that to _default_get_single, which raised "Ticket <wizard_id> no
longer exists" — and the user saw a confusing error about a
ticket that obviously DID exist (just not with that id).
Two fixes:
(a) action_generate_summary + action_generate_all_summaries now
return True. The form field write is visible to the client via
the call response; the wizard re-renders with the new
ai_summary populated. No spurious navigation, no context
pollution.
(b) default_get only consults active_id / active_ids when
active_model is helpdesk.ticket. Explicit
default_ticket_id[s] context keys still take precedence and
aren't gated by active_model (they're the caller's strong
signal).
Verified live: opening the wizard with active_id=99999 and NO
active_model no longer raises 'Ticket 99999 no longer exists' —
just creates the wizard cleanly. The normal flow (default_ticket_id
+ active_model='helpdesk.ticket') still works as before.
Bumps fusion_helpdesk_central to 19.0.2.3.3.
The old flow fired OpenAI on wizard open with just ticket + chatter,
so the AI summary was just a paraphrase of what the user originally
reported — your engineering analysis (scope, limitations, recommended
approach) never made it to the owner. Restructure to a two-step flow:
1. Open wizard → empty findings + empty summary, NO OpenAI call
2. You write findings: scope / effort / approach / risk
3. Click 'Generate Summary from Findings' → OpenAI runs with
ticket + chatter + findings, where the prompt explicitly tells
the model to weight findings MORE THAN the original report
4. Review/edit, then Send
Bulk wizard mirrors the flow per line: each row gets its own
findings + summary, one 'Generate All Summaries' button fans out
parallel OpenAI calls using each line's own findings.
Updated SUMMARY_PROMPT to:
- Tell the model the support engineer's findings are authoritative
- Emit a bullet structure that leads with the recommendation, not
the user's restated ask
- Side with findings over the original report when they conflict
New tests cover:
- default_get does NOT fire OpenAI (regression guard for auto-AI)
- Findings text actually reaches the OpenAI prompt
- Send works with a manually-typed summary (no AI in the loop)
- Existing bulk + validation paths still pass with the new shape
Also folds in the deferred code-review #7: ThreadPoolExecutor now
explicitly cancels pending futures on timeout via
shutdown(wait=False, cancel_futures=True) so a slow OpenAI day can't
hold the wizard open for ceil(N/workers)*15s.
Bumps fusion_helpdesk_central to 19.0.2.3.0.
Smoke-tested live on nexa: opening the wizard makes zero OpenAI calls;
clicking Generate with findings='My findings: scope is XL, ~8h' makes
exactly one call and the findings text is verifiably in the prompt
body received by call_openai_chat.
Adding the 'Fusion Helpdesk Central' block to General Settings so the
three ICP keys the engagement flow reads are configurable from a real
form instead of forcing admins to open Technical → System Parameters.
Three settings, all wired via config_parameter= so the existing read
paths (engagement_wizard, _fc_send_engagement_reminders) keep working
unchanged:
- fusion_helpdesk_central.openai_api_key (password widget — doesn't
render plaintext on the form)
- fusion_helpdesk_central.openai_model (default 'gpt-4o-mini')
- fusion_helpdesk_central.engagement_reminder_days (default 3, 0
disables the reminder cron entirely)
Bumps fusion_helpdesk_central to 19.0.2.2.0.
Find under Settings → Fusion Helpdesk Central. The block has two
sub-sections: "Owner Approval — AI Summary" (key + model) and
"Owner Approval — Reminder Cadence" (days).
Adds a one-click 'loop the owner into the chatter' shortcut on the
ticket form — separate from the engagement approval flow, just keeps
the owner in the loop on ongoing communication.
What's new on helpdesk.ticket:
- x_fc_owner_display (computed Char): 'Kris Pathinather <kris@…>',
read live from fusion.helpdesk.client.key so a change to the owner
contact reflects immediately on every existing ticket.
- x_fc_owner_email_resolved (computed Char): email-only slice, drives
view visibility (the field + button only render when an owner is
configured).
- x_fc_owner_is_follower (computed Boolean): True when a partner with
the owner email is in message_partner_ids. Swaps the button for a
green 'Following' badge when the owner is already on the thread.
- action_add_owner_as_follower(): find-or-create the owner partner by
email and message_subscribe. Idempotent — second call is a no-op,
no duplicate partner. Raises UserError with a clear message if no
owner is configured.
View extension on the helpdesk ticket form: injects right after the
existing partner_id ('Customer') field in the customer side group,
so it reads as 'Customer | Owner Contact [Add as Follower]' — same
row, no layout shift when the state flips to 'Following'.
Tests cover the compute display in three states (configured,
no-client-label, no-owner-on-key), the action's three paths
(create-and-subscribe, reuse-existing-partner, idempotent-when-
already-following), and the UserError when nothing is configured.
Smoke-tested live on nexa: ticket with x_fc_client_label='ENTECH'
displays 'Kris Pathinather <kris@enplating.ca>'; first click adds
res.partner #723 to followers and flips owner_is_follower to True;
second click is a no-op.
Bumps fusion_helpdesk_central to 19.0.2.1.0.
Findings from the post-feature code review on commit 396170b4. Addresses
the two CRITICAL + one HIGH + two MEDIUM issues; rest are deferred.
CRITICAL #1 — magic-link token race:
Two near-simultaneous POSTs on the same /engagement/<token>/approve
could both SELECT state='pending' under READ COMMITTED, both post
chatter, and let the last writer flip the outcome. Now the POST path
does an atomic UPDATE helpdesk_ticket SET token=NULL WHERE token=%s
AND state='pending' RETURNING id — the loser gets no row back and
renders the friendly invalid-link page. Verified live: 2 concurrent
POSTs → 1 wins, 1 loses, exactly 1 chatter row.
CRITICAL #2 — reminder cron without per-row savepoint:
Per CLAUDE.md rule #14, a DB failure mid-loop aborts the whole
transaction and silently kills the rest of the batch. Wrap each row's
send_mail+write in `with self.env.cr.savepoint()`. Also corrected the
success-count log (was len(stale), now actual sent count).
HIGH #3 — turnaround pivot summed instead of averaged:
fields.Float defaults to SUM aggregator; meaningless for per-ticket
decision delays. Added aggregator='avg' so the pivot reads "avg
turnaround per ticket" not "summed wait time".
HIGH #4 — added test_concurrent_claim_only_one_wins regression test
that fires two real HTTP POSTs against the same token and asserts
exactly one wins + exactly one approval chatter row exists.
MEDIUM #6 — cron nextcall pinned to 09:00 tomorrow so reminders land
in business hours regardless of when the module was last upgraded.
MEDIUM #10 — escalate failed owner-partner-create from WARNING to
ERROR (via _logger.exception) since silent attribution to the bot
account is a real audit-trail confusion.
Deferred (follow-up commits): #5, #7 (executor cleanup), #8, #9,
#11–#14 — none are bugs, all spec-drift or hardening.
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).
Three coordinated changes on top of the section grouping:
1. **Mark as Critical** — a red chip on the New tab sets priority='3'
when submitted. The central post-create hook auto-applies a "Critical"
helpdesk.tag (shipped via fusion_helpdesk_central data XML, noupdate=1
so support can recolor without losing it on upgrade), giving support
a kanban-groupable signal that doesn't rely on remembering what
priority='3' means. Scoped to in-app-channel tickets only, so a
support agent manually setting Urgent on their own ticket isn't
silently tagged.
2. **KPI cards above the sections** — Total / Open / Closed / Critical
in a 4-up grid (auto-collapses to 2x2 under 540px). Each card uses
its own saturated gradient so it reads on both light and dark mode —
the dialog backdrop is irrelevant because the gradient brings its
own background. Counts are computed in JS from state.tickets so they
always match what's rendered below.
3. **Colored stage pills** — red Critical, green Solved, dark-yellow New,
orange Cancelled, blue for In Progress / Testing / On Hold. Critical
priority gets a *separate* red pill alongside the stage pill so you
keep stage info even on escalated tickets. Stage matching is
substring-based (lowercased) so a renamed "Resolved" or "Done" stage
on central still maps to the green pill.
Tests cover the new is_critical=True → priority='3' wiring and the
default omission so SLA / stage defaults keep working for normal
tickets. Bumps fusion_helpdesk to 19.0.1.7.0 and
fusion_helpdesk_central to 19.0.1.2.0. End-to-end smoke test verified
live: priority=3 + x_fc_client_label triggers the Critical tag.
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>