A one2many field that raises inverse_name errors, or a related-records field that just won’t save no matter what looks right in the view — both usually trace back to the same misunderstanding: treating one2many and many2many as two flavors of the same thing, when one of them stores nothing at all and the other owns a real table.
The short version: a one2many is not real storage — it's a computed view into a many2one field that must already exist on the other model, pointed back at this one. A many2many genuinely creates (or reuses) its own junction table with two columns. Every "why won't this field save" or "why do I need an inverse field" question about either one is answered by that single distinction.
many2one: the field that actually exists
Neither relation below makes sense without this one first: a many2one is a real column — a foreign key to another table, storing exactly one related record’s id. order_line_ids-style fields don’t exist without a many2one somewhere pointing the other way; that’s the whole mechanism a one2many relies on.
one2many: a view, not a column
Per Odoo’s own developer tutorial on model relations, a one2many is the inverse of a many2one — it does not store any data of its own. It’s defined with a comodel and an inverse_name: the name of the actual many2one (or Reference) field that must already exist on the other model and point back at this one.
class SaleOrder(models.Model):
_name = 'sale.order'
order_line_ids = fields.One2many('sale.order.line', 'order_id', string='Order Lines')
class SaleOrderLine(models.Model):
_name = 'sale.order.line'
order_id = fields.Many2one('sale.order', string='Order') # this is what order_line_ids actually reads
order_line_ids creates no column on sale.order at all — reading it runs a query against sale.order.line filtering by order_id, live, every time. This is exactly why a one2many needs an inverse_name, and a many2many doesn’t: there’s a real many2one column to point at.
Watch out: because a one2many is virtual, it cannot exist without the corresponding many2one already defined on the other model — a one2many pointed at an inverse_name that doesn't exist (or exists but doesn't actually point back at the right model) fails at load time, not silently. If a one2many field won't behave, the very first thing worth confirming is that its inverse_name field genuinely exists on the comodel and is itself a working many2one.
many2many: two real columns, in its own table
A many2many is different in kind, not just cardinality — per the same documentation, it’s a genuinely bidirectional relationship: any record on either side can relate to any number of records on the other. That needs storage neither model’s own table can hold on its own, so Odoo creates a separate junction table with exactly two columns, one foreign key to each side:
tag_ids = fields.Many2many(
'res.partner.category',
relation='partner_category_rel', # the junction table
column1='partner_id',
column2='category_id',
)
Leaving relation/column1/column2 unset lets Odoo generate reasonable defaults from the model names — spelling them out explicitly matters mainly when two different many2many fields (in different models) are meant to genuinely share the same underlying junction table and columns, which has to be declared on both sides to actually happen rather than silently creating two separate tables that happen to look similar.
Comparison
one2many | many2many | |
|---|---|---|
| Storage | None — computed from a many2one elsewhere | Owns a real junction table |
| Requires | A many2one/Reference field already on the comodel (inverse_name) | Nothing pre-existing — creates its own table |
| Cardinality | One record here → many there, each of which points back to exactly one record here | Any record on either side ↔ any number on the other |
| Typical use | Order lines, invoice lines — a clear “belongs to exactly one parent” relationship | Tags, categories — free association, either side can have many of the other |
The tell that you picked the wrong one
If the relationship is genuinely “each child belongs to exactly one parent” (an order line belongs to exactly one order), a many2many technically still works — the junction table just happens to only ever have one row per child — but it throws away that guarantee and adds a table that didn’t need to exist. If the relationship is genuinely “either side can have many of the other” (a contact can have many tags, a tag applies to many contacts), a one2many can’t express it at all, since it has nowhere on the “many” side to point a single many2one back to just one parent.
This distinction matters beyond modeling correctness, too: a record rule restricting who can see the child model (sale.order.line, say) is a separate rule from whatever protects the parent — see Why Can’t This User See a Record They Should? for how those combine, since a one2many’s virtual nature doesn’t exempt its child records from having their own independent access rights and record rules.
Frequently asked questions
Can a one2many and a many2many both point at the same target model?
Yes — nothing prevents a model from having both kinds of relation to the same comodel, as long as the underlying fields (the `many2one` a `one2many` reads, and the junction table a `many2many` owns) are genuinely distinct from each other.
Why does creating a record through a one2many sometimes feel like it "just works" with no inverse_name trouble?
Because the ORM and many view widgets handle the common case (a form's embedded list of child records) by setting the child's inverse_name field automatically when a new row is added through that exact one2many — the mechanism is still the same virtual-field-over-a-many2one relationship underneath, it's just that the UI is doing the bookkeeping for you in that one specific flow.
Does a one2many field show up as a column if I inspect the database table directly?
No — inspecting sale_order's own table directly shows no order_line_ids column at all, only whatever real columns exist there. The relationship lives entirely on the other side, as the order_id column on sale_order_line.
What happens if I delete the many2one field a one2many depends on?
The one2many breaks at load time — it has nothing left to read from, since it was never anything more than a query against that specific field. This is the direct consequence of it storing nothing of its own.
Is there a performance difference between the two?
A one2many's read is a live query against the child model every time it's accessed (though cached within a request like any computed value); a many2many's read is a join against its junction table. Neither is inherently faster in general — the real-world difference comes from how many rows are actually involved and how the surrounding view queries them, not from the field type itself.
Further reading
- Chapter 7: Relations Between Models — official Odoo 18.0 developer tutorial, the primary source for the mechanics above, with a full worked example building both kinds of relation.
- Fields — Odoo Development docs, a field-by-field reference including every relational field’s constructor parameters.
And on this site: the Domain Filter Builder & Explainer for querying across a relational field either way, and the Report Builder, where a one2many is exactly what a repeater block (a report’s table of order lines, invoice lines, etc.) resolves against.