Skip to content

OD121 cursor-rollback

Business code rolls back the cursor, discarding the caller's work with its own.

Category Severity Series Needs the index
Correctness Warning all no

What it reports

A call to rollback() on a cursor the function was given: cr.rollback(), self.env.cr.rollback(), request.env.cr.rollback() or self._cr.rollback(), a cr parameter included. Each function is checked on its own, apart from the functions it nests, and sees the cursors the functions enclosing it open.

It stays silent on a rollback:

  • in a function that also makes a commit OD103 reports;
  • on a cursor the function binds itself: one a with statement yields, such as with registry.cursor() as cr: or with closing(registry.cursor()) as cr:, or one bound with = to anything but a cursor it was given, such as cr = registry.cursor() or cr = new_cr or self.env.cr;
  • on the cursor of an environment built on such a name or on a .cursor() call, with api.Environment(cr, ...), self.env(cr=cr) or with_env(), or of one a with statement yields;
  • in a block that ends by raising or by calling sys.exit(), since the transaction is lost anyway;
  • under if <cursor>.readonly:, where a read-only cursor holds no write to lose;
  • in _handle_error() of an ir.http class, or in its _handle_exception() for a module before 16.0: the hook Odoo calls once the request has failed.

A name holds a cursor or an environment the function binds only when every path reaching the rollback binds it so, and a function nested in the one binding it sees it unless a parameter of its own hides it, both as OD103 describes.

Why it matters

Odoo runs each request, cron job and RPC call in one transaction, and the cursor's rollback() ends that whole transaction. Everything done before it is gone: the records the caller created, the messages it posted, the work of every override that ran first, none of which knows. The code then carries on in a new transaction, so the request ends with a success and commits only what came after the rollback, a state nobody wrote on purpose.

To abort, raise an exception and let Odoo roll back. To try a step that may fail and keep going, run it in a savepoint, which undoes that step alone:

with self.env.cr.savepoint():
    self._create_invoice()

Example

models/sale_order.py
class SaleOrder(models.Model):
    _inherit = "sale.order"

    def action_x_try_invoice(self):
        try:
            self._create_invoices()
        except UserError:
            self.env.cr.rollback()
            return False
        return True

If the order was confirmed in the same call, the rollback undoes the confirmation too, and the user is not told.

models/sale_order.py
class SaleOrder(models.Model):
    _inherit = "sale.order"

    def action_x_try_invoice(self):
        try:
            with self.env.cr.savepoint():
                self._create_invoices()
        except UserError:
            return False
        return True

Suppressing

self.env.cr.rollback()  # odooscan: ignore[OD121]

See suppressing a finding.