Documentation API REST

Pour les besoins d’un client, j’ai du interfacer une appli métier avec Dolibarr. Je vais essayer de mettre ici tout ce que j’ai fait/trouvé.

J’ai commencer par identifier plusieurs solutions.

  1. attaquer la base de données directement
    Ça, de mon point de vue, c’est con. C’est le meilleur moyen de planter la base à un moment ou à un autre.

  2. utiliser les fonctionnalités d’export et d’import.
    J’ai fait quelques tests pas concluants. Trop de manipulations, risques de problèmes avec les tables, etc. Par exemple, 2 versions identiques (en terme de numéro de version) de Dolibarr peuvent ne pas pouvoir importer un même fichier CSV. Si l’un des 2 Dolibarr est une vieille version sur laquelle on a appliqué des mises à jour alors que l’autre est une installation neuve, les tables ne sont pas dans le même ordre dans la base. Conséquence, on peut importer/exporter uniquement sur le même Dolibarr. Pas vers un autre. Il n’y a donc pas de constance.

  3. utiliser l’API SOAP
    Bon, ça aussi c’est con. L’API fonctionne plutôt pas mal mais étant donné qu’elle est dépréciée, je n’allais pas développer sur un système voué à disparaître à court terme.

  4. utiliser l’API REST
    Après quelques essais et malgré le manque de documentation, je me suis dit, pourquoi pas. C’est donc cette méthode que je vais essayer de développer ici.

*** INSTALLATION

Pour installer, aller dans la page des modules et activer le module API REST. Dans la page de configuration du module, il y a 2 liens. Pas la peine de cliques sur le premier, il ne fonctionne pas en l’état. C’est un peu dommage d’en avoir fait un lien car ça incite à le suivre mais c’est comme ça.

Le lien a cette forme : http://<mon serveur>/api/index.php/login?login=<mon login utilisateur>&password=yourpassword
copiez le lien, remplacez yourpassword par votre mot de passe utilisateur. Normalement, <mon serveur> et <mon login utilisateur> sont déjà remplacés. collez le lien dans votre navigateur pour générer votre <token>. ce dernier est lié à votre compte utilisateur

Notez bien ce <Token> dans un coin !!!

Maintenant, vous pouvez aller sur le 2nd lien de la page de configuration du module. En haut a droite, coller le <token> puis cliquez sur explore. Normalement, vous allez voir apparaître toutes les actions accessibles avec ce <token>. Si vous n’avez pas beaucoup d’actions, c’est certainement que les modules ne sont pas activés. Par exemple, si vous voulez voir les factures (invoices), il faut activer le module adéquat dans votre configuration Dolibarr. Idem pour produits, etc. Pour information, Thirdparties, ce sont les Tiers.
Sur cette page d’exploration de l’API, vous pouvez faire pas mal de tests. aussi bien en lecture qu’en écriture, modification ou suppression

*** UTILISATION

Grosso modo, pour utiliser REST, il faut appeler une url du genre suivant http://<mon_serveur>/api/index.php/<action>
avec une des 4 méthodes : GET, POST, PUT, DELETE, en remplaçant <action> par l’action sur laquelle vous voulez intervenir. Ex : http://<mon_serveur>/api/index.php/invoices

Pour ce faire, il existe plusieurs méthodes. Je suis parti d’un bout de code que j’ai trouvé et modifié mais il existe également des librairies telles que http://phphttpclient.com/ . Je ne l’ai pas encore essayée mais ça devrait fonctionner. Pour ma part, voici ma fonction :

function CallAPI($method, $apikey, $url, $data = false)
{
    $curl = curl_init();
    $httpheader = ['DOLAPIKEY: '.$apikey];

    switch ($method)
    {
        case "POST":
            curl_setopt($curl, CURLOPT_POST, 1);
            $httpheader[] = "Content-Type:application/json";

            if ($data)
                curl_setopt($curl, CURLOPT_POSTFIELDS, $data);

            break;
        case "PUT":

	    curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PUT');
            $httpheader[] = "Content-Type:application/json";

            if ($data)
                curl_setopt($curl, CURLOPT_POSTFIELDS, $data);

            break;
        default:
            if ($data)
                $url = sprintf("%s?%s", $url, http_build_query($data));
    }

    // Optional Authentication:
	//    curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
	//    curl_setopt($curl, CURLOPT_USERPWD, "username:password");

    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
	curl_setopt($curl, CURLOPT_HTTPHEADER, $httpheader);

    $result = curl_exec($curl);

    curl_close($curl);

    return $result;
}

