Reading data
One method covers 90 % of the need:
search_read. It searches and returns the requested fields in one
round trip. The other read methods exist for the remaining 10 %.
01search_read, the right reflex
customers = models.execute_kw(DB, uid, KEY, 'res.partner', 'search_read',
[[['customer_rank', '>', 0]]], # domain
{'fields': ['name', 'email', 'phone', 'city'], # fields wanted
'limit': 10,
'order': 'create_date desc'})
Without fields, the
workspace returns every field of every row, computed ones
included. On account.move that multiplies both the response size
and the call duration by ten. Ask for what you need, nothing more.
02Domains
A domain is a list of conditions. Each condition is a triplet
[field, operator, value]. Side by side, they combine with an
implicit AND.
# posted customer invoices above 500,000 FCFA [['move_type', '=', 'out_invoice'], ['state', '=', 'posted'], ['amount_total', '>', 500000]]
For an OR, or a negation, the operator goes
before the conditions it governs, and it takes two of them
(one for !).
# name = "Kouassi" OR email contains "kouassi" ['|', ['name', '=', 'Kouassi'], ['email', 'ilike', 'kouassi']] # three branches: one more OR per extra condition ['|', '|', ['a', '=', 1], ['b', '=', 2], ['c', '=', 3]] # everything but drafts ['!', ['state', '=', 'draft']] # customers in Abidjan OR Bouake, and active ['&', ['active', '=', True], '|', ['city', '=', 'Abidjan'], ['city', '=', 'Bouake']]
The operator announces what follows: "OR of (this) and (that)". Read left to right like a sentence, nested domains stop being unreadable. And when one OR governs three conditions, you need two of them in a row.
03Operators
| Operator | Effect |
|---|---|
= · != | Equal, different. |
> · >= · < · <= | Comparisons, dates included. |
in · not in | Membership. ['state', 'in', ['draft', 'sent']]. |
like · not like | Contains, case sensitive. |
ilike · not ilike | Contains, case insensitive. The right pick for a text search. |
=like · =ilike | Full pattern, with % and _ up to you. |
child_of | The record and its descendants: a contact and its child contacts, a location and its sublocations. |
parent_of | The record and its ancestors. |
On a many2one field, a direct comparison takes the
id (['partner_id', '=', 42]); an ilike
works on the label (['partner_id', 'ilike', 'Kouassi']). And a
dot walks the relation: ['partner_id.city', '=', 'Abidjan'].
04The other read methods
| Method | When |
|---|---|
search | You only want the id, to hand them to an action. |
read | You already have the id and want fields. read([[1, 2, 3]], {'fields': [...]}). |
search_count | You only want the number. Do not fetch a thousand rows to count a thousand. |
name_search | You search the way an input field does: name_search('kou', limit=8) returns [id, label] pairs. |
read_group | You want totals, not rows. |
fields_get | You want to know what the model holds. See Models & fields. |
05Group and total
read_group does the arithmetic on the server side. That is
the difference between a one second answer and a thirty thousand row export
to add up at home.
totals = models.execute_kw(DB, uid, KEY, 'account.move', 'read_group',
[[['move_type', '=', 'out_invoice'],
['state', '=', 'posted'],
['invoice_date', '>=', '2026-01-01']],
['amount_total_signed'], # aggregated fields
['partner_id']], # grouping
{'lazy': False, 'orderby': 'amount_total_signed desc', 'limit': 20})
for row in totals:
print(row['partner_id'][1], row['amount_total_signed'], row['__count'])
Every result row carries the grouping value, the requested aggregates, a
__count, and a __domain ready to be handed back to
search_read for the detail of that group.
Add the granularity to the grouping field:
['invoice_date:month'], or :week,
:quarter, :year. And pass several fields to cross
two axes, for instance ['partner_id', 'invoice_date:month'] with
lazy: False.
06Pagination & sorting
limit and offset slice, order sorts
with the usual SQL syntax.
PAGE = 500
offset = 0
while True:
batch = models.execute_kw(DB, uid, KEY, 'account.move.line', 'search_read',
[[['parent_state', '=', 'posted']]],
{'fields': ['date', 'account_id', 'debit', 'credit'],
'limit': PAGE, 'offset': offset, 'order': 'id'})
if not batch:
break
handle(batch)
offset += PAGE
Paginating without
order, or sorting on a moving date, skips or repeats rows
between two pages. order: 'id' is boring and correct.
07The context
The context is a dictionary passed alongside the arguments. It does not change what you ask for, it changes how the workspace answers.
| Key | Effect |
|---|---|
lang | The language of returned labels: fr_FR, en_US. Technical values never change. |
tz | The timezone used by computed fields that depend on it. datetime values stay UTC in the response. |
active_test | False to also see archived records, which the workspace hides by default. |
allowed_company_ids | The company scope of the call. See multi company. |
everything = models.execute_kw(DB, uid, KEY, 'product.product', 'search_read',
[[]],
{'fields': ['name', 'active'],
'context': {'active_test': False, 'lang': 'en_US'}})
08Images & attachments
Binary fields come back base64 encoded. A product image reads like any other field.
import base64
[product] = models.execute_kw(DB, uid, KEY, 'product.product', 'read',
[[512]], {'fields': ['name', 'image_1920']})
if product['image_1920']:
with open('product.png', 'wb') as f:
f.write(base64.b64decode(product['image_1920']))
Attached documents live in ir.attachment: filter on
res_model and res_id to find those of a record,
then read datas. Mind the volume: only ask for
datas on the files you will actually download.