Writing data
Writing comes down to three methods. The difficulty is elsewhere: in relational fields, in the business actions that move a document forward, and in not creating the same thing twice.
01create, write, unlink
# create: returns the new id
partner_id = models.execute_kw(DB, uid, KEY, 'res.partner', 'create', [{
'name': 'Ets Kouassi & Freres',
'email': '[email protected]',
'phone': '+225 07 00 00 00 00',
'city': 'Abidjan',
'customer_rank': 1,
}])
# update: returns True, and accepts a list of ids
models.execute_kw(DB, uid, KEY, 'res.partner', 'write',
[[partner_id], {'city': 'Bouake'}])
# delete
models.execute_kw(DB, uid, KEY, 'res.partner', 'unlink', [[partner_id]])
create accepts a list of
dictionaries and returns the created id in order. A hundred
contacts in one call instead of a hundred calls: same code, ten times
faster.
A record
referenced elsewhere refuses to be deleted: a posted invoice, an already
sold product. The workspace is protecting the consistency of your books. The
right move is archiving:
write(ids, {'active': False}), which hides without breaking the
history.
02Relational fields
A many2one is written with an id.
one2many and many2many are written with
commands: triplets that say what to do with the list.
| Command | Effect |
|---|---|
(0, 0, {values}) | Create a line and attach it. The most common form. |
(1, id, {values}) | Update an existing line. |
(2, id, 0) | Detach the line and delete it. |
(3, id, 0) | Detach without deleting. |
(4, id, 0) | Attach an existing record. |
(5, 0, 0) | Detach everything. |
(6, 0, [ids]) | Replace the whole list with these identifiers. |
order_id = models.execute_kw(DB, uid, KEY, 'sale.order', 'create', [{
'partner_id': partner_id,
'order_line': [
(0, 0, {'product_id': 512, 'product_uom_qty': 3, 'price_unit': 15000}),
(0, 0, {'product_id': 731, 'product_uom_qty': 1}),
],
}])
A line created without price_unit takes the price from the
pricelist that applies to the customer. That is almost always what you want:
only force the price when you have a reason to.
models.execute_kw(DB, uid, KEY, 'res.partner', 'write',
[[partner_id], {'category_id': [(6, 0, [3, 7])]}])
03Default values
default_get gives the values the interface would propose on
creation. Useful when a model has required fields whose expected value you
do not know.
defaults = models.execute_kw(DB, uid, KEY, 'sale.order', 'default_get',
[['company_id', 'currency_id', 'pricelist_id', 'warehouse_id']])
Recomputations triggered
by typing on screen do not automatically apply to an API
create. Set explicitly what you need, or read the record back
after creation to see what the workspace computed by itself.
04Business actions
A document does not move forward by writing its state. Every
button of the interface maps to a method that does all the
work: numbering, journal entries, stock moves, checks. Call the method, never
the field.
| Action | Model | Method |
|---|---|---|
| Confirm a quotation | sale.order | action_confirm |
| Invoice an order | sale.order | _create_invoices |
| Post an invoice | account.move | action_post |
| Reset an accounting document | account.move | button_draft then button_cancel |
| Confirm a purchase order | purchase.order | button_confirm |
| Validate a stock transfer | stock.picking | button_validate |
write(id, {'state':
'posted'}) looks like it works: the document changes state on screen.
But no entry was generated, no number assigned, no stock moved. You end up
with a posted invoice that does not exist in accounting, and the imbalance
only shows up on the balance sheet.
05A full walkthrough
From quotation to collected payment, in five calls.
# 1. the quotation
order_id = models.execute_kw(DB, uid, KEY, 'sale.order', 'create', [{
'partner_id': partner_id,
'order_line': [(0, 0, {'product_id': 512, 'product_uom_qty': 3})],
}])
# 2. confirmation: the quotation becomes a sales order
models.execute_kw(DB, uid, KEY, 'sale.order', 'action_confirm', [[order_id]])
# 3. invoicing
models.execute_kw(DB, uid, KEY, 'sale.order', '_create_invoices', [[order_id]])
[order] = models.execute_kw(DB, uid, KEY, 'sale.order', 'read',
[[order_id]], {'fields': ['invoice_ids']})
invoice_id = order['invoice_ids'][0]
# 4. posting: numbering + journal entries
models.execute_kw(DB, uid, KEY, 'account.move', 'action_post', [[invoice_id]])
# 5. payment, through the payment registration wizard
wizard_id = models.execute_kw(DB, uid, KEY, 'account.payment.register', 'create',
[{'payment_date': '2026-09-17', 'journal_id': 7}],
{'context': {'active_model': 'account.move', 'active_ids': [invoice_id]}})
models.execute_kw(DB, uid, KEY, 'account.payment.register',
'action_create_payments', [[wizard_id]],
{'context': {'active_model': 'account.move', 'active_ids': [invoice_id]}})
A window that opens on top of
a screen is a model like any other: you create it with its values, then call
its validation method. The context tells it what to work on:
active_model and active_ids. Same mechanism for
batch invoicing, credit notes or follow-ups.
06Do not create twice
A program that runs every hour eventually hits a row it already handled: dropped network, manual rerun, restart. Without care, every retry duplicates.
ref = f"SHOP-{external_order['id']}"
existing = models.execute_kw(DB, uid, KEY, 'sale.order', 'search',
[[['client_order_ref', '=', ref]]], {'limit': 1})
if existing:
order_id = existing[0]
else:
order_id = models.execute_kw(DB, uid, KEY, 'sale.order', 'create',
[{'partner_id': partner_id, 'client_order_ref': ref,
'order_line': lines}])
Carry the reference of the source system in a field you can search:
client_order_ref on an order, ref on a contact or
an accounting document. Or use an
external identifier, which exists for
exactly this.
07What the API will not do for you
- No transaction across calls. Every call commits on its own. An interrupted sequence leaves an intermediate state: plan the retry rather than hoping for atomicity.
- No writing on billed resources. Users, companies, applications and the subscription are handled in the portal.
- No bypassing permissions. A user without write access on a model will not force it through the API.