Module
contract_line_successor, verified against 18.0.1.0.1.9 on the
current 18.0 branch.
Summary
Two related findings when calling the public surface of
contract.line from an external client over XML-RPC / JSON-RPC:
- Concrete bug:
stop(date_end) crashes with TypeError on a
plain ISO-string date argument — reproducible deterministically.
- API-design observation: the natural "close + successor with
overrides" flow that the bundled contract_price_revision wizard
implements has no public-method equivalent — the wizard reaches
into _prepare_value_for_plan_successor, so external callers that
want the same pattern have to either inherit the model or replicate
the helper. A small public-API surface here would let RPC callers
use the addon without leaning on a leading-underscore method.
1. Reproducing the stop() TypeError
The public model method contract.line.stop(date_end) receives
date_end as an ISO string when called over XML-RPC, and the first
comparison against the field value crashes.
import odoorpc
o = odoorpc.ODOO("localhost", port=8069)
o.login("db", "admin", "admin")
line_id = o.env["contract.line"].search(
[("is_stop_allowed", "=", True)], limit=1,
)[0]
o.execute_kw(
"contract.line", "stop",
[[line_id], "2026-08-31"],
)
Result:
odoorpc.error.RPCError: '<' not supported between instances of
'str' and 'datetime.date'
Originating at the date comparisons in stop():
def stop(self, date_end, manual_renew_needed=False, post_message=True):
...
for rec in self:
if date_end < rec.date_start: # str < date
rec.cancel()
else:
if not rec.date_end or rec.date_end > date_end: # date > str
...
Confirmed: the crash happens before any write, so the
contract.line row is left untouched — no partial state on the
RPC error path.
Root cause
XML-RPC / JSON-RPC marshalls Python datetime.date arguments to
ISO strings on the wire. The ORM coerces date values on field
assignment (write() / create()), but it does not coerce
positional method arguments back to date. So the public method
receives str where it expects date, and the first comparison
against a fields.Date value crashes.
The bundled wizards work fine because they construct date objects
in Python and call stop() in the same transaction.
Suggested fix
A single coercion at the entry of stop():
def stop(self, date_end, manual_renew_needed=False, post_message=True):
date_end = fields.Date.to_date(date_end)
...
fields.Date.to_date is a no-op when the argument already is a
date, so in-process callers are unaffected.
2. Related observation: plan_successor() has the same root
cause but is hard to reach via RPC
plan_successor() also takes positional date arguments and calls
get_next_invoice_date internally with them — get_next_invoice_date
does relativedelta arithmetic that would fail the same way.
In practice, plan_successor()'s validator
(is_plan_successor_allowed) gates the path: it requires
not is_auto_renew and date_end >= context["date_start"]. With
typical contract lines (auto-renew=True, open-ended date_end) the
validator raises ValidationError before the date arithmetic runs,
so the TypeError is masked. The bug is still present in the code
path; it's just hard to surface with normal data.
The same single-line coercion at the entry of plan_successor() (or
inside _prepare_value_for_plan_successor's top, behind the gate)
would close it.
3. API observation: the "close + successor + override" pattern
needs a public entry point
The bundled contract_price_revision wizard implements a useful
pattern that external callers reasonably want to invoke:
# contract_price_revision/wizards/contract_price_revision.py
date_end = self._get_old_line_date_end(line)
line.stop(date_end)
new_line = line.copy(self._get_new_line_value(line))
line.update({"successor_contract_line_id": new_line.id})
This composes stop() (public) with _prepare_value_for_plan_successor
(leading-underscore) and copy(vals) (public). The result is "close
the predecessor, create a successor with arbitrary overrides on top
of the OCA-prepared vals, link them bidirectionally".
plan_successor() is the closest public method, but its semantic is
different — it extends a contract line with a new period within or
overlapping the existing window (validator requires
line.date_end >= new.date_start). The wizard's close-then-restart
case doesn't fit that semantic, which is why the wizard bypasses
plan_successor() and uses the leading-underscore helper directly.
That leaves external callers (who can't inherit the model) without a
public way to invoke this flow. They have to either:
- Build the close+create+link sequence themselves, losing OCA's
full field-copy via _convert_to_write(self.read()[0]),
get_next_invoice_date for recurring_next_date, the
predecessor link, the chatter audit message, and the
is_plan_successor_allowed / is_stop_allowed validators; or
- Wrap the model in a local
_inherit addon (which is what we
ended up doing) to call the leading-underscore helpers in-process.
Suggestion (open to direction)
Either of these would help external callers:
- (a) Promote
_prepare_value_for_plan_successor to a documented
extension point. Add the same fields.Date.to_date coercion at
the top, and note in its docstring that it's the supported way for
addons (or, via RPC, for thin wrappers) to compute successor vals.
- (b) Add a public wrapper method, e.g.
succeed_line(date_start, date_end, is_auto_renew, overrides=None, post_message=True) that does `stop() + _prepare_value_for_plan_successor
- .copy(overrides on top) + bidirectional link
in one call, with ISO-string coercion at the entry. Thecontract_price_revision`
wizard could then call this instead of duplicating the sequence.
Happy to submit a PR for any of the above (the stop() coercion is
trivially low-risk; the public-wrapper option is more design-touch
and benefits from a maintainer steer). Just wanted to surface the
findings first.
Thank you for the addon — it's the right shape for what we needed,
and the only friction was on the RPC entry path.
Module
contract_line_successor, verified against18.0.1.0.1.9on thecurrent
18.0branch.Summary
Two related findings when calling the public surface of
contract.linefrom an external client over XML-RPC / JSON-RPC:stop(date_end)crashes withTypeErroron aplain ISO-string date argument — reproducible deterministically.
overrides" flow that the bundled
contract_price_revisionwizardimplements has no public-method equivalent — the wizard reaches
into
_prepare_value_for_plan_successor, so external callers thatwant the same pattern have to either inherit the model or replicate
the helper. A small public-API surface here would let RPC callers
use the addon without leaning on a leading-underscore method.
1. Reproducing the
stop()TypeErrorThe public model method
contract.line.stop(date_end)receivesdate_endas an ISO string when called over XML-RPC, and the firstcomparison against the field value crashes.
Result:
Originating at the date comparisons in
stop():Confirmed: the crash happens before any write, so the
contract.linerow is left untouched — no partial state on theRPC error path.
Root cause
XML-RPC / JSON-RPC marshalls Python
datetime.datearguments toISO strings on the wire. The ORM coerces date values on field
assignment (
write()/create()), but it does not coercepositional method arguments back to
date. So the public methodreceives
strwhere it expectsdate, and the first comparisonagainst a
fields.Datevalue crashes.The bundled wizards work fine because they construct
dateobjectsin Python and call
stop()in the same transaction.Suggested fix
A single coercion at the entry of
stop():fields.Date.to_dateis a no-op when the argument already is adate, so in-process callers are unaffected.2. Related observation:
plan_successor()has the same rootcause but is hard to reach via RPC
plan_successor()also takes positional date arguments and callsget_next_invoice_dateinternally with them —get_next_invoice_datedoes
relativedeltaarithmetic that would fail the same way.In practice,
plan_successor()'s validator(
is_plan_successor_allowed) gates the path: it requiresnot is_auto_renewanddate_end >= context["date_start"]. Withtypical contract lines (auto-renew=True, open-ended date_end) the
validator raises
ValidationErrorbefore the date arithmetic runs,so the TypeError is masked. The bug is still present in the code
path; it's just hard to surface with normal data.
The same single-line coercion at the entry of
plan_successor()(orinside
_prepare_value_for_plan_successor's top, behind the gate)would close it.
3. API observation: the "close + successor + override" pattern
needs a public entry point
The bundled
contract_price_revisionwizard implements a usefulpattern that external callers reasonably want to invoke:
This composes
stop()(public) with_prepare_value_for_plan_successor(leading-underscore) and
copy(vals)(public). The result is "closethe predecessor, create a successor with arbitrary overrides on top
of the OCA-prepared vals, link them bidirectionally".
plan_successor()is the closest public method, but its semantic isdifferent — it extends a contract line with a new period within or
overlapping the existing window (validator requires
line.date_end >= new.date_start). The wizard's close-then-restartcase doesn't fit that semantic, which is why the wizard bypasses
plan_successor()and uses the leading-underscore helper directly.That leaves external callers (who can't inherit the model) without a
public way to invoke this flow. They have to either:
full field-copy via
_convert_to_write(self.read()[0]),get_next_invoice_dateforrecurring_next_date, thepredecessor link, the chatter audit message, and the
is_plan_successor_allowed/is_stop_allowedvalidators; or_inheritaddon (which is what weended up doing) to call the leading-underscore helpers in-process.
Suggestion (open to direction)
Either of these would help external callers:
_prepare_value_for_plan_successorto a documentedextension point. Add the same
fields.Date.to_datecoercion atthe top, and note in its docstring that it's the supported way for
addons (or, via RPC, for thin wrappers) to compute successor vals.
succeed_line(date_start, date_end, is_auto_renew, overrides=None, post_message=True)that does `stop() + _prepare_value_for_plan_successorin one call, with ISO-string coercion at the entry. Thecontract_price_revision`wizard could then call this instead of duplicating the sequence.
Happy to submit a PR for any of the above (the
stop()coercion istrivially low-risk; the public-wrapper option is more design-touch
and benefits from a maintainer steer). Just wanted to surface the
findings first.
Thank you for the addon — it's the right shape for what we needed,
and the only friction was on the RPC entry path.