Skip to content

[18.0][IMP] sale_order_type: flexible pricelist precedence#3

Open
dnplkndll wants to merge 5 commits into
18.0from
18.0-sale_order_type-flexible-precedence
Open

[18.0][IMP] sale_order_type: flexible pricelist precedence#3
dnplkndll wants to merge 5 commits into
18.0from
18.0-sale_order_type-flexible-precedence

Conversation

@dnplkndll
Copy link
Copy Markdown

@dnplkndll dnplkndll commented May 16, 2026

Summary

Adds a per-type precedence mode to sale.order.type so different types can resolve conflicts between their own propagated fields and the partner's defaults differently:

Mode Behavior
type_first (default — preserves legacy) Sale type value wins.
partner_first Partner property wins; sale type only fills gaps.
partner_only Sale type's propagated fields are ignored entirely.

A _sot_resolve helper consolidates the resolution across all seven propagated fields (pricelist, payment term, warehouse, picking_policy, incoterm, route, invoice journal). The five SO _compute_* overrides collapse to one-line calls into the helper.

Manual edits to SO fields are preserved across recomputes via trigger narrowing — re-firing only on type_id or partner_id change, matching the intent of lef-adhoc's #4273.

Why

The current module hard-wires "type wins" — partner pricelists, payment terms etc. are silently overwritten whenever a type carries a value. Community feedback (issue #3690, PR OCA#3019, PR OCA#4273) signals demand for configurable precedence. PR OCA#4328 (currently merged at branch HEAD) introduced an alert for the pricelist case but didn't change the rule. This PR makes the rule itself configurable while keeping legacy behavior as the default.

The precedence field on `sale.order.type` is only visible in developer mode (groups=\"base.group_no_one\") so normal sales-ops users don't see it. A company-level default for new types lives in Settings → Sales.

What changed

  • Models: new `precedence` selection on `sale.order.type`; new `sale_order_type_default_precedence` on `res.company` + the settings relation; new `sot_resolve` helper on `sale.order`; six new sibling `effective*` related fields on `res.partner` (paralleling `effective_pricelist_id` from [18.0] [FW] [IMP] sale_order_type: warn about partner and sale_order_type pricelist mismatch OCA/sale-workflow#4328 for full symmetry).
  • Views: dev-mode `precedence` field on the type form; adaptive partner-form alert that adjusts its wording based on the active mode; a subtle muted hint under the SO's pricelist when precedence is non-default.
  • Tests: new `TestPrecedence` class — six unit tests on `_sot_resolve` plus integration tests covering all seven fields and the new related fields. Tagged `post_install` to ensure chart-of-accounts readiness.
  • Docs: `DESCRIPTION.md` / `USAGE.md` / `CONFIGURE.md` updated.
  • Manifest: bumped to `18.0.2.0.0`.
  • No migration script — Selection default `type_first` preserves behavior on upgrade.

Test plan

  • All 30 `TestPrecedence` tests pass against a fresh DB.
  • Existing 24 `TestSaleOrderType` tests pass unchanged.
  • Pre-commit clean (ruff / ruff-format / pylint-odoo / oca-gen-addon-readme).
  • Doodba review env via `ledoent/erp` with this branch overlayed in `repos.yaml`.
  • UI screenshots for the three modes (alert variants, SO hint).

Notes for reviewers

  • Runboat is GitHub-App-bound to `OCA/*` only, so it won't fire here. Validation happens via a Doodba review env on `ledoent/erp` before any upstream OCA submission.
  • This is a review-only PR on the fork — not intended for direct merge. Once we've shaken it out with a real review env, the plan is to open an equivalent PR upstream against `OCA/sale-workflow`.

Companion module

Soft-dependency: when web_field_provenance is also installed, the _sot_resolve helper additionally stamps cascade provenance via _stamp_provenance(..., source="r", rule="Sale Order Type cascade") so the OWL badge surfaces in the form view (green cog on cascade-set fields, pencil on user-set fields, grey circle on defaults). Integration is gated behind hasattr(self, "_user_set") so this module installs cleanly with or without the companion.

Visual proof of the three badge states (against res.partner.title as a demo target — same machinery applies to any opted-in field on sale.order):

State Screenshot
Default default
Cascade cascade
User-anchored user

rrebollo and others added 3 commits May 6, 2026 07:38
…ner/none)

Three precedence modes configurable per `sale.order.type`, exposed in
developer mode only:

  - `type_first`  (default — preserves legacy behavior)
  - `partner_first` — partner's value wins; type fills gaps
  - `partner_only` — type's propagated fields are ignored

A `_sot_resolve` helper consolidates the resolution rule for the seven
fields the module propagates (pricelist, payment term, warehouse,
picking_policy, incoterm, route, invoice journal). Existing computes
collapse to a one-line call into the helper.

Adds sibling `effective_*` related fields on `res.partner` for full
symmetry with the `effective_pricelist_id` introduced in PR OCA#4328.
Adapts the partner-form alert to the active mode and adds a subtle SO
form caption when precedence is non-default.

Manual edits to SO fields are preserved across recomputes via trigger
narrowing — re-firing only on `type_id` / `partner_id` change (matches
the lef-adhoc OCA#4273 intent).

Tests: new `TestPrecedence` class — six unit tests on `_sot_resolve`
plus integration tests across the seven fields and `effective_*`
related fields. Existing 24 tests unaffected.

No migration script needed: the new `precedence` field defaults to
`type_first`, preserving behavior on upgrade. A company-level
`sale_order_type_default_precedence` is exposed in Settings as the
default-for-new-types convenience knob.
…nance (Huly OCA-23)

`_sot_resolve` now accepts an optional `fname` argument; when the
companion module `web_field_provenance` is installed, manual user
edits to that field win over the precedence rules regardless of mode.
Falls through silently when `_user_set` is not on the record, so this
module continues to work standalone.

Threads field names through the seven existing call sites
(warehouse_id, picking_policy, payment_term_id, pricelist_id,
incoterm, route_id). The integration is one `hasattr` check inside
the helper — zero behavior change without OCA-23 installed.

Documents the pattern in DESCRIPTION.md so reviewers see the upstream
roadmap and the standalone vs. composed semantics.
@dnplkndll
Copy link
Copy Markdown
Author

Follow-up: forward-compat hooks for web_field_provenance (Huly OCA-23)

Pushed bef06115:

  • _sot_resolve now takes an optional fname argument.
  • When record._user_set(fname) is available (i.e. web_field_provenance is installed — see Huly OCA-23) and returns True, the manual edit wins over the precedence rules regardless of mode.
  • Without OCA-23 installed it's a no-op — zero behavior change vs. the prior commit.

This sets the table for the user's question about preserving a manual Net 30 → CC/Prepay → Net 30 round-trip across subsequent type changes. The dirty-bit infrastructure lives in OCA-23's web_field_provenance module (an ORM-level provenance map + _origin fallback + Salesforce/SAP-style badge widget). When it lands, installing it on top of this PR is sufficient — no further changes here.

Discussion + cross-system research summary on the Huly issue (just commented there with the integration sketch and concrete probe cases).

…provenance is loaded

Wires the cascade-attribution call into every compute that propagates
a type field onto the SO. New helper `_sot_stamp_cascade_if_available`
on both `sale.order` and `sale.order.line`:

  - no-op when `web_field_provenance` (Huly OCA-23) is not installed
    (soft dependency via `hasattr(self, "_stamp_provenance")`)
  - no-op when the resolved value did NOT come from the type
  - otherwise calls `_stamp_provenance([fname], source="r",
    by="sot.cascade", rule="Sale Order Type cascade")` so the OWL
    badge shows the green-cog icon and the tooltip reads
    "Set by Sale Order Type cascade".

Wired through six cascade sites: warehouse_id, picking_policy,
payment_term_id, pricelist_id, incoterm on sale.order; route_id on
sale.order.line. `_prepare_invoice` (journal_id) is intentionally not
stamped here because the target record is the account.move, not the
sale.order.

Soft-dependency guard means the wiring is safe to ship without
web_field_provenance installed. Integration tests follow in the next
commit (commit-then-test workflow; will squash for clean review).
dnplkndll added a commit to ledoent/web that referenced this pull request May 17, 2026
Two integration bugs surfaced when wiring the first consumer
(sale_order_type cascade via ledoent/sale-workflow#3):

* **Onchange diff leak**: stamping `_provenance` via `write()` during
  a compute leaked the field into Odoo's onchange diff. Form views
  that don't declare `_provenance` (most do not — the badge consumes
  it via `web_read` only) raised `KeyError: '_provenance'`. Switched
  `_stamp_provenance_keys` to raw SQL + `invalidate_recordset` so the
  stamp does not surface as an in-flight ORM change.

* **NewId records cannot be SQL-updated**: skip records whose `id` is
  not yet a database integer. This affects two paths:
    1. Form / onchange — stamping is the wrong thing anyway, see above.
    2. `create()` precompute — there is no DB row to UPDATE.
  Both paths fall back to the cascade attribution being applied on
  the next persistent-record write that touches the field, which
  matches typical user flows (the cascade re-fires when a user
  changes `type_id` on a saved SO). Documented in the docstring; a
  deferred-flush mechanism is the right long-term fix and stays on
  the ROADMAP.

Existing 23 web_field_provenance tests still pass; the sibling
sale-workflow PR's 60-test suite goes 60/60 once the SO cascade is
exercised on persistent records (separate commit there).
…r() in cascade computes

Discovered while wiring cascade attribution into web_field_provenance:
the original _sot_resolve short-circuit checked _user_set(fname) and
returned current_value to preserve manual edits. But by that point
super() had already overwritten current_value with the partner
default, so the "preserve" returned an empty/wrong value.

The correct pattern is the one documented in web_field_provenance's
_user_set docstring: filter user-anchored records OUT of super()
before it runs.

This commit:

* Adds SaleOrder._sot_preserve_user_set(fname) and a line-level
  mirror SaleOrderLine._sot_preserve_user_set(fname). Both return an
  empty recordset when web_field_provenance is not installed (soft
  dependency, hasattr guard).
* Each cascade compute now does:
    preserved = self._sot_preserve_user_set("<field>")
    target = self - preserved
    res = super(<Class>, target)._compute_<field>()
    for ... in target.filtered("type_id"):
        ... resolve + stamp via cascade
* _sot_resolve no longer consults _user_set (was misleading — the
  check happens at the compute-filter level now). The fname
  parameter is kept for caller-side parity.

Wired through five SO computes (warehouse_id, picking_policy,
payment_term_id, pricelist_id, incoterm) and the SaleOrderLine
route_id compute. _prepare_invoice unchanged.

Tests in the next commit. With both installed, the full suite goes
60/60 (TestPrecedence + TestPrecedenceWithProvenance +
TestPrecedenceMultiCompany + TestSaleOrderType + provenance suite).
@dnplkndll dnplkndll closed this May 18, 2026
@dnplkndll dnplkndll reopened this May 18, 2026
@dnplkndll dnplkndll closed this May 19, 2026
@dnplkndll dnplkndll reopened this May 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants