Commit Graph

1403 Commits

Author SHA1 Message Date
gsinghpal
bb814a46ff feat(jobs): order-level ship-readiness helpers
_fp_order_ship_state + _fp_mark_order_shipped enforce spec D4 ship-together:
the order ships only when every active job on it is awaiting_ship/done.
Shared by the tablet shipping endpoints and /fp/workspace/load.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 09:14:36 -04:00
gsinghpal
be7256ce4c feat(logistics): technicians can create/edit delivery, POD, chain-of-custody
Spec D5 delivery-completion set. Dispatch records (route/vehicle/pickup)
stay read-only for technicians.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 09:13:11 -04:00
gsinghpal
d37f10f1c3 feat(receiving): technicians can count+close receivings from the tablet
ACL: grant group_fp_technician write+create on fp.receiving / line / damage.
sudo the internal sale.order x_fc_receiving_status write so a non-privileged
technician isn't blocked inside action_mark_counted / action_close.

Tests deferred to entech (local Docker unavailable this session).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 09:11:39 -04:00
gsinghpal
b98ee8a6fb docs(plating): implementation plan for technician receiving + shipping tablet
Bite-sized TDD plan across receiving ACL+sudo, delivery ACL, fp.job
ship-readiness helpers, shipping endpoints, and the workspace shipping
panel. Also patches the spec to record the sale.order status-write sudo
fix found during planning.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 00:50:23 -04:00
gsinghpal
df0de97a68 docs(plating): spec — technician receiving + shipping from the workstation tablet
Design for letting Technicians receive a confirmed order and ship a finished
order from the fp_job_workspace tablet surface. Receiving is ACL-only (the
panel + endpoints already exist); shipping adds a workspace panel + two
sudo-backed endpoints (generate label, mark shipped) gated on all order jobs
being awaiting_ship.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 00:35:40 -04:00
gsinghpal
49a0a953e5 fix(plating): single bilingual CoC — remove the separate French print action
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>
2026-05-28 23:33:00 -04:00
gsinghpal
64eb34cdff fix(plating): CoC signer follows Settings "Certificate Owner" (no stale freeze)
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>
2026-05-28 23:20:38 -04:00
gsinghpal
cd0c08f348 fix(plating): parse Fischerscope .doc/.docx/RTF dropped on the cert form
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>
2026-05-28 23:01:02 -04:00
gsinghpal
6a5364e053 fix(plating): compact CoC first column + 3-line part data
- 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>
2026-05-28 22:28:04 -04:00
gsinghpal
ec78fc148d feat(plating): fully bilingual CoC — all labels EN/FR
Convert every remaining CoC label from single-language (is_fr branch) to
bilingual EN/FR: document title, customer block (Name / Address / Contact /
Email / Phone), Fischerscope thickness report title + metadata (Equipment /
Product / Application / Directory / Calibration Std. / Operator / Measured /
Measuring Time), reading stats (Mean / Std Dev / Range), Source file,
Certified By, Name, and the Certification Statement heading. The statement
paragraph now prints both English and French. Reuses the SO report's inline
.fp-bl-en/.fp-bl-fr bilingual classes. Bump reports 19.0.11.30.0.

