Bite-sized TDD plan: version model + part load/save helpers, save-on-confirm
hook, wizard + SO-line auto-load, and the part Descriptions-tab history list.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Dedicated fp.part.description.version model: latest auto-loads both internal +
customer-facing into a new order line; on SO confirm, a changed description
saves a new version titled "S#### · date". Browsable per-part history;
default_specification_text kept synced. SO surfaces only (not quotes).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The order-line onchange still auto-ticks "Save as Default" (so a new part's
spec is remembered next time) — only the explanatory popup is removed, per
client request. The ticked checkbox on the line is the cue now.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Carrier/service/weight inputs + Generate Label + Mark Shipped, shown when the
job is awaiting_ship and gated read-only ("Waiting on: WO-xxxx") until every
job on the order is ready. Reuses workspace card tokens; dark-mode accent
override included.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
generate_label (sudo'd FedEx machinery) and mark_shipped (as the technician),
both enforcing the order-level ship-together gate. /load now returns a
shipping block (carrier/service/weight + readiness + any existing label)
when the job is awaiting_ship.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
_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>
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>
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>
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>
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>
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.