The CoC body now renders English + the French translation together, so the
separate "Certificat de Conformité (Français)" print option was redundant.
- Removed the action_report_coc_fr report action and the now-dead
report_coc_fr template; renamed action_report_coc_en to "Certificate of
Conformance" (print filename "CoC - <name>").
- fp_notification_template: dropped the per-partner-language EN/FR branch —
CoC email attachments always render the single bilingual action_report_coc_en.
- fp_hide_default_reports: dropped the FR sequence record.
- Refreshed the report_coc.xml design note.
- Bump reports 19.0.11.32.0, notifications 19.0.7.1.0.
Deployed on entech (-u removed the orphan FR action + template). Verified the
cert Print menu now lists only "Certificate of Conformance" and it renders
clean. The dead action_report_coc_fr ref in the uninstalled
fusion_plating_bridge_mrp is left as-is (module not loaded).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Changing Settings -> Certificate Owner didn't move existing certs: the signer
was snapshotted from the company owner at cert-creation time, and the CoC
prefers that snapshot over the live owner.
- _fp_create_certificates no longer freezes the company owner into
certified_by_id; it snapshots ONLY a deliberate per-spec signer. Empty
certified_by_id then resolves the LIVE company owner in the CoC report.
- action_issue lazy-fill made robust: resolves the company via the SO /
env.company (fp.certificate has no company_id) so it fills the CURRENT
owner at issue and the "Certified By" gate still passes.
- Settings help text corrected: signature comes from the user's Plating
Signature (Preferences -> My Profile), not "HR Employee".
- Data fix on entech: cleared certified_by_id on 5 stale draft CoCs with no
per-spec signer so they follow the current owner.
Bump certificates 19.0.9.3.0, jobs 19.0.11.4.0. Verified: CoC-30058 resolves
signer = Garry Singh (has Plating Signature), renders clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The cert form's x_fc_local_thickness_pdf field only stored the upload; only
the Issue Certs wizard parsed it. Add create/write hooks on the jobs-side
fp.certificate that, when a NON-PDF is written to that field, run the wizard's
parser: readings -> thickness_reading_ids, header metadata -> x_fc_thickness_*,
microscope image (RTF) -> x_fc_thickness_image_id, then relocate the source to
x_fc_local_thickness_evidence_id and clear the PDF field (mirrors the wizard's
non-PDF end state). Real PDFs pass through untouched for the page-2 merge.
Re-entry guarded via the fp_skip_thickness_parse context flag. Bump jobs
19.0.11.3.0.
Deployed + verified on entech: CoC-30065 (.doc) back-filled to 3 readings +
metadata (operator BK) + extracted microscope image, renders inline (242KB);
PDF cert CoC-30040-02 correctly left untouched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Column titles now render inline "English / French" on one line (was
stacked), cutting header height. First column drops "Line Item": it is
now Part Number / No. de pièce, Description / Description, Serial Number /
Numéro de série with a tight line-height.
- First-column DATA shows three lines — part number, part name, serial
number — via new fp.certificate._fp_resolve_part_identity() (part name
from the job's part catalog, serials from the matching SO line; blanks
fall back to "-"). Bump certificates 19.0.9.2.0, reports 19.0.11.31.0.
Deployed + verified on entech (CoC-30059: ('9876699373',
'VALVE BODY - COMPLETE - ASSY', ''), 243KB render).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Make every CoC classic-body column title bilingual — English (bold) over
the French translation (italic grey), matching Steelhead and the SO
report's stacked-header convention. Cert-info headers (Date of
Certification / Generated By / Work Order #) and line-item headers
(Process / Customer PO / Shipped / NC Qty / Customer Job No.) now show
both languages. First column carries Part Number / Line Item,
Description, and Serial Number, each translated. Bump reports 19.0.11.29.0.
Deployed + render-verified on entech (CoC-30065, 224KB).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
My prior change removed the .cert-statement-box border but the signature +
statement table was never bordered, leaving the whole section borderless.
Add class="bordered" so the two main columns (Certified By | Certification
Statement) get the outer box + divider like the other report tables; the
statement text keeps no separate inner box. Bump reports 19.0.11.28.2.
Deployed on entech (fusion_plating_reports upgrade clean).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The .cert-statement-box border was redundant next to the bordered tables;
render the statement as plain text (padding 0). Bump reports 19.0.11.28.1.
Deployed on entech (fusion_plating_reports upgrade clean).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Customer block: remove the (usually empty) customer-logo third column;
Address | Contact now split 50/50.
- Remove the heavy header bottom border (the Sale Order header has none) and
the hr.heavy rule between the customer block and the cert info table.
- Drop now-dead CSS (.fp-coc h1, hr.heavy, .customer-logo). Bump reports to
19.0.11.28.0.
Deployed + render-verified on entech (CoC-30065, 222KB PDF, no QWeb errors).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Drop the hard spec_reference gate on fp.certificate.action_issue. The
customer-facing description (_fp_resolve_customer_facing_description,
walks job -> SO line, reuses fp_customer_description) now drives the CoC
Process column; spec_reference prints only when an estimator fills it.
- CoC EN/FR reports swap web.external_layout for fp_external_layout_clean +
paperformat_fp_a4_portrait. New shared coc_header (company logo + address
left, Nadcap logo centre, title + Code128 barcode right) mirrors the Sale
Order header. Removed the 3-logo Nadcap/AS9100/CGP accreditation strip and
the body H1s; padding-top 0 on both body wrappers.
- Un-gate the Issue Certs wizard thickness upload (was invisible unless the
customer was thickness-flagged) so a Fischerscope report can be attached to
ANY cert; merge (page 2) + inline readings already render unconditionally.
- Update issue-gate tests, bump versions (certificates 19.0.9.1.0,
reports 19.0.11.27.0, jobs 19.0.11.2.0), record CLAUDE.md rule 14c.
Deployed + render-verified on entech (CoC-30065, 223KB PDF, no QWeb errors).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The NexaCloud->Odoo ledger now verifies every new invoice against its
SOURCE billing system before posting, instead of trusting NexaCloud's
unreliable created_at/status/paid_at:
- _fc_verify routes by stripe_invoice_id prefix (in_ -> Stripe REST,
lago: -> Lago REST) and returns source-truth
{invoice_date, void, draft, paid, paid_at, amount_paid}, or None when it
can't be determined/reached (left for the next run).
- _ingest_invoices(post=True, verified=...) uses the source invoice date
(and accounting date), and reconciles a payment ONLY when the source
confirms paid.
- _cron_sync_verified posts only finalized invoices; skips void + draft,
logs unverified for retry. Replaces the old _cron_ingest_recent.
Cron cron_fc_invoice_ledger is enabled daily on nexamain. First live run:
23 already-posted, 1 void + 2 Stripe drafts + 5 zero-amount all skipped,
0 new posted, ledger intact at $3,403.46.
Tests: routing/guards (no network), verified date+reconcile, and the cron's
void/draft/unverified filtering (sources patched). FCB_EXIT=0 on odoo-trial.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
_post_and_reconcile_paid: for invoices NexaCloud marks paid, set the ledger
entry's invoice_date AND accounting date to the original NexaCloud date,
post, then reconcile the Stripe payment dated to the actual paid_at. Unpaid
invoices stay draft. Per-invoice isolated. 76 tests green on odoo-trial.
One operator (e.g. "Gurpreet Singh") manages several distinct customer
businesses; naming partners from full_name mislabeled Mobility Specialties
Inc and Apex Vita Corporation as "Gurpreet Singh". Read the company field,
name the partner by company (mark is_company), and rewrite existing partners
so prior full_name-based names are corrected on re-ingest. 75 tests green.
Surfaced by the nexamain dry-run against real data:
- reader: cast invoice_items.invoice_id::text (uuid = text[] mismatch).
- readers: set_client_encoding('UTF8') — invoice descriptions contain "×".
- ingest: add a balancing line when invoice.subtotal != sum(items). 9 paid
base-plan invoices store the charge in subtotal with NO invoice_items, so
itemized ingestion under-recorded revenue by ~$1,143 (37%); the reconciling
line makes the Odoo invoice total match what Stripe billed.
74 tests green on odoo-trial.
Odoo becomes the accounting SoR by ingesting NexaCloud's real Stripe
invoices (read-only via the existing DSN) into native account.move
customer invoices: per-service-family income accounts, tax derived to
match the source invoice.tax, Stripe payments reconciled via
account.payment.register (invoice shows paid), idempotent on
x_fc_nexacloud_invoice_id, draft-first with bulk-post + a daily cron
(inactive). Plus a prune helper for the now-obsolete metered shadow data.
73 tests green on odoo-trial. Account codes use dots (Odoo 19 rejects '-').
Pivot from recompute-metered-billing to INGEST NexaCloud's real Stripe
invoices into Odoo account.move (posted + payment-reconciled + HST), driven
by the dual-run finding that 94% of NexaCloud revenue is Stripe service
invoices + add-ons + proration outside the per-deployment/CPU model. Full
accounting SoR, all history + ongoing, revenue split by service family,
draft-first rollout. Build/test on trial; reuses the read-only DSN + partner
mapping. Supersedes the metered direction for NexaCloud (engine kept inert).
Previous engagement notice used <blockquote> to style the findings
quote. Odoo's mail.thread renderer auto-tags every <blockquote> with
data-o-mail-quote-node="1" and the chatter UI then HIDES the content
behind a "..." widget — exactly the wrong UX since the findings are
the load-bearing content, not throwaway quoted text. Swapped both
quote blocks for styled <div>s with the same visual treatment (left
border, light background, padding) so they render fully inline with
no toggle.
Also expanded the notice to mirror more of what the owner sees in the
engagement email: now includes BOTH "Our reply" (the findings) and
"Summary sent to the owner" (the AI summary). The employee can see
the full context being used for the decision, not just the engineer's
reply. Skipped the Original Request section because the employee
wrote it themselves — would just clutter the thread.
white-space:pre-wrap preserves multi-line findings/summaries that the
engineer typed with line breaks. The two sections are visually
distinct: findings in light blue (matching the email's "Our Reply"
treatment), summary in light grey (matching "Summary for the
Decision" in the email).
Verified live on ticket #54: new message body has no <blockquote>,
no data-o-mail-quote attribute, and contains both section headers
with their content rendered inline.
Bumps fusion_helpdesk_central to 19.0.2.4.2.
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.
- CRITICAL: reconciliation upsert keyed on (service, partner, period) collided
when one customer has two deployments (two subs) in a period — the second
overwrote the first. Add external_subscription_id to the model + a
UNIQUE(service_id, external_subscription_id, period) constraint, and key the
upsert per subscription. New test proves two subs for one partner keep two rows.
- raise a clear error if the nexacloud service is missing (was a confusing
per-row failure).
- _fc_resolve_subscription: the integer fallback no longer reaches a different
service's tagged subscription (latent multi-service IDOR); live untagged subs
stay resolvable and the partner-link authz is unchanged.
Full suite green on odoo-trial.
_api_record_usage now resolves the target subscription via the source
app's own id (x_fc_nexacloud_subscription_id, scoped to the service)
before falling back to a direct Odoo sale.order id. This is what lets
NexaCloud push usage against the shadow subscriptions the importer
created from NexaCloud UUIDs — closing the flip-day mapping gap the
review flagged. Authz unchanged (partner must be linked to the service).
fusion.billing.reconciliation gains the compute: _compute_reconciliation
(flat + charge overage vs external, status match/delta at a tolerance) and
_reconcile_rows (resolve shadow sub -> flat + charge, upsert one row per
service/partner/period, per-row isolated). The wizard gains a read-only
_read_reconciliation_rows (NexaCloud usage cpu_hours*3600 + invoice-item
subtotals per YYYY-MM) and a "Run Reconciliation" button. 2a amended to
stamp x_fc_nexacloud_plan_id on shadow subs so reconciliation can find the
charge. Read-only on NexaCloud; writes only reconciliation rows (shadow
guarantees intact). 8 new tests, full suite green on odoo-trial.
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.
Previous fix (col=1 on the group) didn't work — Odoo still rendered
the group's string as a left-column label inside the form sheet's
flow, so the textarea got pushed into a narrow right column. The
summary field looked entirely missing because its content split
between the button row (on the right) and the textarea (collapsed
nowhere visible).
Right idiom (lifted from Odoo's own mail.compose.message wizard):
WIDE textareas live directly at the form level, not inside <group>.
Section titles use <separator string="…"> which renders as a
horizontal divider with the label above. The textarea then takes
the full sheet width naturally.
Same pattern applied to bulk mode for consistency.
Also moved Personal Note into the top compact group with Owner /
Owner Email since it's a one-line input that belongs with the
header info, not pretending to be a wide section.
Bumps fusion_helpdesk_central to 19.0.2.3.2.
Add action_test_connection — a read-only connectivity/schema check that
reports source row counts and imports nothing, the safe first step before
a dry-run. Wire a "Test Connection" button on the wizard. Document the
end-to-end run in the README: least-privilege read-only DB role SQL, the
fusion_billing.nexacloud_dsn system parameter (libpq DSN = NexaCloud's
URL minus +asyncpg), and the Test → dry-run → real-run flow. Refresh the
stale SCAFFOLD status. 53/53 green on odoo-trial.
User reported the rich text editor showing raw HTML tags as literal text
instead of rendering them as formatted prose. Root cause: Odoo's Editor
delegates content insertion to setElementContent() (web/core/utils/html.js),
which only takes the innerHTML branch when the content was flagged as safe
markup via owl's markup() helper. Plain strings fall through to the
textContent branch, which is what the user was seeing:
<p>Ask the client if the stairlift has power. Check:</p> <ul> <li>...
instead of the rendered paragraph + list.
The canonical html_field.js in @html_editor wraps its value with markup()
before passing it to the Wysiwyg config; I missed that detail.
FIX
- import markup from @odoo/owl
- in wysiwygConfig getter, wrap the saved content_html string with
markup() before assigning to config.content
- pass markup("") for empty content (avoids editor confusion with falsy)
- load-bearing comment to keep future refactors from re-introducing the bug
VERIFIED
- upgrade clean
- 7 stale asset bundles flushed, container restarted, login serves 200
- new bundle 014fee9 renders 10029808 bytes
- node --check PARSE_OK
- compiled bundle contains: content:rawHtml?markup(rawHtml):markup("")
which is exactly the markup-wrapped path the Editor wants
Bumped to 19.0.2.2.4.
Co-authored-by: Cursor <cursoragent@cursor.com>
The Findings and Summary fields rendered at half-width because their
enclosing <group> defaulted to col="2" — Odoo reserves a label column
even when the field has nolabel="1", so the textarea was squeezed
into the right half of the dialog while the left half sat empty.
Switch both groups to col="1" so the field uses the entire group
width. Also tag both fields with widget="text" explicitly (it was
inferred from the Text field type, but being explicit makes the
intent obvious to anyone reading the view) and migrate the button
row to a flex div so the helper text aligns with the button vertical
center.
Bumps fusion_helpdesk_central to 19.0.2.3.1.
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.
REGRESSION FROM b22bb11b (Wysiwyg integration).
While inserting the new Wysiwyg methods (wysiwygConfig getter, onWysiwygLoad,
onToggleSource) between setup() and the existing onMounted / onWillUnmount
hook calls, I accidentally closed setup() early with the new
`this.wysiwygEditors = {};` assignment. That left the original
`onMounted(async () => {...});` and `onWillUnmount(...);` calls dangling
INSIDE the class body but OUTSIDE any method - which is invalid JS.
JavaScript's class-body parser sees the bare `onMounted(async () => ...)`
and tries to interpret it as a method declaration where `onMounted` is the
name and the parens are the parameter list. `async () => {...}` is not a
valid parameter, so the bundle fails with:
Uncaught SyntaxError: Unexpected token '('
web.assets_web.min.js:28807
That single parse failure tanks the entire backend asset bundle, leaving
users with a completely blank screen on /odoo (and any other backend
route). Frontend bundle was unaffected.
FIX
Move the onMounted / onWillUnmount calls back inside setup() where they
belong. Add a load-bearing comment explaining why they must stay there so
this regression cannot silently come back during a future refactor.
VERIFIED
- line 51: setup() opens
- lines 87, 93: onMounted, onWillUnmount calls INSIDE setup
- line 142: _initDrawflow as a normal class method (outside setup)
- upgrade clean
- bundle 10029245 bytes, exactly one onMounted( occurrence in
FlowchartDesigner class body
- node --check on the freshly-rendered web.assets_web.min.js -> PARSE_OK
Bumped to 19.0.2.2.3.
Co-authored-by: Cursor <cursoragent@cursor.com>
Resolves findings from the post-build review:
- C1: a partial import was indistinguishable from success. action_run_import
now logs failed rows at ERROR (survives nexa's log_level=warn) and the
wizard shows red/amber banners with failed/skipped counts.
- H3: an unrecognized billing_cycle silently fell back to monthly (wrong
plan AND price). Now raised per-row -> failed[], never silently mis-billed.
- M5: a NULL plan price silently became a $0 line. Prices now preserve
NULL-vs-0.0; a missing price for the subscription's cycle is failed[].
- H2: post-connect query/schema errors now become a clean UserError, not a
raw SQL traceback (matches the connection-error path).
- M4: per-row failures now record the exception type and log a traceback.
- MED#3: charge plan_id set explicitly False so re-runs re-assert the
shadow-safe NULL even if it was changed between runs.
- HIGH-edge: re-run only rewrites x_fc_* on existing subs; partner_id/plan_id/
line are set at creation only (never rewrite immutable fields).
- account_link: partner email match is now case-insensitive (=ilike) to avoid
duplicate partners against a differently-cased pre-existing partner.
Shadow-safety invariant unchanged and re-confirmed. 52/52 green on odoo-trial.
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).
fusion.billing.import.wizard backfills NexaCloud into Odoo: read-only
psycopg2 reader (_read_nexacloud_rows, DSN from ir.config_parameter)
split from pure-Odoo writes (_import_rows/_do_import) so the logic is
unit-tested headless. Maps users→partners+links (reusing
_resolve_or_create_partner, stashing stripe_customer_id), plans→a
cpu_seconds charge catalog (included_quota=cpu_seconds_quota,
unit_batch=3600, $0.0075/core-hour, plan_id NULL), and deployments→one
DRAFT shadow sale.order per deployment with the flat price set
explicitly. Shadow-safe by construction: draft + no payment token +
charge plan_id NULL (rating cron is a no-op). Idempotent re-runs;
per-row savepoints isolate bad rows; dry-run rolls back. 11 tests,
50/50 green on odoo-trial.
price_per_unit was a Monetary field, so a realistic sub-cent rate like
$0.0075/core-hour was rounded to $0.01 on write, corrupting the rate.
Make it Float(16,6). Also stop _compute_billable from rounding the
overage amount to 2 decimals mid-calc — that lost the half-cent on
sub-cent rates and would drift against the source app, which keeps
usage amounts at 4 decimals and only rounds at the invoice total.
Now rounds to 6 dp (float-noise only); cent-rounding defers to the
invoice line. Exposed while building the NexaCloud importer.
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.
Smaller UX simplification on the client side: the owner is already a
contact in entech's address book, so picking one is faster + safer than
re-typing their email and name (and avoids typos creeping into the
approval-email To: header).
What changed:
- Entech settings: drop fhd_owner_email + fhd_owner_name char fields;
add fhd_owner_partner_id Many2one to res.partner exposed in the
same "Owner Approval" block as a single partner selector. Quick-create
+ create-and-edit kept enabled so admins can spin up a new partner
inline if the owner isn't already in the system.
- controllers/main.py::_read_config: derives owner_email + owner_name
from the selected partner via the new _resolve_owner_contact helper.
Missing / dangling partner id → blank email + name → central simply
won't see the keys and the Engage button stays disabled (correct
"not configured" behaviour).
- Nexa side: ZERO changes. Still receives owner_email + owner_name
strings on the ticket payload, still upserts client_key.owner_email/
name. The partner abstraction stops at the entech boundary.
- migrations/19.0.2.1.0/post-migration.py auto-resolves the legacy
fusion_helpdesk.owner_email ICP value to an existing res.partner
(lowest-id match on lowercased email), writes the new
fusion_helpdesk.owner_partner_id key, and deletes the obsolete
owner_email + owner_name ICP rows so a future reader doesn't trip
over stale config.
Verified live on entech: kris@enplating.ca → res.partner #2308 ("Kris
Pathinather"), legacy keys purged, controller._resolve_owner_contact
returns the expected (email, name). The piggyback payload is unchanged
so existing client_key sync continues to work without a central
redeploy.
Bumps fusion_helpdesk to 19.0.2.1.0. fusion_helpdesk_central stays at
19.0.2.0.0 (no central-side changes required).
Replace the plain <textarea> in the flowchart designer's node-editor
right-panel with Odoo 19's native rich text editor so admins write
formatted prose / lists / bold / links / inline images without typing
HTML tags. The raw <textarea> stays available behind a toggle for the
power-user case (pasting markup from elsewhere, debugging).
CHANGES
manifest:
- depends += 'html_editor' (provides @html_editor/wysiwyg)
- bumped to 19.0.2.2.1
components/flowchart_designer/flowchart_designer.js:
- import { Wysiwyg } from '@html_editor/wysiwyg'
- import { MAIN_PLUGINS } from '@html_editor/plugin_sets'
- register Wysiwyg in static components
- state.sourceMode boolean (default false = rich text mode)
- wysiwygConfig getter builds the EditorConfig for the SELECTED node;
onChange reads editor.getContent() and writes back into the same
selectedMeta.content_html the rest of the designer already uses,
so the save path is unchanged
- onWysiwygLoad(editor) captures the editor instance per dfId so the
onChange callback can resolve the right one when nodes switch
- onToggleSource flushes the current editor's content before flipping
modes so unsaved keystrokes don't get lost
components/flowchart_designer/flowchart_designer.xml:
- replaced <textarea>...</textarea> with a conditional block:
sourceMode == false -> <Wysiwyg t-key="'wysiwyg-' + selectedNodeId"
config="wysiwygConfig"
onLoad="onWysiwygLoad.bind(this)"/>
sourceMode == true -> <textarea class="font-monospace" rows="10"/>
- t-key forces the editor to re-mount with the freshly-selected node's
content; otherwise switching nodes would keep showing the first
selected node's HTML
- new toolbar row above the editor has a "HTML Source" / "Rich Text"
toggle button (eye / code icons) so the user can flip at will
- hint text updated to reflect what each mode supports
components/flowchart_designer/flowchart_designer.scss:
- widened the right editor panel from 320px to 360px to give the
Wysiwyg toolbar room to breathe
- new .fr-wysiwyg-shell rule frames the embedded editor with the same
border + background as the other form-controls in the panel, with
a min-height of 180px and max-height 320px so it scrolls when the
content grows. Pins .o-we-toolbar inside the shell so it stays in
view as the user scrolls long content.
The save path, the runtime renderer, and the data model are unchanged -
content_html is still sanitised HTML stored on fusion.repair.flowchart.node.
Verified on local westin-v19:
- upgrade clean (no errors, no warnings)
- login serves 200 after restart
- 4 stale asset bundles flushed; Drawflow JS still served 46KB at
/fusion_repairs/static/src/lib/drawflow/drawflow.min.js
- Wysiwyg export confirmed at
/usr/lib/python3/dist-packages/odoo/addons/html_editor/static/src/wysiwyg.js:25
- MAIN_PLUGINS export confirmed at plugin_sets.js:103
Bumped to 19.0.2.2.1.
Co-authored-by: Cursor <cursoragent@cursor.com>