Deployed + render-verified on entech (CoC-30059 with thickness block, 243KB).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 22:11:40 -04:00
gsinghpal
9d9be17542 feat(plating): bilingual EN/FR column titles on the CoC
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>
2026-05-28 21:54:45 -04:00
gsinghpal
1d1bbfe612 fix(plating): border the CoC signature/statement table
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>
2026-05-28 21:37:28 -04:00
gsinghpal
b1257b6983 fix(plating): remove border around CoC certification statement
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>
2026-05-28 21:32:53 -04:00
gsinghpal
687decca28 fix(plating): clean up CoC layout — drop empty logo column + separating lines
- 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>
2026-05-28 21:25:32 -04:00
gsinghpal
307afbf3c0 feat(plating): CoC spec-optional + SO-style header + thickness for any cert
- 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>
2026-05-28 21:17:09 -04:00
gsinghpal
fecd2415f6 changes 2026-05-27 19:23:17 -04:00
gsinghpal
e36318f7a5 feat(billing): Stripe/Lago-verified go-forward sync + activate daily cron
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>
2026-05-27 18:37:36 -04:00
gsinghpal
feddca19d6 docs(billing): record verified backfill (Stripe+Lago) + go-forward verification caveat 2026-05-27 17:57:27 -04:00
gsinghpal
95378ff1da fix(billing): skip zero-amount invoices (no lines) — drop empty move, don't post nothing 2026-05-27 17:33:36 -04:00
gsinghpal
c8529b8a99 feat(billing): post + reconcile only PAID invoices, keeping original dates
_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.
2026-05-27 17:29:41 -04:00
gsinghpal
7a66d7849d fix(billing): name ledger partners by company, not the NexaCloud user's full_name
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.
2026-05-27 17:24:48 -04:00
gsinghpal
9ad09c32b0 fix(billing): robust shadow prune (charges before products + archive fallback) 2026-05-27 17:02:43 -04:00
gsinghpal
6b63df8c3d fix(billing): ledger live-run fixes — UUID cast, UTF-8, reconciling line
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.
2026-05-27 16:57:00 -04:00
gsinghpal
72d3130c88 feat(billing): NexaCloud invoice ledger — ingest invoices to account.move
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 '-').
2026-05-27 16:50:31 -04:00
gsinghpal
f6518b4d7e docs(billing): TDD plan for NexaCloud invoice ledger (ingest -> account.move, posted+reconciled+HST) 2026-05-27 16:44:21 -04:00
gsinghpal
bf6ee2bb2c docs(billing): design spec — NexaCloud invoice ledger (Odoo as accounting SoR)
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).
2026-05-27 16:33:46 -04:00
gsinghpal
077f898283 chnages 2026-05-27 16:12:22 -04:00
gsinghpal
779539d1b5 docs(billing): dual-run stand-up results — shadow import done, reconciliation 2 match / 7 delta (stopped before flip) 2026-05-27 15:57:54 -04:00
gsinghpal
34a65f9c4a fix(fusion_helpdesk_central): chatter notice no longer collapsed; adds summary
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.
2026-05-27 15:36:46 -04:00
gsinghpal
97cce8c755 docs(billing): record NexaCloud surgical deploy (inert; .env + Cursor WIP preserved) 2026-05-27 15:32:15 -04:00
gsinghpal
fe98fadf61 fix(fusion_helpdesk_central): engagement now posts public chatter for employee
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.
2026-05-27 15:31:58 -04:00
gsinghpal
32c7026558 feat(fusion_helpdesk_central): owner email shows 3 sections — Request / Reply / Summary
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.
2026-05-27 15:26:26 -04:00
gsinghpal
76866a7c76 fix(fusion_helpdesk_central): wizard dialog closed on Generate Summary click
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.
2026-05-27 15:12:33 -04:00
gsinghpal
f19ca02e05 docs(billing): record odoo-nexa deploy (installed, inert); NexaCloud deploy blocked on Cursor WIP 2026-05-27 15:12:16 -04:00
gsinghpal
1f5eaf0386 docs(billing): handoff update — sub-project #2 complete (2a/2d shipped, 2b/2c code-complete) 2026-05-27 14:52:28 -04:00
gsinghpal
a82f09ea50 fix(billing): reconciliation review fixes — per-subscription key, IDOR guard
- 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.
2026-05-27 14:51:43 -04:00
gsinghpal
a5144a925c feat(billing): /usage resolves subscription by source app id (enables 2b)
_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).
2026-05-27 14:37:30 -04:00
gsinghpal
2bdf4ef6a0 feat(billing): 2d dual-run reconciliation (Odoo-computed vs NexaCloud-actual)
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.
2026-05-27 14:34:23 -04:00
gsinghpal
3ba9f2821e docs(billing): spec + TDD plan for 2d NexaCloud dual-run reconciliation 2026-05-27 14:31:26 -04:00
gsinghpal
0104e87750 fix(fusion_helpdesk_central): Generate Summary crashed wizard with self-id collision
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.
2026-05-27 14:30:53 -04:00
gsinghpal
1f818096db fix(fusion_helpdesk_central): findings + summary actually span full width
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.
2026-05-27 14:17:23 -04:00
gsinghpal
bb873e8a7a feat(billing): importer Test Connection guard + operator runbook
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.
2026-05-27 14:16:32 -04:00
gsinghpal
d4ef4d55e0 fix(fusion_repairs): wrap Wysiwyg content with markup() so HTML renders, not escapes
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>
2026-05-27 13:55:20 -04:00
gsinghpal
fc8963da99 fix(fusion_helpdesk_central): findings + summary textareas span full width
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.
2026-05-27 13:53:38 -04:00
gsinghpal
c520803c84 feat(fusion_helpdesk_central): findings-first wizard, explicit Generate button
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.
2026-05-27 13:49:02 -04:00
gsinghpal
7349f3180d docs(billing): note flip-day usage-API subscription-id mapping for 2b 2026-05-27 13:45:23 -04:00
gsinghpal
2414b6328e fix(fusion_repairs): designer setup() scope - onMounted/onWillUnmount were stranded outside, broke entire backend bundle
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>
2026-05-27 13:44:58 -04:00
gsinghpal
5605012245 fix(billing): importer review fixes — surface failures, validate, dedupe
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.
2026-05-27 13:44:51 -04:00
gsinghpal
52849777dd feat(fusion_helpdesk_central): expose OpenAI key + cron settings in UI
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).
2026-05-27 13:36:44 -04:00
gsinghpal
6f060896bf feat(billing): 2a NexaCloud→Odoo importer (read-only, idempotent, shadow-safe)
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.
2026-05-27 13:34:47 -04:00