Ce n’est ni bon, ni dans les règles de l’art mais pour mon besoin, cela fonctionne.
la fonction prend 4 paramètres :
$method : string « GET », « POST », « PUT », « DELETE »
$apikey : string « votre <token> généré plus haut »
$url : string l’url à appeler. Ex : « http://<mon_serveur>/api/index.php/invoices »
$data : string flux au format json. ce champ n’est pas obligatoire.

*** EXEMPLES

Maintenant, quelques exemples.
dans tous les cas :
- $apiKey = « mon <token> »;
- $apiUrl = « http://<mon_serveur>/api/index.php/ »;

// Récupérer la liste des produits
	$listProduits = [];
	$produitParam = ["limit" => 10000, "sortfield" => "rowid"];
	$listProduitsResult = CallAPI("GET", $apiKey, $apiUrl."products", $produitParam);
	$listProduitsResult = json_decode($listProduitsResult, true);

	if (isset($listProduitsResult["error"]) && $listProduitsResult["error"]["code"] >= "300") {
	} else {
		foreach ($listProduitsResult as $produit) {
			$listProduits[intval($produit["id"])] = html_entity_decode($produit["ref"], ENT_QUOTES);
		}
	}

Commentaires :
- je récupère les 10’000 premiers produits triés par leur id dans la base
- html_entity_decode est nécessaire car les apostrophes sont encodés
- il est facile d’utiliser la même méthode (en remplaçant products par dictionnarycountries) pour récupérer la liste des pays

// Créer un produit
	$ref = "ma_reference_produit_X203ZZ";
	$newProduct = [
		"ref"	=> $ref,
		"label"	=> $ref
	];
	$newProductResult = CallAPI("POST", $apiKey, $apiUrl."products", json_encode($newProduct));
	$newProductResult = json_decode($newProductResult, true);

Commentaires :
- avant de créer un produit, il peut être sage de vérifier qu’il existe. en reprenant le premier exemple, cela fait :

// ma référence
	$ref = "ma_reference_produit_X203ZZ";
// existe-t-elle dans mon tableau
	$produitKey = array_search($ref, $listProduits);
	if ($produitKey) {
// oui
		$fk_product = $produitKey;
	} else {
// non
// Créer un produit
		$newProduct = [
			"ref"	=> $ref,
			"label"	=> $ref
		];
		$newProductResult = CallAPI("POST", $apiKey, $apiUrl."products", json_encode($newProduct));
		$newProductResult = json_decode($newProductResult, true);
		if (isset($newProductResult["error"]) && $newProductResult["error"]["code"] >= "300") {
// il y a eu une erreur
			echo "<pre>ERROR", var_dump($newProductResult), "</pre>";
			exit;
		} else {
// tout va bien
			$fk_product = $newProductResult;
			$listProduits[$fk_product] = $ref;
		}
	}

Commentaires :
- je regarde si la référence de mon article existe dans le tableau créé dans le premier exemple.
- si elle existe, j’utilise sa clé dans le tableau comme id
- si elle n’existe pas, je crée l’article puis le j’ajoute à mon tableau pour les prochaines fois et je récupère l’id créé
- j’ai opté pour cette méthode afin de limiter les appels API quand je dois importer 500 commandes par exemple. Je récupère une fois la liste des produits au début au lieu de chercher à chaque fois dans Dolibarr.

// créer une commande avec 2 articles

// le tableau qui contiendra toutes les lignes d'articles de la commande
	$newCommandeLine = [];

// article 1
	$ref1 = "ma_reference_produit_X203ZZ";
	$prix1 = 10;
	$qtt1  = 100;
	$tva1 = 20;
	$fk_product1
