Staff Trial End Override #665

Merged
tobru merged 7 commits from feature/staff-trial-end-override into main 2026-09-10 13:40:48 +00:00
Owner
No description provided.
An organization converted from a trial to a paid limit profile keeps the
trial window Odoo was given at signup, so its usage stays free. Staff had
no way to end that window from the portal.

`set_odoo_trial_end` writes `trial_end_date` directly to the organization's
own Odoo sale order, at the close of the chosen local day plus the existing
safety margin. No portal field: Odoo stays the single source of truth, which
is only safe while the portal derives no window of its own, since a derived
window would later be recomputed over the manual value. That precondition,
plus the ones that need Odoo itself, is what `odoo_trial_end_block_reason`
and `odoo_trial_end_block_reason_for` express, so the model, the form and the
template cannot disagree about whether the write is allowed. Unlike the
derived pushes this raises on failure: staff triggered it and must see the
result.

The drift comparison the sanity check and the sync command each carried is
replaced by one `odoo_trial_window_drift` helper, so nothing can disagree
about what "in sync" means either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The staff organization page now shows the sale order's trial window - in the
Summary card and, with the portal's expected values when they differ, in the
"Change limit profile & trial" card - and offers a date input that writes the
trial end straight to Odoo.

The control is disabled with the reason spelled out whenever the write is not
allowed, but the reason is re-checked in `clean()`: a disabled input is simply
not submitted, so the rendering is an affordance, not the gate. The input is
prefilled with the current effective end, so shortening a window does not mean
transcribing a date from the display above, and the entered day is bounded to
[organization creation, today + 365 days] - a mistyped century would otherwise
free years of usage on one click, with no portal undo.

`odoo_datetime` parses Odoo's naive-UTC strings for `localtime_tag`, which
would otherwise emit an attribute browsers parse inconsistently and render an
unset field as the literal word "False".

The trial section is built from the stored organization, because
StaffOrganizationLimitProfileView re-renders this page with a rejected form's
changes still applied in memory and would otherwise describe a window Odoo
never saw.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A decommissioned organization looked like any other one on the staff detail
page, so its dead instances and namespaces read as live and its management
controls as usable. It now carries a badge next to the name and a banner
saying when it was decommissioned and that its billing history stays in Odoo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tests were reaching the real production Odoo from their setup code, and
nothing said so: the portal's Odoo paths swallow `Exception`, so an unmocked
call leaves the test green whatever it did on the far side.

The autouse `forbid_real_odoo` fixture intercepts `OdooClient._connect`, the
one lazy choke point every wire call passes through, and fails the test via
`pytest.fail` - a BaseException, so the production `except Exception` cannot
absorb the alarm. Reads are refused as loudly as writes: a test that wants a
degraded Odoo has to mock the call to raise, so what it exercises is visible
in the test rather than an accident of the guard.

The 48 tests that were reaching the read path are mocked, mostly through one
shared `mock_odoo_reads` fixture - their subject is elsewhere, but they render
a page that happens to consult Odoo. Two needed more than that:

- `test_origin_form_degrades_to_integer_input_when_odoo_down` now fails the
  underlying `search_read` instead of stubbing the getter, so it exercises the
  degraded path for real.
- `mock_odoo_failure` also mocks the portal-tag lookup, which runs first,
  reads through `core.odoo`'s own CLIENT and has no `try/except`. Without that
  the res.partner create failure `test_odoo_integration_failure_handling` was
  written for was never reached; the test now pins where the failure comes
  from.

`core.odoo`'s process-lifetime caches are rebound per test. One test was
poisoning `_PRICELISTS` with a sale-order row for the rest of the session,
which made the inventory of offenders order-dependent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The design doc carries the durable rationale: why no portal field is needed,
where a direct write would not be durable, the semantics of the entered date,
and what was deliberately left out. The plan file is a one-page as-built
summary - what shipped, the key decisions, where it diverges from the original
task plan, and the manual verification checklist, since none of the UI can be
checked visually by a test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat: fix button for a drifted Odoo trial window
All checks were successful
Tests / test (push) Successful in 1m54s
dcfd55ce69
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
refactor: move tenant-view link above the card on staff instance detail
All checks were successful
Tests / test (push) Successful in 1m22s
33b29fd401
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tobru merged commit 3311744e00 into main 2026-09-10 13:40:48 +00:00
tobru deleted branch feature/staff-trial-end-override 2026-09-10 13:40:48 +00:00
Sign in to join this conversation.
No description provided.