MADATA / DEV developer docs Français madata.africa

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

pythonthe three methods
# 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 in batch

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.

Deleting often fails, and that is normal

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.

CommandEffect
(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.
pythona quotation and its lines, in one create
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.

pythonreplace the tags of a contact
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.

pythonwhat the screen would have pre-filled
defaults = models.execute_kw(DB, uid, KEY, 'sale.order', 'default_get',
    [['company_id', 'currency_id', 'pricelist_id', 'warehouse_id']])
The API does not replay screens

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.

ActionModelMethod
Confirm a quotationsale.orderaction_confirm
Invoice an ordersale.order_create_invoices
Post an invoiceaccount.moveaction_post
Reset an accounting documentaccount.movebutton_draft then button_cancel
Confirm a purchase orderpurchase.orderbutton_confirm
Validate a stock transferstock.pickingbutton_validate
Never touch state

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.

pythonquotation, order, invoice, payment
# 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]}})
Wizards are called like models

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.

pythonlook before creating
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.