// article 2
	$ref2 = "ma_reference_produit_B707FD";
	$prix2 = 13;
	$qtt2  = 37;
	$tva2 = 20;

	$newCommandeLine[] = [
		"desc"		=> $ref1,
		"subprice"	=> $prix1,
		"qty"		=> $qtt1,
		"tva_tx"	=> floatval($tva1),
		"fk_product"=> $fk_product1
	];

	$newCommandeLine[] = [
		"desc"		=> $ref2,
		"subprice"	=> $prix2,
		"qty"		=> $qtt2,
		"tva_tx"	=> floatval($tva2),
		"fk_product"=> $fk_product2
	];

	if (count($newCommandeLine) > 0) {
		$newCommande = [
			"socid"			=> $clientDoliId,
			"type" 			=> "0",
			"lines"			=> $newCommandeLine,
			"note_private"	=> "Commande importée automatiquement depuis l'application",
		];
		$newCommandeResult = CallAPI("POST", $apiKey, $apiUrl."orders", json_encode($newCommande));
		$newCommandeResult = json_decode($newCommandeResult, true);
	}

Commentaires :
- $clientDoliId vaut l’id du client dans la base doli. Soit vous le connaissez, soit vous pouvez le chercher auparavant
- type => 0, c’est une commande client (par opposition à 1 = commande fournisseur)

// Valider une commande 
	$newCommandeValider = [
		"idwarehouse"	=> "0",
		"notrigger"		=> "0"
	];
	$newCommandeValiderResult = CallAPI("POST", $apiKey, $apiUrl."orders/".$newCommandeResult."/validate", json_encode($newCommandeValider));
	$newCommandeValiderResult = json_decode($newCommandeValiderResult, true);

Commentaires :
- on voit dans cet exemple, en avant dernière lignes, on a : $apiUrl.« orders/ ».$newCommandeResult."/validate".
$newCommandeResult est l’id de la commande crée (récupéré dans l’exemple précédent)

// chercher si le client existe dans la base
	$clientSearch = json_decode(CallAPI("GET", $apiKey, $apiUrl."thirdparties", array(
		"sortfield" => "t.rowid", 
		"sortorder" => "ASC", 
		"limit" => "1", 
		"mode" => "1",
		"sqlfilters" => "(t.nom:=:'".$nom_client."')"
		)
	), true);

Commentaires :
- limit => 1 pour ne renvoyer que 1 client
- mode => 1 car on cherche un client (on aurait aussi pu chercher un fournisseur qui est aussi un tiers mais avec un statut différent)
- sqlfilters syntaxe un peu particulière mais il y a qq autres exemples sur la page d’explorer d’API

//client n'existe pas. le crée puis on récupère son id
	$newClient = [
		"name" 			=> "nom société client",
		"email"			=> "email société client",
		"client" 		=> "1",
		"code_client"	=> "-1"
	];
	$newClientResult = CallAPI("POST", $apiKey, $apiUrl."thirdparties", json_encode($newClient));
	$newClientResult = json_decode($newClientResult, true);
	$clientDoliId = $newClientResult;

Commentaires :
- client => 1 car c’est un client (et pas un fournisseur)
- code_client => -1 pour que le code client soit généré automatiquement.
- on récupère l’id du client dans $clientDoliId

*** CONCLUSION

Voila, c’est loin d’être parfait mais ça donne les bases.
Vous trouverez pas mal d’autres informations dans le code de Dolibarr, en regardant dans htdocs/<dossier>/class/api_xxx_class.php
Ex : htdocs/societe/class/api_thirdparties.class.php pour les tiers.
les factures se trouvent dans htdocs/compta/facture/class

Si vous avez des questions, n’hésitez pas, je répondrai dans la mesure de mes compétences.
Bon courage à tous

Chag

17 « J'aime »

:sunglasses: +1
Ça mérite une bonne place sur le wiki !
Merci pour cet article

1 « J'aime »

Je fais comment ?

Sur le wiki il est noté :
ask to [email protected] to request an account to contribute to this documentation :wink:

