Odoo · Transacciones · PostgreSQL
Cada petición HTTP y cada ejecución de cron en Odoo es una única transacción de PostgreSQL: o se confirma todo, o no se confirma nada. Ese modelo es cómodo hasta que necesitas que algo falle un poco: crear un registro que quizá ya existe, seguir procesando un lote aunque un elemento falle, o guardar un log de error que no desaparezca con el rollback.
El síntoma
«current transaction is aborted»
Cuando PostgreSQL rechaza una sentencia (una clave duplicada, un CHECK que no se cumple, una FK rota), marca la transacción entera como abortada. A partir de ahí rechaza cualquier consulta hasta que se haga rollback. Si tu código captura la excepción y sigue como si nada, la siguiente consulta, que puede estar en otro módulo, falla con un error que no tiene nada que ver con la causa real.
# ❌ Captura el error pero deja la transacción inservible
try:
self.env["product.tag"].create({"name": name})
except Exception:
pass # "ya existe, no pasa nada"
# La siguiente consulta revienta:
# psycopg2.errors.InFailedSqlTransaction:
# current transaction is aborted, commands ignored until end of transaction block
self.env["product.tag"].search([("name", "=", name)])
La solución
cr.savepoint() alrededor de lo que puede fallar
Un savepoint delimita un bloque: si dentro salta una excepción, PostgreSQL deshace solo ese bloque y la transacción principal sigue sana. En Odoo, self.env.cr.savepoint() además vacía las escrituras pendientes del ORM al entrar y al salir, así que el error salta dentro del bloque y no más tarde.
from psycopg2.errors import UniqueViolation
def _get_or_create_tag(self, name):
Tag = self.env["product.tag"]
try:
with self.env.cr.savepoint():
return Tag.create({"name": name})
except UniqueViolation:
# Otro proceso la creó entre medias: la transacción sigue válida
return Tag.search([("name", "=", name)], limit=1)
Este patrón «intenta crear y, si existe, búscalo» es más robusto que «busca y, si no existe, créalo» cuando hay concurrencia real (webhooks que llegan a la vez, varios workers procesando la misma cola): entre el search y el create otro proceso puede adelantarse, y solo la constraint de unicidad lo detecta de verdad.
except Exception alrededor de un savepoint también se traga errores de programación y de permisos. Si esperas un duplicado, captura UniqueViolation; si esperas una validación, ValidationError.
Lotes
Procesar 500 registros aunque fallen 3
En un cron que procesa un lote, un error en un elemento no debería tirar los otros 499. Un savepoint por elemento aísla los fallos; el registro que falla se marca para revisarlo y el resto se confirma al final del cron.
def _cron_sync_pending(self):
for record in self.search([("sync_state", "=", "pending")], limit=500):
try:
with self.env.cr.savepoint():
record._sync_one()
except (UserError, ValidationError, requests.RequestException) as exc:
record.write({"sync_state": "error", "sync_error": str(exc)})
Logs
Un log de error que sobreviva al rollback
Si guardas el log de una llamada a una API en la misma transacción y luego la operación falla, el rollback se lleva también el log: justo el registro que necesitabas para depurar. La salida es escribirlo con un cursor propio, que se confirma de forma independiente.
def _log_api_call(self, endpoint, status, body):
with self.env.registry.cursor() as cr:
env = self.env(cr=cr)
env["my.api.log"].create({
"endpoint": endpoint,
"status": status,
"body": body[:10000],
})
# al salir del with, el cursor propio hace commit
Úsalo con moderación: un cursor propio es una conexión más al pool y no ve los datos sin confirmar de la transacción principal.
| Situación | Herramienta |
|---|---|
| Crear algo que puede estar duplicado | savepoint() + except UniqueViolation |
| Lote donde un fallo no debe parar el resto | Un savepoint() por elemento |
| Log que debe sobrevivir al error | registry.cursor() propio |
| Test que espera un error | self.assertRaises de Odoo (ya abre su savepoint) |
| Confirmar a mitad de un cron muy largo | self.env.cr.commit(), con cuidado e idempotencia |
Resumen
Un error de PostgreSQL aborta toda la transacción. Si quieres recuperarte de él, envuelve la operación en self.env.cr.savepoint() y captura la excepción concreta; si necesitas que un log sobreviva al fallo, escríbelo con un cursor propio. Capturar sin savepoint solo mueve el error a otra línea.