Bite-sized TDD plan: charge-type model + config UI, wizard charge/tax fields,
totals = one tax on (subtotal+charge), per-line lot pricing, SO-create tax on
all lines + typed charge line, and the express summary/line view changes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Direct/Express order entry: a searchable/creatable fp.additional.charge.type
replaces the fixed Tooling Charge; one order-level account.tax applies to
(subtotal + charge); per-line lot pricing (flat lot total, derived unit price,
qty preserved). Reordered summary. Quotes out of scope.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Grant Odoo Billing (account.group_account_invoice) to group_fp_manager via
implied_ids; Quality Manager + Owner inherit it. Billing only (not Accountant);
the SO-origin workflow gate in fusion_plating_jobs is unchanged, so managers
invoice from the Sale Order's Create Invoice action. Tests assert Manager/Owner
get Billing and Shop Manager does not.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Read-only per-part version history (version#, reference, customer-facing,
order, by/when) below the curated templates list.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wizard line (direct + express) and SO line now pre-fill BOTH internal +
customer-facing from the part's latest version (fallback to
default_specification_text), without clobbering typed text.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Each part-bearing line writes a deduped version (final order# + date) via
_fp_save_description_version, after the parent-number rename so the title
reflects the confirmed order number.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.