Commit Graph

1339 Commits

Author SHA1 Message Date
gsinghpal
45b698beb5 feat(configurator): per-customer default lead time on partner profile
Adds two Integer fields to res.partner:
  - x_fc_default_lead_time_min_days
  - x_fc_default_lead_time_max_days

Set once on the customer's Plating Defaults tab (Fulfilment group);
auto-copies onto every new Express Order via the existing
_onchange_partner_id hook. Operator can still override per-order
since the onchange only fills when the wizard field is still blank.

Field declaration lives in fusion_plating_configurator (alongside
the rest of the partner cascade reads). View edit lives in
fusion_plating_invoicing where the Plating Defaults tab already
hosts the other partner-level defaults (invoice strategy, deposit
%, delivery method, deadline-days). Invoicing depends on
configurator, so the fields are registered before the view loads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:38:36 -04:00
gsinghpal
de6336ba42 changes 2026-05-27 10:36:48 -04:00
gsinghpal
c876767755 Merge branch 'main' of https://github.com/gsinghpal/Odoo-Modules 2026-05-27 10:36:37 -04:00
gsinghpal
d1fc3d8720 fix(express): show Tax on totals + add tooling as real SO line
Three related fixes on the Express Orders totals card:

1. Totals card now breaks out Subtotal / Tax / Tooling Charge /
   Grand Total. Previously the "Subtotal" and "Grand Total" rows
   both read from total_amount (same value rendered twice) and no
   tax was shown at all. Customers on a fiscal position-mapped
   tax rate (Ontario HST, etc.) had their taxes silently dropped
   from the preview.

2. tooling_charge now feeds the Grand Total. The total_amount
   compute previously summed line subtotals only. Added a real
   SO line for the tooling charge in action_create_order so the
   eventual sale.order.amount_total matches the preview AND the
   invoice carries a "Tooling Charge" line item.

3. tax_ids is now visible as an optional column on the lines
   list. Operator can see + override the auto-applied tax per
   line. Default still comes from FP-SERVICE product mapped
   through partner.property_account_position_id (fiscal position).

New compute fields on fp.direct.order.wizard:
  - total_subtotal (sum of line.qty * line.unit_price, pre-tax)
  - total_tax (sum of line + tooling taxes via compute_all)
  - total_amount (subtotal + tax + tooling — was just subtotal)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:33:31 -04:00
gsinghpal
a78ceaba51 docs(claude): fusion_helpdesk deploy procedures + 2026-05-27 handoff
Durable: nexa/entech upgrade commands, central service-account Contact
Creation prerequisite, backup-outside-addons-path gotcha, smoke-tests-must-
call-the-controller lesson. Plus current deploy status + the one remaining
step (browser confirmation of My Tickets / New on entech).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:26:21 -04:00
gsinghpal
6c15a7b1cf feat(fusion_helpdesk): customer follow-up + embedded ticket inbox
Squash-merge of feat/helpdesk-customer-followup. The billing and
fusion_login_audit work from that branch is already on main (landed
separately); this lands only the helpdesk feature.

- Identity keystone: submit() forwards partner_email/partner_name/
  x_fc_client_label so the central Helpdesk find-or-creates the customer
  partner and subscribes them as a follower (enables reply emails + magic link).
- Embedded in-app 'My Tickets' inbox: server-side scoped read/reply RPC
  endpoints, per-user seen tracking (fusion.helpdesk.ticket.seen), systray
  unread badge. Defense-in-depth scope domain + _norm_email normalisation
  (wildcard emails cannot widen scope).
- fusion_helpdesk_central: x_fc_client_label field + list/search views +
  branded acknowledgement email template.
- Deployed and smoke-tested live: nexa central 19.0.1.1.0, entech client
  19.0.1.4.1 (requires Contact Creation on the central service account).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:23:33 -04:00
