MADATA / DEV documentation développeur English madata.africa

Recettes

Des morceaux de code à copier, testés contre un espace réel. Remplacez l’adresse, la base, l’identifiant et la clé : le reste fonctionne.

01Python

Rien à installer : xmlrpc.client est dans la bibliothèque standard.

pythonmadata.py, un client minimal et reutilisable
import os
import xmlrpc.client


class Madata:
    """Client minimal de l'API de donnees d'un espace MADATA."""

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

    def appel(self, modele, methode, *args, **kw):
        return self.models.execute_kw(
            self.db, self.uid, self.cle, modele, methode, list(args), kw)

    def chercher(self, modele, domaine, champs, **kw):
        return self.appel(modele, 'search_read', domaine, fields=champs, **kw)


ma = Madata(
    espace=os.environ["MADATA_ESPACE"],
    login=os.environ["MADATA_LOGIN"],
    cle=os.environ["MADATA_CLE"],
)

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

02Node.js

En JSON-RPC, fetch suffit : aucune dépendance.

javascriptmadata.mjs
const ESPACE = process.env.MADATA_ESPACE;
const URL_BASE = `https://${ESPACE}.madata.app/jsonrpc`;
const DB = `prod_${ESPACE}`;
const CLE = process.env.MADATA_CLE;

async function rpc(service, method, args) {
  const r = await fetch(URL_BASE, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      jsonrpc: '2.0', method: 'call',
      params: { service, method, args },
    }),
  });
  const data = await r.json();
  // Une erreur arrive en HTTP 200 : c'est la cle « error » qui compte.
  if (data.error) {
    const d = data.error.data || {};
    throw new Error(`${d.name || 'erreur'} : ${d.message || data.error.message}`);
  }
  return data.result;
}

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

const appel = (modele, methode, args = [], kw = {}) =>
  rpc('object', 'execute_kw', [DB, uid, CLE, modele, methode, args, kw]);

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

console.table(clients);

03PHP

phpclient XML-RPC avec ripcord
<?php
require_once 'ripcord.php';

$espace = getenv('MADATA_ESPACE');
$url = "https://{$espace}.madata.app";
$db  = "prod_{$espace}";
$cle = getenv('MADATA_CLE');

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

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

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

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

04curl

Pour un contrôle rapide, ou pour déboguer depuis un serveur où vous n’installerez rien.

bashtrois appels, de la version a la lecture
ESPACE=kouassi
CLE=votre-cle-d-api
[email protected]

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

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

# les 5 derniers contacts (uid = 7 dans cet exemple)
curl -s https://$ESPACE.madata.app/jsonrpc -H "Content-Type: application/json" \
  -d "{\"jsonrpc\":\"2.0\",\"method\":\"call\",\"params\":{
        \"service\":\"object\",\"method\":\"execute_kw\",
        \"args\":[\"prod_$ESPACE\",7,\"$CLE\",\"res.partner\",
                  \"search_read\",[[]],
                  {\"fields\":[\"name\"],\"limit\":5}]}}"

05Recette : les impayés du jour

pythonfactures echues, du plus ancien au plus recent
from datetime import date

impayes = ma.chercher('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(f['amount_residual'] for f in impayes)
print(f"{len(impayes)} factures echues, {total:,.0f} FCFA")
for f in impayes[:20]:
    print(f['invoice_date_due'], f['partner_id'][1], f['amount_residual'])

06Recette : synchroniser un catalogue

Le motif « chercher, sinon créer » appliqué à un flux d’articles venu d’un autre système. Rejouable sans rien dupliquer.

pythonimport d’articles, rejouable
def synchroniser(article):
    """article = {'ref': 'SKU-001', 'nom': '...', 'prix': 12000}"""
    existant = ma.appel('product.template', 'search',
                        [['default_code', '=', article['ref']]], limit=1)
    valeurs = {
        'name': article['nom'],
        'default_code': article['ref'],
        'list_price': article['prix'],
    }
    if existant:
        ma.appel('product.template', 'write', existant, valeurs)
        return existant[0], 'mis a jour'
    return ma.appel('product.template', 'create', valeurs), 'cree'


for a in flux_externe():
    id_, action = synchroniser(a)
    print(a['ref'], action, id_)

07Recette : joindre un document

pythonattacher un PDF a une facture
import base64

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

ma.appel('ir.attachment', 'create', {
    'name': 'bon_de_livraison.pdf',
    'datas': contenu,
    'res_model': 'account.move',
    'res_id': facture_id,
    'mimetype': 'application/pdf',
})

08Mettre en production

Une clé dédiée
Une par programme, nommée d’après lui, portée par un utilisateur dont les droits correspondent au travail à faire.
Des secrets hors du code
Variables d’environnement ou gestionnaire de secrets. Jamais dans un dépôt, même privé.
Une reprise pensée
Réessais espacés sur les erreurs passagères, arrêt franc sur les erreurs de droits, et une recherche avant chaque écriture rejouable.
Un journal
Modèle, méthode, arguments, réponse ou erreur complète. C’est ce qui transforme un incident en correction de dix minutes.
Un test de fumée
Un search_count au démarrage : si la clé est révoquée, vous le saurez tout de suite, pas au milieu d’un import.
Des lots
Regroupez lectures et écritures. Le réseau coûte plus cher que le calcul.
Un doute, un blocage

Passez par le support depuis votre espace, avec l’appel exact et la réponse reçue. Et si ce que vous voulez faire tient en une phrase plutôt qu’en un programme, le serveur MCP le fait peut-être déjà.