Bonjour
Je vous ai créé un compte sur le wiki pour compléter la page existante :
https://wiki.dolibarr.org/index.php/Module_Web_Services_REST
j’ai envoyé le login et mdp par mail
@+
Philippe

2 « J'aime »

allez @chag au boulot :laugh:

Mission accomplie :happy:
en Anglais et en Français

6 « J'aime »

:woohoo:
MERCI !!!

Bonjour,

Je recherche à réaliser les actions suivantes via l’API REST :

  1. Création d’une facture à partir d’une commande validée (orderid=1)
  2. Ajouter des lignes à une facture nouvelle (invoiceid=1)
  3. Valider une facture

Pourriez-vous m’assister ?
Cordialement,

Bonjour,

  1. Créer une facture à partir d’une commande, je n’ai pas trouvé. J’imagine que ce doit être possible mais je n’ai pas creusé davantage.
  2. Pour modifier une facture, en utilisant POST et l’adresse : invoices/{id}/lines (ou {id} est l’id de la facture à modifier), vous pourrez ajouter des lignes (d’après l’explorateur d’API)
  3. valider une facture, je n’en ai pas trouvé trace mais vous pouvez essayer en utilisant l’adresse /invoices/{id}/validate. Cela fonctionne pour les bons de commande. Peut-être que cela fonctionne également pour les factures

Bon courage

1 « J'aime »

Bonjour,

Il semble malheureusement que les fonctions passerelles entre l’API et la classe Facture ne soient pas implémentées :unhappy:

La méthode PUT permet effectivement d’apporter des modifications aux éléments d’en-tête d’une facture mais elle ne semble pas permettre d’ajouter des lignes comme cela se fait à travers la fonction postLine ou putLine de la classe api_orders.class

De même, la méthode passerelle validate de l’API pour les factures n’est pas implémentée :unhappy:

Je vais devoir me plonger dans le code de Dolibarr pour tenter moi-même une implémentation de ces fonctions essentielles :pinch:

Je vous remercie de votre réponse,
Cordialement,

PS : DLB version 5.0.4

Je me suis bêtement inspiré de la fonction postLine et validate le la classe api_order.class pour implémenter ces passerelles sur les factures (via api_invoices.class) :

Par contre lorsque j’appelle la méthode il doit manquer quelque chose à faire :whistle:


(…)

L’URL d’appel est donc :
http://XXX/api/index.php/invoices/1/lines?DOLAPIKEY=XXX

Et l’erreur est la suivante :

[code]


404
Not Found

Routes.php:458 at route stage get route negotiate message [/code]

Cordialement,

En recherchant une solution à mon problème j’ai trouvé cette référence :
https://github.com/Dolibarr/dolibarr/issues/6811

J’ai pu ajouté des lignes à ma facture via la méthode POST et l’argument LINES (à la création quoi).
Cette solution pourrait convenir à mon besoin.

A présent j’ai bien généré une facture standard avec les attributs suivants :
- En-tête : Complété
- Lignes : Ajoutées
- Etat : Payée (pas de réglement référencé)
- Statut : Validée (Identification PROVXXX non mise à jour :confused: )

A faire :
- Validation : L’identifiant de la facture demeure PROVXXX au lieu de FA201706-XXX
- Règlement : A générer + lier à la facture via l’API
- Edition PDF : A générer via l’API et récupérer l’url de téléchargement du fichier

Désolé de ne pas avoir répondu plus tot, je bossais sur autre chose.
Pour ajouter des lignes, tu ne peux pas les ajouter à la creation de la facture comme je fais ?

Pas de soucis, rien de pressé, je me débrouille jusque là :wink:
Pour l’ajout de lignes à la création c’est bon, çà marche, et la méthode me convient très bien.

Reste à déterminer comment valider cette facture et générer sa référence FA201706-XXX par contre, ainsi que générer le PDF et le récupérer via l’API

C’est finalement pas bien compliqué :happy:

Comme Dolibarr et mon application sont hébergés sur le même réseau j’ai ajouté la méthode GET /invoices/{id}/documentpath de sorte que mon application puisse directement aller chercher fichier et gérer le DOWNLOAD.
Pour une application distante j’ai ajouté la méthode GET /invoices/{id}/documentstream qui retourne le document en mode binaire.

