Odoo module upgrades, done for you
Migrating a sale-order rules suite from Odoo 15 to 17
Another pattern we see constantly: a compliance module that gates action_confirm(). Every deal must have a partner with a business ID, the customer's credit must have been checked, and the customer's previous order must be fully delivered before a new one goes through.
The Odoo 15 code
def action_confirm(self):
for order in self:
partner = order.partner_id
if not partner.vat:
raise UserError(_("%s has no business ID on file.", partner.name))
if not partner.x_credit_checked:
raise UserError(_("Credit check pending for %s.", partner.name))
previous = self.search([
("partner_id", "=", partner.id),
("id", "!=", order.id),
("state", "in", ("sale", "done")),
], order="date_order desc", limit=1)
if previous and previous.state != "done":
raise UserError(_("Previous order %s is not delivered yet.", previous.name))
return super().action_confirm()
Plus a red warning ribbon in the form view driven by attrs="{'invisible': [('x_credit_checked', '=', True)]}".
What broke โ and one thing that broke silently
- The loud one (Odoo 17): the
attrs=ribbon. View validation fails on install with "attrs" and "states" attributes are no longer used. Annoying but honest โ it becomesinvisible="x_credit_checked". - The silent one (Odoo 17):
sale.orderno longer has adonestate. Locking moved to a separatelockedboolean. The code above installs cleanly on 17 and never raises the delivery rule again โprevious.state != "done"is true for every order, but the domain("state", "in", ("sale", "done"))still matches, so the rule fires for every customer with any prior order, or (with slightly different logic) never fires at all. Either way the business rule the module exists for is broken, and nothing in the log says so.
The Odoo 17 version
The delivery rule needs to check what it always actually meant โ the pickings โ instead of a state that used to imply them:
previous = self.search([
("partner_id", "=", partner.id),
("id", "!=", order.id),
("state", "=", "sale"),
], order="date_order desc", limit=1)
undelivered = previous.picking_ids.filtered(
lambda p: p.state not in ("done", "cancel")
)
if undelivered:
raise UserError(_("Previous order %s is not delivered yet.", previous.name))
(Odoo 17 also introduced a delivery_status field on sale orders, which can simplify this further โ we use it when the target version has it.)
Why this case matters
This is the migration failure mode people underestimate. A module can install perfectly on the new version while its business logic is quietly wrong. That's why we don't stop at a clean install: we write tests that assert the rules โ confirm blocked without a business ID, blocked pending credit check, blocked with an undelivered order, allowed when all three pass โ and run them on the target version.
Try the working result before you pay a cent. No signup to get started.
Common questions
How would I even notice a silent break like the done-state one?
Usually a customer notices first โ orders confirming that shouldn't, or every order blocked. Diffing your module's assumptions against the target version's models is exactly the work of a port; it's why we test behavior, not just installation.