MADATA / DEV developer docs Français madata.africa

Recipes

Code to copy, tested against a real workspace. Replace the address, the database, the login and the key: the rest works.

01Python

Nothing to install: xmlrpc.client is in the standard library.

pythonmadata.py, a minimal reusable client
import os
import xmlrpc.client


class Madata:
    """Minimal client for the data API of a MADATA workspace."""

    def __init__(self, workspace, login, key):
        self.url = f"https://{workspace}.madata.app"
        self.db = f"prod_{workspace}"
        self.key = key
        common = xmlrpc.client.ServerProxy(f"{self.url}/xmlrpc/2/common")
        self.uid = common.authenticate(self.db, login, key, {})
        if not self.uid:
            raise RuntimeError("credentials refused")
        self.models = xmlrpc.client.ServerProxy(f"{self.url}/xmlrpc/2/object")

    def call(self, model, method, *args, **kw):
        return self.models.execute_kw(
            self.db, self.uid, self.key, model, method, list(args), kw)

    def search(self, model, domain, fields, **kw):
        return self.call(model, 'search_read', domain, fields=fields, **kw)


ma = Madata(
    workspace=os.environ["MADATA_WORKSPACE"],
    login=os.environ["MADATA_LOGIN"],
    key=os.environ["MADATA_KEY"],
)

for c in ma.search('res.partner', [['customer_rank', '>', 0]],
                   ['name', 'email'], limit=5):
    print(c['id'], c['name'], c['email'] or '-')

02Node.js

In JSON-RPC, fetch is enough: no dependency.

javascriptmadata.mjs
const WORKSPACE = process.env.MADATA_WORKSPACE;
const ENDPOINT = `https://${WORKSPACE}.madata.app/jsonrpc`;
const DB = `prod_${WORKSPACE}`;
const KEY = process.env.MADATA_KEY;

async function rpc(service, method, args) {
  const r = await fetch(ENDPOINT, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      jsonrpc: '2.0', method: 'call',
      params: { service, method, args },
    }),
  });
  const data = await r.json();
  // An error arrives with HTTP 200: the "error" key is what counts.
  if (data.error) {
    const d = data.error.data || {};
    throw new Error(`${d.name || 'error'}: ${d.message || data.error.message}`);
  }
  return data.result;
}

const uid = await rpc('common', 'authenticate',
                      [DB, process.env.MADATA_LOGIN, KEY, {}]);

const call = (model, method, args = [], kw = {}) =>
  rpc('object', 'execute_kw', [DB, uid, KEY, model, method, args, kw]);

const customers = await call('res.partner', 'search_read',
  [[['customer_rank', '>', 0]]],
  { fields: ['name', 'email'], limit: 5 });

console.table(customers);

03PHP

phpXML-RPC client with ripcord
<?php
require_once 'ripcord.php';

$workspace = getenv('MADATA_WORKSPACE');
$url = "https://{$workspace}.madata.app";
$db  = "prod_{$workspace}";
$key = getenv('MADATA_KEY');

$common = ripcord::client("$url/xmlrpc/2/common");
$uid = $common->authenticate($db, getenv('MADATA_LOGIN'), $key, []);

$models = ripcord::client("$url/xmlrpc/2/object");

$customers = $models->execute_kw($db, $uid, $key,
    'res.partner', 'search_read',
    [[['customer_rank', '>', 0]]],
    ['fields' => ['name', 'email'], 'limit' => 5]);

foreach ($customers as $c) {
    echo $c['name'], ' ', ($c['email'] ?: '-'), PHP_EOL;
}

04curl

For a quick check, or to debug from a server where you will install nothing.

bashthree calls, from version to read
WORKSPACE=kouassi
KEY=your-api-key
[email protected]

# server version
curl -s https://$WORKSPACE.madata.app/jsonrpc -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"call","params":{
        "service":"common","method":"version","args":[]}}'

# uid
curl -s https://$WORKSPACE.madata.app/jsonrpc -H "Content-Type: application/json" \
  -d "{\"jsonrpc\":\"2.0\",\"method\":\"call\",\"params\":{
        \"service\":\"common\",\"method\":\"authenticate\",
        \"args\":[\"prod_$WORKSPACE\",\"$LOGIN\",\"$KEY\",{}]}}"

# the 5 most recent contacts (uid = 7 in this example)
curl -s https://$WORKSPACE.madata.app/jsonrpc -H "Content-Type: application/json" \
  -d "{\"jsonrpc\":\"2.0\",\"method\":\"call\",\"params\":{
        \"service\":\"object\",\"method\":\"execute_kw\",
        \"args\":[\"prod_$WORKSPACE\",7,\"$KEY\",\"res.partner\",
                  \"search_read\",[[]],
                  {\"fields\":[\"name\"],\"limit\":5}]}}"

05Recipe: today’s overdue invoices

pythonoverdue invoices, oldest first
from datetime import date

overdue = ma.search('account.move',
    [['move_type', '=', 'out_invoice'],
     ['state', '=', 'posted'],
     ['payment_state', 'in', ['not_paid', 'partial']],
     ['invoice_date_due', '<', date.today().isoformat()]],
    ['name', 'partner_id', 'invoice_date_due', 'amount_residual'],
    order='invoice_date_due')

total = sum(i['amount_residual'] for i in overdue)
print(f"{len(overdue)} overdue invoices, {total:,.0f} FCFA")
for i in overdue[:20]:
    print(i['invoice_date_due'], i['partner_id'][1], i['amount_residual'])

06Recipe: sync a catalogue

The "look up, otherwise create" pattern applied to a product feed from another system. Replayable without duplicating anything.

pythonproduct import, replayable
def sync(product):
    """product = {'ref': 'SKU-001', 'name': '...', 'price': 12000}"""
    existing = ma.call('product.template', 'search',
                       [['default_code', '=', product['ref']]], limit=1)
    values = {
        'name': product['name'],
        'default_code': product['ref'],
        'list_price': product['price'],
    }
    if existing:
        ma.call('product.template', 'write', existing, values)
        return existing[0], 'updated'
    return ma.call('product.template', 'create', values), 'created'


for p in external_feed():
    id_, action = sync(p)
    print(p['ref'], action, id_)

07Recipe: attach a document

pythonattach a PDF to an invoice
import base64

with open('delivery_note.pdf', 'rb') as f:
    content = base64.b64encode(f.read()).decode()

ma.call('ir.attachment', 'create', {
    'name': 'delivery_note.pdf',
    'datas': content,
    'res_model': 'account.move',
    'res_id': invoice_id,
    'mimetype': 'application/pdf',
})

08Going to production

A dedicated key
One per program, named after it, held by a user whose permissions match the job.
Secrets out of the code
Environment variables or a secret manager. Never in a repository, even a private one.
A planned retry
Backing off on transient errors, a hard stop on permission errors, and a lookup before every replayable write.
A log
Model, method, arguments, full response or error. That is what turns an incident into a ten minute fix.
A smoke test
A search_count at startup: if the key is revoked you know right away, not halfway through an import.
Batches
Group reads and writes. The network costs more than the computation.
A doubt, a blocker

Go through support from your workspace, with the exact call and the response you got. And if what you want to do fits in a sentence rather than a program, the MCP server may already do it.