gsinghpal
45ddb444a7 docs(billing): handoff — note fusion_login_audit also landed on main 2026-05-27 09:06:16 -04:00
gsinghpal
9df3262d30 fix(fusion_login_audit): avoid duplicate row on bad-password
When the login string resolves to an existing user and the password is
wrong, BOTH overrides used to write a failure row:
  - _check_credentials wrapper: result=failure, reason=bad_password
  - _login wrapper (catching the propagating AccessDenied): result=
    failure, reason=unknown_user

Discovered in production smoke on westin-v19 after the deploy: a
single failed login for info@gsafinancialconsulting.com produced two
audit rows (one bad_password, one unknown_user). The unknown_user
label was wrong — the user IS in the system.

Fix: _login now checks whether the login string resolves to any user
BEFORE writing the unknown_user row. If yes, _check_credentials
already logged the attempt and _login skips. If no, the user lookup
in super() failed and _login is the only chance to log.

Regression test test_login_known_user_bad_password_single_row asserts
exactly one row per attempt and that the row carries bad_password
(not unknown_user) when the user exists.

30 tests green locally; production smoke on westin-v19 confirms:
one row per failed login, bad_password, IP 172.18.0.1 captured.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:59 -04:00
gsinghpal
5d9609b5ee chore(fusion_login_audit): release 19.0.1.0.0
Module is feature-complete per
docs/superpowers/specs/2026-05-26-fusion-login-audit-design.md:
- T1  Module skeleton + icon
- T2  fusion.login.audit model (16 fields, declarative Constraint+3 Indexes)
- T3  Security: ACL + admin-only record rule + 5 tests
- T4  _fc_build_event_vals context helper (UA parse, password safety)
- T5  Success hook: _update_last_login -> result=success row
- T6  Bad-password hook: _check_credentials wrapped
- T7  Unknown-user hook: _login override (instance method in 19)
- T8  res.users smart button + Login Activity tab (4 x_fc_* fields)
- T9  Standalone list/form/search/kanban + 2 actions + 3 menus
- T10 res.config.settings + General Settings section (4 knobs)
- T11 Failure-burst alert email + 60-min cooldown
- T12 Daily retention GC cron
- T13 5-min async geo enrichment cron (private/cache/HTTP)
- T14 View-visibility security tests for non-admin
- 29 tests pass; both crons active; 3 menus installed.

Out of scope for v1 (documented in spec): API-key auth, OAuth/SSO,
per-user self-service view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:59 -04:00
gsinghpal
622f133f05 docs(plan): inline corrections from T11/T12/T13 execution
Capture in the plan the Odoo 19 gotchas discovered during execution
that the original plan template missed:
- Test command requires --http-port=0 --gevent-port=0 (running
  container holds 8069).
- Declarative models.Constraint / models.Index (T2).
- res.users.groups_id renamed to group_ids (T3, T6).
- ir.rule groups is additive not restrictive (T3).
- mail.template inline-template ctx IS env.context (T11).
- ir.cron has no numbercall field in 19 (T12).
- registry.cursor() in tests is TestCursor; cr.commit() raises;
  use savepoints (T13).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:59 -04:00
