انتقل إلى المحتوى الرئيسي

خطأ فردي متوقع في Odoo TypeError — كيفية إصلاحه في عام 2026

اضغط على المفردة المتوقعة: النموذج (id1، id2، ...) في Odoo 17/18/19؟ قم بإصلاح الحلقة والتأكد من واحد ونمط الوصول الميداني في ثلاثة أسطر اليوم في كود الإنتاج.

E
ECOSIRE Research and Development Team
|4 مايو 20267 دقائق قراءة1.2k كلمات|

هذه المقالة متاحة حاليًا باللغة الإنجليزية فقط. الترجمة قريبا.

Your Odoo code throws this on a recordset operation:

Your Odoo code throws this on a recordset operation:

TypeError: Expected singleton: sale.order(14, 15, 23)

This is the most common ORM error after MissingError. It fires whenever you try to read a single field's value from a recordset that contains more than one record. Odoo refuses to guess which record you meant. The fix is almost always one of two patterns: add a loop, or call ensure_one().

Quick Fix

Pick one of these two patterns based on intent.

Loop pattern — when you want to operate on each record:

# WRONG — singleton error if recordset has > 1 record
total = orders.amount_total

# RIGHT
for order in orders:
    process(order, order.amount_total)

ensure_one pattern — when you genuinely expect exactly one record:

# Action handlers in form views always receive a single-record recordset
def action_confirm(self):
    self.ensure_one()
    self.state = 'confirmed'

ensure_one() raises ValueError (with a clear message) if the recordset is not exactly one record, instead of letting the singleton error surface later in opaque places.

Why This Happens

Odoo recordsets are list-like. A recordset with three records is model(14, 15, 23). When you write recordset.field, the ORM tries to return one value — and refuses if the recordset has more than one record. The four common triggers:

  1. Server action / button handler that the user invoked from a list view multi-selection. The handler assumed singleton but received many.
  2. Compute method that reads a field on self directly instead of looping for rec in self.
  3. Constraint method that does the same.
  4. Writes to one2many fields without iterating — e.g. self.line_ids.product_id = product when line_ids has 5 lines.

Compute and constraint methods are the silent cause: Odoo always passes them a recordset, never a single record, and forgetting the loop is a one-line bug that fails only when the recordset has more than one row.

Step-by-Step Diagnosis

1. Identify the call site. The traceback shows the exact line. Look at it.

2. Check self's shape. Add logging:

def _compute_total(self):
    _logger.info("compute called with %s records: %s", len(self), self)
    self.total = self.amount_untaxed * 1.1  # raises if len(self) > 1

3. Decide the intent. Is this code supposed to handle one record or many? Most form-view action handlers handle one. Most compute methods handle many. Most server actions handle many.

4. Check upstream callers. A method called from a list-view bulk action will always receive multiple records, even if every test case used one.

Permanent Fix

For compute methods, always loop:

@api.depends('order_line.price_subtotal', 'order_line.tax_id')
def _compute_amount(self):
    for order in self:
        order.amount_untaxed = sum(line.price_subtotal for line in order.order_line)
        order.amount_total = order.amount_untaxed * 1.1

The for order in self is non-negotiable. Even if you "know" the recordset will have one row, Odoo can call this with hundreds during a bulk write or migration.

For constraint methods, always loop:

@api.constrains('amount_total', 'partner_id.credit_limit')
def _check_credit_limit(self):
    for order in self:
        if order.amount_total > order.partner_id.credit_limit:
            raise ValidationError(_("Order exceeds credit limit."))

For action methods called from a button, two patterns are valid:

# Pattern A — explicit single-record action
def action_confirm(self):
    self.ensure_one()
    self.state = 'confirmed'
    return True

# Pattern B — bulk-aware action
def action_confirm(self):
    for order in self:
        order.state = 'confirmed'
    return True

Pattern B is more robust for list-view bulk actions. Pattern A is correct for form-view actions. Both are wrong if you do neither.

For one2many writes, always go through write on the related recordset:

# WRONG — singleton error when line_ids has > 1 line
self.line_ids.product_id = product

# RIGHT
self.line_ids.write({'product_id': product.id})

# OR loop explicitly
for line in self.line_ids:
    line.product_id = product

write({'field': value}) operates on every record in the recordset and is the canonical Odoo way to do bulk updates.

How to Prevent It

  • Pattern: every compute starts with for rec in self. Make it a snippet in your editor. Compute methods that do not loop are a code smell.
  • Pattern: every action method starts with either ensure_one() or a loop. Pick one explicitly. Never rely on the caller passing a single-record recordset.
  • Lint rule. pylint-odoo ships bad-method-argument-name and compute-sudo-no-context-passed checkers. Add a custom checker for "compute method without for x in self" — it catches 90 percent of singleton bugs.
  • Test with multi-record recordsets. When unit-testing compute and constraint methods, always test with a recordset of size 2 or more. A test that only ever passes a single record will not catch this bug.
  • Bulk-aware server actions. In Odoo 18.0+ Server Actions, prefer the "Run for all selected" model and write code that handles multiple records. Old habits of "this is a one-record action" lead to singleton errors when users multi-select.

Frequently Asked Questions

Why does Odoo not silently pick the first record?

Because that would hide bugs. If your code runs against three orders and silently uses the first, you get incorrect totals, half-applied state changes, and corrupted data. Refusing to guess is the safer default. Use recordset[0] explicitly when you really do want the first record.

Is ensure_one() always the right fix in action methods?

Only if your action genuinely cannot handle multi-record selections. List-view actions (the contextual "Action" menu) pass the entire selection. If your handler does ensure_one() there, the user clicking three records gets a confusing error. Prefer the for-loop pattern for most action methods, and reserve ensure_one() for form-view singletons.

What is the cost of looping when I "know" it is a single record?

Negligible. A for rec in self loop over a single-record recordset is one Python iteration — microseconds. The cost of not looping is the bug you are reading this article to fix. Always loop.

Can I use recordset.first() or similar helpers?

Odoo does not ship .first(). You can write your own helper:

def first(recordset):
    return recordset[:1]  # safe even for empty recordsets

recordset[:1] returns a recordset of size 0 or 1, never raising the singleton error and not crashing on empty input. We use this pattern across all ECOSIRE modules.

Why does my onchange method raise singleton when triggered from the UI?

onchange methods receive a NewId recordset, not a database recordset. They are always single-record by definition — no loop is required, but ensure_one() is still good practice as documentation. If your onchange is triggered with multiple records (rare, mostly via custom XML server actions), refactor as a button action instead.

Should I use .mapped('field') to avoid singleton errors?

mapped() returns a list (for non-relational fields) or a recordset (for relational). It never raises singleton because it iterates internally. Use mapped() whenever you need a list of values across a recordset:

emails = orders.mapped('partner_id.email')   # returns list of strings

This is idiomatic Odoo and reads cleaner than an explicit list comprehension.


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

مشاركة:
E

بقلم

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

قم بتحويل أعمالك باستخدام Odoo ERP

تنفيذ وتخصيص ودعم خبير Odoo لتبسيط عملياتك.