Accès API Odoo NEOSEN pour Goralab (lecture seule ventes)


Version 2.0 du 02/09/2026 : testée de bout en bout (XML-RPC, JSON-RPC, et tir réel des 2 webhooks). Contact technique : direction NEOSEN.


1. Ce que cet accès permet (et ses limites, à lire avant de coder)


  • Lecture seule des commandes de vente (sale.order), de leurs lignes (sale.order.line), du catalogue produits (product.template, product.product) et des devises (res.currency).
  • Deux webhooks temps réel : vente confirmée et paiement reçu.

  • Limites importantes (par conception, périmètre « ventes seules ») :

  • Le nom du client est disponible (embarqué dans la commande), mais pas ses coordonnées : email, téléphone, adresse ne sont pas accessibles.
  • Les factures (account.move) ne sont pas lisibles en API. Le webhook paiement vous notifie, mais vous ne pouvez pas re-lire la facture ; vous rebasculez sur la commande via le champ invoice_origin (voir §6).
  • Aucune écriture, aucun accès CRM, RH, comptabilité. Les tentatives renvoient une erreur d'accès (normal).

  • 2. Connexion


    ParamètreValeur
    URL`https://www.digimoov.fr`
    Base de données`marouane177-odoo-17-production-19137003`
    Login`goralab-api@digimoov.fr`
    Mot de passela clé API (transmise séparément par canal sécurisé, jamais par email)

    Protocoles : XML-RPC (/xmlrpc/2/) ou JSON-RPC (/jsonrpc). Les deux sont testés et fonctionnels. La clé API s'utilise à la place du mot de passe.


    3. Exemple XML-RPC (Python)


    
    import xmlrpc.client
    
    URL = "https://www.digimoov.fr"
    DB = "marouane177-odoo-17-production-19137003"
    LOGIN = "goralab-api@digimoov.fr"
    API_KEY = "..."  # transmise séparément
    
    common = xmlrpc.client.ServerProxy(f"{URL}/xmlrpc/2/common")
    uid = common.authenticate(DB, LOGIN, API_KEY, {})   # entier stable, cacheable
    models = xmlrpc.client.ServerProxy(f"{URL}/xmlrpc/2/object")
    
    # Ventes confirmées depuis une date (curseur incrémental sur write_date)
    orders = models.execute_kw(DB, uid, API_KEY, "sale.order", "search_read",
        [[["state", "=", "sale"], ["write_date", ">=", "2026-09-01 00:00:00"]]],
        {"fields": ["name", "date_order", "state", "amount_untaxed", "amount_total",
                    "currency_id", "partner_id", "company_id", "order_line", "write_date"],
         "limit": 200, "order": "write_date asc"})
    
    # Lignes d'une commande
    lines = models.execute_kw(DB, uid, API_KEY, "sale.order.line", "read",
        [orders[0]["order_line"]],
        {"fields": ["product_id", "name", "product_uom_qty", "price_unit", "price_total"]})
    

    En search_read/read, les champs relationnels reviennent en paire [id, libellé] :

    partner_id = [88298, "MIMMIH Nabil"], currency_id = [1, "EUR"], company_id = [2, "DIGIMOOV"], order_line = [id1, id2, ...].

    → C'est ainsi que vous obtenez le nom du client (même si l'accès direct au partenaire est bloqué).


    4. Exemple JSON-RPC (curl)


    
    curl -s https://www.digimoov.fr/jsonrpc -H 'Content-Type: application/json' -d '{
      "jsonrpc": "2.0", "method": "call",
      "params": {"service": "object", "method": "execute_kw",
        "args": ["marouane177-odoo-17-production-19137003", UID, "API_KEY",
          "sale.order", "search_read",
          [[["state", "=", "sale"]]],
          {"fields": ["name", "date_order", "amount_total"], "limit": 10}]}}'
    

    UID = l'entier renvoyé par common.authenticate (stable, cacheable).


    5. Champs utiles


    sale.order : name, date_order, state (draft/sent/sale/cancel), amount_untaxed, amount_total, currency_id, partner_id, company_id, order_line, write_date.


    sale.order.line : order_id, product_id, name, product_uom_qty, price_unit, price_subtotal, price_total.


  • Dates : format AAAA-MM-JJ HH:MM:SS en UTC (pas d'heure locale : convertissez côté Goralab).
  • Montants : exprimés dans la devise de la commande (currency_id). Ne supposez pas EUR partout : lisez toujours currency_id.
  • Société : company_id : 1 = MCM ACADEMY, 2 = DIGIMOOV.
  • Volumétrie : ~44 000 commandes. Utilisez limit/offset et un curseur incrémental sur write_date (jamais un export complet répété).

  • 6. Webhooks temps réel


    Deux notifications sortantes (POST JSON, Content-Type: application/json). Le type d'événement se lit sur le champ _model :


    Événement`_model`DéclencheurFréquence
    Vente confirmée`sale.order`la commande passe à l'état `sale`à chaque confirmation
    Paiement reçu`account.move`la facture entre en `in_payment` ou `paid`**une seule fois par facture** (pas de doublon)

    Payload réel (tel que reçu lors des tests) :


    Vente :

    
    {"_model": "sale.order", "_id": 54510, "_action": "GORALAB_vente_confirmee(#4727)",
     "id": 54510, "name": "S54510", "state": "sale",
     "date_order": "2026-09-02 16:21:08", "amount_untaxed": 100.0, "amount_total": 100.0,
     "partner_id": 88347, "company_id": 2}
    

    Paiement :

    
    {"_model": "account.move", "_id": 23881, "_action": "GORALAB_paiement_recu(#4728)",
     "id": 23881, "name": "FAD210425-02175", "payment_state": "paid",
     "invoice_date": "2026-09-02", "invoice_origin": "S54510",
     "amount_total": 100.0, "partner_id": 88347, "company_id": 2}
    

    Points à respecter impérativement (sinon « ça marche pas ») :


    1. Discriminez par _model (sale.order vs account.move). N'analysez pas _action (c'est un libellé humain, pas un type stable).

    2. Dans le webhook, les champs relationnels sont des ID entiers NUS : partner_id: 88347, company_id: 2 : pas [id, nom] comme dans search_read. Vous n'avez pas le nom dans le payload.

    3. Le webhook est une notification, pas la donnée complète. Modèle recommandé :

    - Vente : sur réception, faites read/search_read sur sale.order id=_id → vous obtenez le nom du client et le détail des lignes.

    - Paiement : account.move n'est pas lisible en API. Utilisez invoice_origin (ici "S54510") pour faire search_read sur sale.ordername = invoice_origin → vous récupérez client + montants.

    4. Paiement, valeur de payment_state : peut valoir in_payment (paiement enregistré, en attente de rapprochement bancaire) ou paid (soldé). L'événement ne part qu'une fois ; traitez-le en upsert idempotent sur name (numéro de facture).

    5. Sécurité : nous mettrons un token secret en query string (?token=...). Vérifiez-le et rejetez toute requête sans le bon token. (Odoo n'envoie pas de signature HMAC.)

    6. Pas de retry : le webhook Odoo est « send and forget », timeout 1 s, aucune reprise si votre endpoint est indisponible. Prévoyez impérativement un poll de réconciliation (ex. toutes les 15 min : search_read sur write_date >= dernier_curseur) pour rattraper les événements manqués. Le temps réel est un confort, le poll est le filet de sécurité.


    7. Ce qu'il nous faut de votre côté


    1. L'URL HTTPS de votre endpoint webhook (nous y ajoutons le token et activons les 2 automatisations, désactivées aujourd'hui).

    2. Un contact technique pour un test d'activation en réel ensemble.


    8. Règles d'usage


  • Clé nominative et révocable ; ne la partagez pas hors de votre équipe.
  • Lecture seule : toute écriture est refusée et journalisée.
  • Préférez l'incrémental au full-scan ; pas de polling sous la minute.

  • Ce document est directement exploitable par votre Claude Code : payloads réels, chemins de re-fetch et pièges connus y sont explicites.