gsinghpal
482f12256e test(fusion_login_audit): view-visibility checks for admin vs non-admin
Asserts the smart-button and Login Activity tab fields are stripped
from res.users get_view() for non-admin users, and present for
Settings admins. Locks down the contract behind the
groups="base.group_system" XML attributes on the form-inheritance
view (the inherited view record cannot carry groups itself per
CLAUDE.md rule #11; the gate must live on the inner nodes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:59 -04:00
gsinghpal
86b8e59c95 feat(fusion_login_audit): async geo enrichment cron
5-min cron processes up to 100 pending rows per pass: private IPs
short-circuit to state=private_ip; same-IP cache (30 days) avoids
duplicate ip-api.com calls; reverse DNS via socket with 1.5s timeout;
HTTP lookup respects ip-api''s X-Rl rate-limit header. Tests cover
private-IP shortcut, cache hit (no HTTP), and internal-state skip --
no network calls needed.

Per-row isolation uses cr.savepoint() instead of cr.commit() because
Odoo 19 TestCursor raises AssertionError on commit/rollback. Recorded
the gotcha as CLAUDE.md rule #14.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:59 -04:00
gsinghpal
1b8038d8e8 feat(fusion_login_audit): nightly retention GC cron
Adds _fc_retention_gc() that deletes rows older than the configured
horizon (default 365 days; 0 = keep forever). Registered as a daily
ir.cron. Tests verify both the delete path and the "keep forever"
short-circuit.

Also documents the Odoo 19 gotcha that ir.cron dropped the numbercall
field (the legacy "-1 = run forever" pattern now raises ValueError at
install time; just omit the field).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:59 -04:00
gsinghpal
a2d13cf83b feat(fusion_login_audit): failure-burst alert email + cooldown
Mail template + helpers (_fc_alert_*, _fc_recent_failure_count,
_fc_send_failure_alert) wired into _check_credentials so that crossing
the consecutive-failure threshold within the window queues exactly one
mail.mail per attempted login per 60-minute cooldown. Master switch
x_fc_login_audit_alert_enabled honoured. Recipients are members of
base.group_system with a non-empty email and share=False; the
__system__ superuser is excluded by Odoo''s default user filter.

Tests (3 new, 22 total green):
  test_failure_burst_queues_one_email
  test_cooldown_suppresses_second_alert
  test_alert_disabled_master_switch

setUp ensures base.user_admin has an email (fusion-dev''s admin user
ships without one; the only user with an email is __system__, which
is filtered out of standard res.users searches).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:59 -04:00
gsinghpal
6f6aa6e90a feat(fusion_login_audit): settings model + page section
Four x_fc_* fields on res.config.settings backed by ir.config_parameter:
retention_days (default 365, 0 = forever), alert_threshold (5),
alert_window_min (15), alert_enabled (True). New "Login Audit" block
on the General Settings page (gated by base.group_system on the block,
NOT on the inherited view record per CLAUDE.md rule #11).

CLAUDE.md gotchas added during this task:
  #5 Boolean config_parameter fields don't round-trip "False" as a
     string — IrConfigParameter.set_param deletes the row on falsy.
     Test with assertFalse, never assertEqual(..., "False").
  #6 ir.ui.view uses group_ids (Odoo 19 rename mirrored from res.users).
     Setting groups_id on an ir.ui.view record raises ValueError at
     install. (The XML attribute groups="..." on inner nodes is
     unrelated and still works.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:59 -04:00
gsinghpal
0513ea23a4 feat(fusion_login_audit): standalone views + menus
List, form, and search views for fusion.login.audit, plus a "Login
Events" full-history action and a "Failed Logins (24h)" pre-filtered
action. Both surface under Settings -> Technical -> Login Audit
(menu items gated by base.group_system). Views are no-create / no-edit
/ no-delete to enforce append-only at the UI layer too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:59 -04:00
gsinghpal
72aa28e6c4 feat(fusion_login_audit): smart button + Login Activity tab on res.users
Adds four x_fc_* fields on res.users: login_audit_ids (One2many),
login_audit_count (compute), last_successful_login (compute, stored),
last_login_ip (compute, stored). action_fc_view_login_audit returns
a window action scoped to the current user. View inheritance adds a
smart button to the button box and a "Login Activity" page to the
notebook, both gated by base.group_system on the inner XML nodes
(NOT on the view record — Odoo 19 forbids that; see CLAUDE.md rule #11).

Tests (2 new, 18 total green):
  test_computed_last_successful_login — uses registry cursor to commit
    the audit row so the stored compute picks it up across the
    TransactionCase boundary.
  test_action_view_login_audit_returns_window_action — smart-button
    action shape + domain scoping.

CLAUDE.md rule #11 added: inherited ir.ui.view records cannot have
groups/group_ids on the record; the gate must be on the inner XML nodes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:58 -04:00
gsinghpal
a7cf44249d feat(fusion_login_audit): hook unknown-user failures via _login
Overrides res.users._login. When the login string does not resolve to
any user, super() raises AccessDenied; we record a row with user_id=NULL
and failure_reason="unknown_user", then re-raise. Closes the gap where
typo'd or scanned logins would otherwise vanish from the audit trail.

The existing _fc_record_login_event helper writes through an independent
registry.cursor(), so the audit row survives the rollback that follows
the re-raised AccessDenied.

Note: in Odoo 19 _login is a plain instance method (not the classmethod
it was in earlier versions) and takes (credential, user_agent_env). The
original plan was written for the classmethod signature; corrected here
and recorded in CLAUDE.md rule #10 so future-Claude does not waste time
re-discovering it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:58 -04:00
gsinghpal
0e6ebe7bc6 feat(fusion_login_audit): hook bad-password failures via _check_credentials
Wraps res.users._check_credentials. On AccessDenied, records a row with
result=failure and failure_reason='bad_password' (or '2fa_failed' when
credential['type'] == 'totp'), then re-raises. Regression test asserts
the attempted password value never lands in any audit field.

The audit row is written through registry.cursor() (independent cursor) so
it survives the rollback that follows AccessDenied — in production
odoo/service/model.py::retrying resets the transaction and http.py closes
the cursor without committing, in tests assertRaises opens its own
savepoint. Either way an inline write would vanish. Tests
enter registry_test_mode and use manual try/except to keep the audit row
visible across the savepoint hierarchy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:58 -04:00
gsinghpal
dced0c66a4 feat(fusion_login_audit): hook successful login via _update_last_login
Overrides res.users._update_last_login to create a fusion.login.audit
row with result=success after the parent runs. The write goes through
sudo() + mail_create_nolog=True. Any exception in the audit path is
caught and logged but never propagates — a broken audit table must
never block a real user from logging in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:58 -04:00
gsinghpal
2ced576204 feat(fusion_login_audit): add _fc_build_event_vals context helper
Single helper builds vals for fusion.login.audit rows from the live
HTTP request, or falls back to ip=''internal'' + geo_lookup_state=''internal''
when there is no request. Parses UA into browser/os/device_type via the
bundled user_agents library. Never reads credential[''password'']. Tests
cover: no-request fallback, UA parsing on a Chrome/Windows UA, and the
regression that no password value leaks into the vals dict.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:58 -04:00
gsinghpal
61a0cb244f feat(fusion_login_audit): admin-only record rule + security tests
Record rule grants admins an unrestricted domain on the audit log;
ACL forbids write/create/unlink for every group (audit is append-only;
sudo() inside auth hooks is the only write path). Defence-in-depth
layering: ACL is the actual gate, the rule documents and locks down
admin access path.

Tests (5, all green) cover:
  test_admin_can_read_through_acl_and_rule — positive path through both.
  test_acl_blocks_read_for_regular_user    — base.group_user denied by ACL.
  test_acl_blocks_read_for_portal_user     — base.group_portal share user
                                             denied (sensitive data leakage
                                             surface closed at ACL layer).
  test_acl_blocks_write_for_admin          — append-only at the write boundary.
  test_acl_blocks_unlink_for_admin         — append-only at the unlink boundary.

Drop the redundant `from . import tests` from the root __init__.py —
Odoo's test loader imports `odoo.addons.<mod>.tests` directly; the
extra import was dead weight (and inconsistent with the repo pattern).

CLAUDE.md gotchas added during this task:
  #6 res.users.groups_id -> group_ids rename (test setUp pitfall).
  #6 ir.rule `groups` is additive, not restrictive — group-scoped
     rules only apply to users in that group, they do not restrict
     non-members. Default to letting the ACL gate; use rules for
     row-level filters ACLs cannot express.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:58 -04:00
gsinghpal
aeea670064 feat(fusion_login_audit): add fusion.login.audit model
- All 16 columns per spec (user, attempted_login, result, failure_reason,
  event_time, ip/geo fields, user_agent triple, device_type, database).
- Check constraint binds failure_reason presence to result value.
- Three composite indexes (user+time, login+time, geo_state+time) supporting
  the per-user, failure-burst, and geo cron queries.
- Minimal admin-read ACL added so subsequent tests can verify writes.
- 3 TransactionCase tests passing: model create, failure_reason nullable on
  success, geo_lookup_state='internal' accepted.

Odoo 19 deprecation note: this implementation uses the declarative
models.Constraint and models.Index attributes (Odoo 19 silently drops the
legacy `_sql_constraints = [...]` list and `init()`/raw-SQL pattern with
only a warning). Captured in CLAUDE.md rule #9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:58 -04:00
gsinghpal
b0836e1c93 feat(fusion_login_audit): module skeleton + icon
Empty installable module with manifest, package inits, and icon.
Subsequent tasks add the audit model, hooks, views, and tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:58 -04:00
gsinghpal
a32946be44 docs(plan): fusion_login_audit implementation plan
15 TDD tasks targeting ~28 tests:
T1 skeleton+icon, T2 model+indexes, T3 security, T4 capture helper,
T5 success hook, T6 bad-password hook, T7 unknown-user hook, T8 user
form (smart button + tab + computes), T9 standalone views + menus,
T10 settings + page section, T11 failure-burst alert + cooldown,
T12 retention GC cron, T13 async geo enrichment cron, T14 view
visibility security tests, T15 manual smoke + release tag.

Self-reviewed: every spec section maps to a task; no placeholders;
method and field names consistent across tasks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:58 -04:00
gsinghpal
01a85c475c docs(spec): fusion_login_audit design
Durable login audit for Odoo 19 (westin-v19). Captures successful and
failed authentications via _update_last_login / _check_credentials /
_login overrides, surfaces history on res.users as a smart button +
"Login Activity" tab (admins-only), async geo-enriches IPs via ip-api.com
through network_logger, 365-day retention with daily GC cron, and
emails Settings admins on N consecutive failures for the same login
within a configurable window.

Motivation: a spot audit of GSA Accounting (uid 63) showed Odoo's
res_users_log keeps only one row per user (rest is GC'd), /var/log/odoo
is empty (warn-level stdout logging), and the container json log
rotates within days — leaving no durable login trail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:58 -04:00
gsinghpal
43b2edcbb5 @
docs(billing): session handoff — core on main, sub-project #2 (NexaCloud) next

Captures resume state for the centralized-billing initiative: core engine done
and on main, the 4-chunk decomposition of sub-project #2 (NexaCloud adapter +
dual-run reconciliation), the pending "where to start" decision, open questions,
and the test/branch workflow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@
2026-05-27 08:56:28 -04:00
gsinghpal
d770c0c3a9 fix(billing): resolve code-review findings (authz, cross-billing, validation, webhook integrity)
- C1/H4: rating cron only rates subs on the charge's own plan_id
- C1: _fc_rate_usage skips creating a line when amount is 0 (still updates existing)
- C2/C4: /usage authorizes each event (exists + is_subscription + linked customer)
- C3: API handlers validate input and return 4xx-shaped errors instead of raising;
       controller maps status=='error' to HTTP 400
- H1: cron uses real billing window [last_invoice_date or start_date, next_invoice_date)
- H2: _aggregate uses half-open window anchored on period_start
- H3: idempotency scoped to (subscription_id, metric_id, idempotency_key)
- H5: webhook stores canonical body, signs+POSTs it verbatim, adds X-Fusion-Event-Id,
       caps backoff at 2**min(attempts,10)
- H6: SSRF guard rejects non-https / localhost / private / link-local webhook_url
- M7: charge_model reduced to standard/package (dropped unimplemented graduated/volume)
- L1: currency_id required on charge + reconciliation
- L2: charge price non-negative + unit_batch positive DB constraints

Adds 17 regression tests (suite 22 -> 39, all green via fcb_test_on_trial.sh).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 08:42:08 -04:00
gsinghpal
a5db0fe71e feat(billing): usage-rating + webhook-dispatch crons
- SaleOrder._fc_rate_usage: aggregates usage, computes overage via
  charge._compute_billable, upserts sale.order.line for the overage product
- FusionBillingUsage._cron_rate_open_periods: hourly cron iterates active
  charges × in-progress subscriptions, calls _fc_rate_usage
- data/ir_cron.xml: two crons — rate usage (hourly), dispatch webhooks (2 min)
- __manifest__.py: registers data/ir_cron.xml in data list
- test_usage.py: test_rate_open_period_creates_overage_line (TDD, FCB_EXIT=0)

Reference: _create_recurring_invoice / _get_invoiceable_lines confirmed in
Enterprise sale_subscription/models/sale_order.py — overage line goes onto
sale.order so native invoicing picks it up via _get_invoiceable_lines.
2026-05-27 08:42:08 -04:00
gsinghpal
c44fd89ed1 feat(billing): wire HTTP controllers to API handlers 2026-05-27 08:42:08 -04:00
gsinghpal
6c395709cf feat(billing): outbound webhook engine (HMAC + retry/backoff)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 08:42:08 -04:00
gsinghpal
0754d0b101 feat(billing): subscription creation handler (sale.order is_subscription)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 08:42:08 -04:00
gsinghpal
2435096f32 feat(billing): inbound API handlers (customer/usage/catalog)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 08:42:08 -04:00
gsinghpal
25952cf226 feat(billing): period usage aggregation by metric function
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 08:42:08 -04:00
gsinghpal
eb1ee85d24 feat(billing): idempotent usage ingestion
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 08:42:08 -04:00
gsinghpal
1e34a67384 feat(billing): metered charge math (quota + overage)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 08:42:08 -04:00
gsinghpal
a1cfab6fe9 feat(billing): identity resolution external account -> partner 2026-05-27 08:42:08 -04:00
gsinghpal
a46e31e710 feat(billing): service API-key generation + matching
Add _match_api_key() class method to fusion.billing.service, with a
TDD test suite (TestServiceApiKey) covering key generation, hash storage,
positive match, and rejection of bad/inactive keys. Also fix
fcb_test_on_trial.sh to use --http-port 8070, as Odoo 19 forces
http_spawn() even under --no-http when --test-enable is set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 08:42:08 -04:00
gsinghpal
032b10752e test(billing): odoo-trial Enterprise test runner + plan test-env fix
Local dev Odoo is Community (can't install the module). Add a guest-exec runner
that syncs the module to the odoo-trial Enterprise sandbox (VM 316, db trial) and
runs --test-enable there; pass = FCB_EXIT=0. Scaffold verified installing on
Odoo 19.0 Enterprise (7 fusion_billing_* tables created).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 08:40:51 -04:00
gsinghpal
e7d63a3859 docs(billing): core engine implementation plan (TDD, 11 tasks) 2026-05-27 08:40:51 -04:00
gsinghpal
2b47bd8b10 feat(billing): design + scaffold fusion_centralize_billing
Centralize billing for all NexaSystems services (NexaCloud, NexaDesk,
NexaMaps, custom apps, memberships) on the Odoo 19 Enterprise instance,
replacing Lago. The module adds only the metering + integration layer;
native sale_subscription / account_accountant / payment_stripe do all the
financial work (invoicing, HST, dunning, portal, credit notes, Stripe).

Includes:
- Design spec (docs/superpowers/specs/2026-05-27-nexa-billing-centralized-design.md):
  6 locked decisions, architecture, data model, usage engine, Lago-shaped
  API, webhook control loop, NexaCloud pilot, phased dual-run migration.
- Module scaffold: 7 fusion.billing.* models (service, account.link, metric,
  charge, usage, webhook, reconciliation), bearer-auth API controller shell,
  security ACLs, README. Compiles on Odoo 19.0; engine/API bodies are stubs
  pending the implementation plan.
- CLAUDE.md rule #15: no sale.subscription model in Odoo 19 — a subscription
  is a sale.order(is_subscription) + sale.subscription.plan (verified live).

Task 0 verified: a single Stripe account is shared across NexaCloud and all
Lago providers, so no Stripe account/card migration is required.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 08:40:51 -04:00
gsinghpal
2f74d5ecb9 fix(plating): add 3 missing icons to process.node Selection
fp.step.template rows already held 'fa-bathtub' (1), 'fa-flag' (2),
and 'fa-undo' (2) — all plating-relevant and presumably valid in an
earlier version of the Selection list. When step_insert snapshot-
copied these into a fresh fusion.plating.process.node via
_copy_snapshot_fields, the ORM rejected them with
ValueError: Wrong value for fusion.plating.process.node.icon
because they weren't in the curated 39-icon list anymore.

Adding 'fa-bathtub' (bathtub / tank / soak), 'fa-flag' (flag /
milestone / gate), and 'fa-undo' (undo / rework / rerun) to the
process.node Selection. Aligns the two lists (template uses
_get_icon_selection -> node._fields['icon'].selection at runtime).

No data migration needed — existing template rows immediately
re-validate against the wider Selection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:43:38 -04:00
gsinghpal
f8abadfc18 fix(configurator): OPEN button errored on missing action.views
FpExpressActionBtns.onOpen called action_open_part which returned an
ir.actions.act_window dict without a 'views' key. Odoo 19's
_preprocessAction in the web client tries to .map over action.views
and throws TypeError: Cannot read properties of undefined (reading 'map').

Fix: include 'views': [[False, 'form']] alongside view_mode='form' on
both copies of action_open_part (wizard line + sale.order.line).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:40:27 -04:00
gsinghpal
164b775206 feat(views): partner Aerospace group + recipe Certificate Output + cert banner
Three view edits to surface the new cert toggles + workflow nudges:

1. res.partner — Plating Documents tab gains a "Aerospace / Defence"
   separator + group with the three new toggles (Nadcap / MTR /
   Customer-Specific). All boolean_toggle widget, default OFF.

2. fp.process.node — Recipe form gains a "Certificate Output" group
   visible only when node_type == 'recipe'. Five requires_* toggles
   + a blue info banner explaining the suppress-only precedence.

3. fp.certificate — Certificate PDF tab gains a yellow alert banner
   when certificate_type is one of the three orphan types AND no
   attachment is set. Tells the operator "this type expects a PDF
   you upload from disk".

Sub: docs/superpowers/specs/2026-05-27-recipe-cert-toggles-design.md
Task: T6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:05:28 -04:00
gsinghpal
b7211468b2 feat(certificates): orphan-cert attachment gate + render early-return
Block fp.certificate.action_issue on Nadcap / Mill Test / Customer-
Specific certs when attachment_id is empty. These three cert types
are manual-attach only — operator uploads the supplier doc /
regulator-issued cert / filled customer template PDF before the
cert can be issued. Prevents shipping the customer an empty PDF.

_fp_render_and_attach_pdf gets an early-return guard so an orphan-
type cert never tries to render a CoC QWeb template.

Sub: docs/superpowers/specs/2026-05-27-recipe-cert-toggles-design.md
Task: T5. Makes test_orphan_cert_issue_blocks_without_attachment pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:04:10 -04:00
gsinghpal
fb6cccc8b1 feat(jobs): three-step cert resolver with recipe suppression
Rewrites fp.job._resolve_required_cert_types as a documented three-step
pipeline:

  Step 1 — partner + part flags (extended to read 3 new orphan-type
           partner toggles: x_fc_send_nadcap_cert / x_fc_send_mill_test
           / x_fc_send_customer_specific)
  Step 2 — recipe-level requires_* Booleans STRIP cert types from
           the wanted set (suppress-only — never adds)
  Step 3 — CoC + thickness bundling preserved (thickness collapses
           into CoC PDF as page 2)

Field-existence guards on partner/recipe attribute reads keep the
resolver robust if the certificates / plating module schemas drift.

Recipe is suppress-only per Q1 locked decision: customer/part is the
ceiling, recipe can only remove. Test 3 (test_recipe_cannot_add_certs_
customer_didnt_want) is the explicit regression guard.

Sub: docs/superpowers/specs/2026-05-27-recipe-cert-toggles-design.md
Task: T4. Makes the 5 resolver tests from T3 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:03:18 -04:00
gsinghpal
ae02164b78 test(jobs): 6 tests for recipe-level cert suppression + orphan gate
Six failing tests in test_recipe_cert_suppression.py covering the
full design surface:

  1. test_recipe_suppresses_thickness
  2. test_recipe_suppresses_nadcap_for_commodity_part
  3. test_recipe_cannot_add_certs_customer_didnt_want (suppress-only
     regression guard — recipe can never add types customer didn't ask for)
  4. test_part_override_coc_recipe_suppresses
  5. test_all_orphan_types_propagate (4-element output + bundling)
  6. test_orphan_cert_issue_blocks_without_attachment

These will all fail until T4 (resolver) and T5 (orphan-attach gate)
land. RED phase of TDD locked in via commit ordering.

Sub: docs/superpowers/specs/2026-05-27-recipe-cert-toggles-design.md
Task: T3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:02:18 -04:00
gsinghpal
a5063cc816 feat(plating): recipe-level cert suppression Booleans
Adds five requires_* Booleans on fusion.plating.process.node
(requires_coc, requires_thickness_report, requires_nadcap_cert,
requires_mill_test, requires_customer_specific), default True.

Recipe is SUPPRESS-ONLY: when False, the recipe never produces that
cert type even if the customer/part requested it. Default True =
existing recipes keep producing the same cert set they produce today.

Surfaced on recipe-level form (node_type == 'recipe'); resolver reads
from job.recipe_id which is always a top-level recipe node.

Post-migrate backfills NULL -> TRUE on existing nodes.

Sub: docs/superpowers/specs/2026-05-27-recipe-cert-toggles-design.md
Task: T2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 01:57:32 -04:00
gsinghpal
89267a9f41 feat(certificates): partner toggles for Nadcap / MTR / Customer-Specific
Adds three Boolean fields (x_fc_send_nadcap_cert, x_fc_send_mill_test,
x_fc_send_customer_specific) to res.partner, default False. Wires
aerospace/defence customers into the existing cert resolver so the
three orphan fp.certificate.certificate_type values become reachable.

Post-migrate idempotently backfills NULL -> FALSE on existing rows.

Sub: docs/superpowers/specs/2026-05-27-recipe-cert-toggles-design.md
Task: T1 of the implementation plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 01:56:36 -04:00
gsinghpal
e599daf4d9 plan: implementation tasks for recipe cert toggles + aerospace parity
Seven tasks, TDD-style:
  T1 — Partner toggles (3 booleans) + post-migrate backfill
  T2 — Recipe booleans (5 requires_*) + post-migrate backfill
  T3 — Six failing tests in test_recipe_cert_suppression.py
  T4 — Three-step resolver implementation
  T5 — Cert action_issue orphan-attachment gate + render guard
  T6 — UI views (partner separator + cert banner + recipe group)
  T7 — Deploy to entech + smoke runbook

Module version landings:
  fusion_plating_certificates  -> 19.0.12.0.0
  fusion_plating               -> 19.0.22.0.0
  fusion_plating_jobs          -> 19.0.8.1.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 01:55:02 -04:00