Maintenant j’aimerais bien implémenter la passerelle sur les produits/services de sorte que mon application n’ait pas à stocker les détails de ceux-ci (tarif/description) et insérer des réglements réalisés depuis l’application externe également.

Top boulot ! Faut partager ou inclure le code dans le core

Pour l’ajout de lignes, j’étais passé aussi par lines au début mais finalement, je me suis aperçu que tu peux créer directement la facture avec toutes les lignes dedans. Du coup, j’ai préféré fonctionner comme ça de façon à limiter le nombre d’appels REST. Quand j’utilise l’API, je crée des lots de factures. Quand tu en fais 1, ça va, tu peux ne pas être trop regardant sur les API call. Quand tu en crée 100 d’un coup, avec création des produits, des clients, des commandes, ça commence à prendre du temps. Ca me semblait un bon moyen.

N’hésitez pas à commenter :

[code]
// File : \compta\facture\class\api_invoices.class.php

(…)
use Luracast\Restler\RestException;

require_once DOL_DOCUMENT_ROOT.’/compta/facture/class/facture.class.php’;
include_once DOL_DOCUMENT_ROOT.’/paypal/lib/paypal.lib.php’;
include_once DOL_DOCUMENT_ROOT.’/core/lib/files.lib.php’;
(…)

/**
 * Validate an invoice
 *
 * @param   int $id             Order ID
 * @param   int $idwarehouse    Warehouse ID
 * @param   int $notrigger      1=Does not execute triggers, 0= execute triggers
 *
 * @url PUT    {id}/validate
 *
 * @return  array
 * FIXME An error 403 is returned if the request has an empty body.
 * Error message: "Forbidden: Content type `text/plain` is not supported."
 * Workaround: send this in the body
 * {
 *   "idwarehouse": 0,
 *   "notrigger": 0
 * }
 */
function validate($id, $idwarehouse=0, $notrigger=0)
{
    if(! DolibarrApiAccess::$user->rights->facture->creer) {
        throw new RestException(401);
    }
    $result = $this->invoice->fetch($id);
    if( ! $result ) {
        throw new RestException(404, 'Invoice not found');
    }

    if( ! DolibarrApi::_checkAccessToResource('facture',$this->invoice->id)) {
        throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login);
    }

    $result = $this->invoice->validate(DolibarrApiAccess::$user, $idwarehouse, $notrigger);
    if ($result == 0) {
        throw new RestException(500, 'Error nothing done. May be object is already validated');
    }
    if ($result < 0) {
        throw new RestException(500, 'Error when validating invoice: '.$this->invoice->error);
    }

    return array(
        'success' => array(
            'code' => 200,
            'message' => 'Invoice validated'
        )
    );
}


/**
 * Generate an invoice document
 *
 * @param   int $id             Invoice ID
 * @param   string $modele      Generator to use.
 * @param   int $hidedetails    Hide details of lines
 * @param   int $hidedesc       Hide description
 * @param   int $hideref        Hide ref
 *
 * @url PUT    {id}/generatedocument
 *
 * @return  array
 * FIXME An error 403 is returned if the request has an empty body.
 * Error message: "Forbidden: Content type `text/plain` is not supported."
 * Workaround: send this in the body
 * {
 *   "modele": '',
 *   "hidedetails": 0,
 *   "hidedesc": 0,
 *   "hideref": 0
 * }
 */
function generateDocument($id, $modele, $hidedetails=0, $hidedesc=0, $hideref=0)
{
    if(! DolibarrApiAccess::$user->rights->facture->creer) {
        throw new RestException(401);
    }
    $result = $this->invoice->fetch($id);
    if( ! $result ) {
        throw new RestException(404, 'Invoice not found');
    }

    if( ! DolibarrApi::_checkAccessToResource('facture',$this->invoice->id)) {
        throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login);
    }

    $result = $this->invoice->generateDocument($modele, NULL, $hidedetails, $hidedesc, $hideref);
    if ($result == 0) {
        throw new RestException(500, 'Error nothing done. May be object is already generated');
    }
    if ($result < 0) {
        throw new RestException(500, 'Error when validating invoice: '.$this->invoice->error);
    }

    return array(
        'success' => array(
            'code' => 200,
            'message' => 'Invoice document generated',
        )
    );
}

