Saltar al contenido principal

Odoo AttributeError Recordset no tiene atributo - Guía de reparación

¿Presionó AttributeError en un conjunto de registros de Odoo el 18/17/19? Diagnostique nombres de modelos, conjuntos de registros vacíos y cambios de nombres de campos con solución de problemas completa para producción.

E
ECOSIRE Research and Development Team
|4 de mayo de 20266 min de lectura1.2k Palabras|

Este artículo actualmente está disponible solo en inglés. La traducción estará disponible próximamente.

Your Odoo code raises this on a routine ORM call:

Your Odoo code raises this on a routine ORM call:

AttributeError: 'sale.order' object has no attribute 'partner_invoice_email'

Or the empty-recordset variant:

AttributeError: 'sale.order()' object has no attribute 'amount_total'

Both fire on Odoo 17.0/18.0/19.0 and both look identical at first glance — but the empty parentheses in 'sale.order()' are the tell. They point at completely different bugs. This guide separates the two and fixes each properly.

Quick Fix

If the recordset is empty (the model name is followed by empty parens like 'sale.order()'), guard the access:

# WRONG — fails when search returns nothing
order = self.env['sale.order'].search([('name', '=', ref)], limit=1)
total = order.amount_total

# RIGHT — guard the empty case
order = self.env['sale.order'].search([('name', '=', ref)], limit=1)
total = order.amount_total if order else 0.0

If the field genuinely does not exist on the model, you have a typo or a missing dependency:

# Use the field that actually exists
email = order.partner_id.email   # not partner_invoice_email

Why This Happens

Odoo recordsets behave like Python objects but with one crucial difference: an empty recordset still answers attribute lookups on its model, but field accesses return falsy or raise depending on context. The error has four common roots:

  1. Empty recordset. search() returned nothing, but the next line assumes a record exists. .amount_total on an empty sale.order recordset evaluates fine in some flows and raises in others (especially after read() or mapped()).
  2. Field renamed between versions. Odoo 17.0 renamed several fields. Custom code referring to the old name breaks on upgrade.
  3. Custom field defined in a module that is not installed in the current database. Code expects x_studio_priority but the customization is only on production, not staging.
  4. Wrong model. A typo like self.env['sale_order'] (underscore instead of dot) returns the right model anyway in Odoo 17+ for legacy reasons, but self.env['sale.orders'] (plural) returns nothing — and accessing fields on nothing blows up.
  5. Subclass mismatch. A controller calls a method on a recordset it received via JSON-RPC, but the recordset's model is the parent class and the method lives on a child class.

Step-by-Step Diagnosis

1. Read the error precisely. Note the model and the missing attribute. The model name with empty parens means empty recordset. The model name without parens means the field genuinely does not exist on that model.

2. Confirm the field exists. In a shell:

'partner_invoice_email' in env['sale.order']._fields  # True or False

If False, the field is not on the model — typo, version rename, or missing module.

3. Check installed modules.

env['ir.module.module'].search([
    ('state', '=', 'installed'),
    ('name', 'in', ['my_custom_module']),
])

If your custom module is not installed in this database, fields it adds will be missing.

4. Check for empty recordsets in the call chain. Add logging:

order = self.env['sale.order'].search(domain, limit=1)
_logger.info("Order found: %s", order)  # logs sale.order() if empty

5. Inspect the field across versions. Field renames are documented in the upgrade migration scripts. Check /odoo/addons/<module>/migrations/<version>/ for the field's history.

Permanent Fix

For empty recordsets, use Python's truthiness check on recordsets — empty recordsets are falsy:

order = self.env['sale.order'].search([('name', '=', ref)], limit=1)
if not order:
    raise UserError(_("No order matches reference %s.") % ref)
return order.amount_total

For loops, iterate the recordset directly — Python handles empty cases gracefully:

for order in self.env['sale.order'].search(domain):
    order.action_confirm()  # safe — does nothing if domain matched zero rows

For field renames after a major upgrade, find the new name in Odoo's upgrade scripts:

grep -r "rename_field" /opt/odoo/addons/sale/migrations/ 2>/dev/null
# or check the OCA upgrade analysis report

Then update your code:

# Odoo 17.0
total = order.partner_invoice_id.email_normalized

# Odoo 18.0+ — field consolidated
total = order.partner_id.email_normalized

For missing custom modules, make the dependency explicit in your module's manifest:

{
    'name': 'My Sales Extension',
    'depends': ['sale', 'sale_management', 'my_custom_partner_fields'],
    ...
}

Without the explicit depends, Odoo will install your module against a base that lacks the field, and runtime will fail.

For controller / JSON-RPC type mismatches, validate the model server-side:

@http.route('/api/order/confirm', auth='user', type='json')
def confirm_order(self, order_id):
    order = request.env['sale.order'].browse(int(order_id)).exists()
    if not order:
        return {'error': 'not_found'}
    if not hasattr(order, 'action_confirm'):
        return {'error': 'unsupported_model'}
    order.action_confirm()
    return {'ok': True}

How to Prevent It

  • hasattr() for optional fields. When you read a field that might not exist (cross-module, optional integration), guard it: if hasattr(rec, 'x_studio_priority').
  • Always check if recordset: before field access when the recordset came from search() or browse(int). Make this a code-review rule.
  • Lint for stale field names post-upgrade. The OCA pylint-odoo extension catches many of these. Wire it into CI before you upgrade.
  • Test on a database with the same modules as production. Empty recordsets and missing fields are the kind of bug that only shows up against the real data shape. Restore production into staging before every major change.
  • Use type hints. Odoo's type stubs (odoo-stubs) help your IDE catch typos like sale.orders before runtime. Worth the setup cost on any module larger than 1,000 lines.
  • Read mapped() carefully. recordset.mapped('partner_id.email') returns an empty list for empty recordsets — safe. recordset.partner_id.email on an empty recordset can raise. Prefer mapped() when you want list semantics.

Frequently Asked Questions

Why does recordset.field not raise on an empty recordset in some places but does in others?

Empty recordsets return field defaults for Char/Float/Boolean fields silently, but Many2one fields can return either an empty recordset or raise depending on whether the field has been read into the cache. The behaviour shifted slightly in 18.0 and 19.0. Always guard with if recordset: for non-trivial paths instead of relying on the implicit default.

Is getattr(recordset, 'field', None) a good defense?

It is the wrong tool. getattr returns the field descriptor, not its value, and can mask real typos. Use hasattr(recordset, 'field_name') to test existence, then access normally. For values, guard with if recordset:.

How do I know if a field exists on a model without starting Odoo?

Check the model's Python source: every fields.Type(...) declaration. For inherited models, walk the inheritance chain. Or use the OCA module_analysis tool which dumps the full effective field list for a model. In a running shell, env['sale.order']._fields is authoritative.

Why does the same code work in Odoo SaaS but fail in self-hosted?

Almost always because the SaaS instance has a Studio customization (a custom field added via the UI) that does not exist in your self-hosted database. Studio fields are stored as ir.model.fields rows and module-data; export the customization and install it on self-hosted to match.


Need help with a tricky Odoo error? ECOSIRE's Odoo experts have shipped 215+ modules — get expert help.

Compartir:
E

Escrito por

ECOSIRE Team

Technical Writing

The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.

ECOSIRE

Transforme su negocio con Odoo ERP

Implementación, personalización y soporte experto de Odoo para optimizar sus operaciones.