/**
 * Get an invoice document storage path
 *
 * @param int   $id             Id of invoice
 *
 * @url GET {id}/documentpath
 *
 * @return object
 */
function getDocumentPath($id)
{
    global $db, $conf;

    if(! DolibarrApiAccess::$user->rights->facture->lire) {
        throw new RestException(401);
    }
        
    $result = $this->invoice->fetch($id);
    if( ! $result ) {
        throw new RestException(404, 'Invoice not found');
    }
    
    if( ! DolibarrApi::_checkAccessToResource('facture',$this->invoice->id)) {
        throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login);
    }

    $ref = dol_sanitizeFileName($this->invoice->ref);
    $fileparams = dol_most_recent_file($conf->facture->dir_output . '/' . $ref, preg_quote($ref, '/').'[^\-]+');
    $filepath = $fileparams['fullname'];

    if (! $filepath || ! is_readable($filepath)) {
        throw new RestException(404, 'Invoice document not generated');
    }

    return array(
        'success' => array(
            'code' => 200,
            'message' => NULL,
            'ref' => $ref,
            'path' => $filepath,
        )
    );
}


/**
 * Get an invoice document stream
 *
 * @param int   $id             Id of invoice
 *
 * @url GET {id}/documentstream
 *
 * @return object
 */
function getDocumentStream($id)
{
    global $db, $conf;

    if(! DolibarrApiAccess::$user->rights->facture->lire) {
        throw new RestException(401);
    }
        
    $result = $this->invoice->fetch($id);
    if( ! $result ) {
        throw new RestException(404, 'Invoice not found');
    }
    
    if( ! DolibarrApi::_checkAccessToResource('facture',$this->invoice->id)) {
        throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login);
    }

    $ref = dol_sanitizeFileName($this->invoice->ref);
    $fileparams = dol_most_recent_file($conf->facture->dir_output . '/' . $ref, preg_quote($ref, '/').'[^\-]+');
    $filepath = $fileparams['fullname'];

    if (! $filepath || ! is_readable($filepath)) {
        throw new RestException(404, 'Invoice document not found');
    }

    $encoding = 'UTF-8';
    $type = 'application/octet-stream';
    $attachment = true;
    $filename = $ref.'.pdf';

    header('Content-Description: File Transfer');
    if ($encoding)   header('Content-Encoding: '.$encoding);
    if ($type)       header('Content-Type: '.$type.(preg_match('/text/',$type)?'; charset="'.$conf->file->character_set_client:''));
    // Add MIME Content-Disposition from RFC 2183 (inline=automatically displayed, atachment=need user action to open)
    if ($attachment) header('Content-Disposition: attachment; filename="'.$filename.'"');
    else header('Content-Disposition: inline; filename="'.$filename.'"');
    header('Content-Length: ' . dol_filesize($filepath));
    // Ajout directives pour resoudre bug IE
    header('Cache-Control: Public, must-revalidate');
    header('Pragma: public');

    ob_clean();
    flush();
        
    readfile($filepath);

}

/**
 * Get an invoice Paypal Payment Url
 *
 * @param int   $id             Id of invoice
 *
 * @url GET {id}/paypalpaymenturl
 *
 * @return object
 */
function getPaypalPaymentUrl($id)
{
    if(! DolibarrApiAccess::$user->rights->facture->lire) {
        throw new RestException(401);
    }
        
    $result = $this->invoice->fetch($id);
    if( ! $result ) {
        throw new RestException(404, 'Invoice not found');
    }
    
    if( ! DolibarrApi::_checkAccessToResource('facture',$this->invoice->id)) {
        throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login);
    }

    return array(
        'success' => array(
            'code' => 200,
            'message' => NULL,
            'ref' => $ref,
            'url' => getPaypalPaymentUrl(0,'invoice',$this->invoice->ref),
        )
    );        

}

(… )[/code]