Déployer Dolibarr : ETL Checklist avec des exemples concrets

(Version française – à destination des équipes IT, développeurs et chefs de projet qui souhaitent intégrer Dolibarr dans leurs flux de données)


1. Introduction

Dolibarr est un ERP/PGPM (Gestion de petites entreprises) open‑source très populaire parmi les PME qui souhaitent disposer d’une solution autogérée, évolutive et facile à prendre en main.
Pour en tirer pleinement parti dans un contexte ETL (Extract‑Transform‑Load), il faut :

  1. Exporter les données depuis Dolibarr (ou les sources tierces qui l’alimentent).
  2. Transformer ces données pour les adapter au modèle cible (BI, Data‑Warehouse, CRM, etc.).
  3. Charger les données dans le système de destination.

Cet article propose une checklist détaillée à suivre pour chaque phase, accompagnée d’exemples concrets (scripts, requêtes SQL, configurations API) que vous pourrez adapter immédiatement à votre projet.


2. Prérequis techniques

Élément Version recommandée Pourquoi
Dolibarr v17+ (ou la dernière stable) Ajoute des points d’extension (hooks, API REST) et améliore la stabilité de l’API.
Serveur web Apache 2.4+ / Nginx 1.23+ Fournit les réécritures d’URL nécessaires aux appels API.
PHP 7.4 – 8.2 (selon la version de Dolibarr) Support de Typed Properties et de mysqli moderne.
Base de données MySQL/MariaDB 10.3+ Dolibarr stocke tout dans des tables simples, faciles à exploiter.
Outils ETL Pentaho Data Integration (PDI), Talend, ou scripts Python/SQLAlchemy Vous pouvez choisir l’outil qui s’intègre le mieux à votre chaîne CI/CD.
Système de contrôle de version Git Pour versionner les scripts ETL et les configurations (Dockerfile, Makefile, .env).
Sécurité TLS/HTTPS, secret management (Vault, AWS Secrets Manager) Protéger les mots de passe et les clés d’API.

⚠️ Astuce : Si vous déployez Dolibarr dans Docker, créez un docker-compose.yml avec les variables d’environnement DOLIBARR_DSN, DOLIBARR_USER, DOLIBARR_PASSWORD. Cela simplifie la restauration et la migration des bases de données.


3. Checklist ETL – Phase par phase

3.1 Extraction (E)

✅ Action Détails Exemple concret
Activer l’API REST de Dolibarr Dans Administration → Gestion avancée → Web Services, cocher “Enable REST API”. Choisir les modules à exposer (ex : cart, product, account). GET /api/dictionaries pour la liste des domaines (modules) actifs.
Obtenir les credentials Créez un User dédié avec les droits read sur les tables d’intérêt (ex : llxf_user, llxf_product, llxf_order). Enregistrez le mot de passe dans un secret manager. username: etl_user
password: ${ETL_PASSWORD}
Tester l’accès à une ressource bash curl -s -u "$ETL_USER:$ETL_PASSWORD" "https://erp.example.com/dolibarr/api/v1/product?range=1-100" \| jq . | Retour JSON contenant id, name, price, fk_category.
Identifier les sources complémentaires Export CSV/Excel de rapports personnalisés (ex : stocks par entrepôt) si vous avez des modèles métiers spécifiques. SELECT ref, label FROM llxf_product WHERE active = 1; → sauvegarder en /tmp/products.csv.

Exemple de script d’extraction (Python + requests)

import os, requests, json
BASE_URL = "https://erp.example.com/dolibarr/api/v1"
USER = os.getenv("ETL_USER")
PASS = os.getenv("ETL_PASSWORD")
def fetch_table(table, limit=None):
url = f"{BASE_URL}/{table}"
params = {}
if limit:
params["range"] = f"1-{limit}"
resp = requests.get(url, auth=(USER, PASS), params=params)
resp.raise_for_status()
return resp.json()
products = fetch_table("product", limit=500) # première page d'exemple
with open("products.json", "w", encoding="utf-8") as f:
json.dump(products, f, ensure_ascii=False, indent=2)
print(f"✔️ {len(products)} produits récupérés")

Points de vigilance

  • Respecter le rate‑limit (Dolibarr: 60 req/min par défaut). Utilisez sleep ou le paramètre range pour la pagination.
  • Gérer les erreurs 429 (Too Many Requests) en implémentant une stratégie d’attente exponentielle.


3.2 Transformation (T)

✅ Action Détails Exemple concret
Normaliser les champs Renommer les clés JSON, convertir les dates ('2024-09-08 14:23:00' → ISO‑8601), mapper les listes (ex : catégories). category_id = item["categories"][0]["id"].
Enrichir les données Ajouter des attributs dérivés : price_including_tax = price * 1.20 ou calculer le délai moyen de livraison à partir de order_date. margin_rate = (sale_price - purchase_price) / purchase_price.
Gestion des nulls / duplicates Supprimer les doublons (product_id unique), remplir les champs obligatoires avec des valeurs par défaut. if not item["price"]: item["price"] = 0.0.
Appliquer le schéma cible Construire un dictionnaire Python/Flattened JSON qui correspond exactement au modèle de la table de destination (ex : dim_product). python\nschema = {"sku": item["reference"], "name": item["name"], "category_id": item["category_id"], "price_ht": item["price"], "tax_rate": item["vat_price"]}\n
Persistir les transformations Sauvegarder les fichiers de mapping (CSV, YAML) dans le repo Git pour versionner les changements. mappings/product.yaml.
Optimiser la taille Filtrer les colonnes inutiles (description_long > 5000 caractères souvent non utilisé). del item["description_long"].

Exemple de transformation (YAML + Jinja2)

mappings/product.yaml :

fields:
sku: reference
name: name
category_id: categories_id
price_ht: price
tax_rate: vat_rate

transform_product.py :

import yaml, json
from jinja2 import Template
with open("mappings/product.yaml") as f:
cfg = yaml.safe_load(f)
def render(item):
tmpl = Template(
'{"sku": "{{ sku }}","name":"{{ name|e }}","category_id":{{ category_id }},"price_ht":{{ price_ht }},"tax_rate":{{ tax_rate }}'
)
return tmpl.render(**{k: str(v) if isinstance(v, (int, float)) else v for k, v in cfg.items()})
data = json.load(open("products.json"))
transformed = [json.loads(render(p)) for p in data]
with open("products_transformed.json", "w", encoding="utf-8") as f:
json.dump(transformed, f, ensure_ascii=False, indent=2)
print(f"✔️ {len(transformed)} lignes prêtes pour le chargement")

Bonnes pratiques

  • Centraliser tous les mappings dans un répertoire /mappings/ versionné.
  • Utiliser des tests unitaires (pytest) pour vérifier que chaque transformation respecte le contrat attendu (ex : price_ht > 0).


3.3 Chargement (L)

✅ Action Détails Exemple concret
Choisir le mécanisme de destination INSERT direct via MySQL client
• Bulk load (LOAD DATA INFILE)
• API intermédiaire (ex : PostgREST)
Pour 10 k lignes, LOAD DATA INFILE est souvent 5‑10× plus rapide.
Créer le schéma cible DDL correspondant au modèle du Data‑Warehouse (ex : dim_product, fact_sales). sql\nCREATE TABLE dim_product (\n sku VARCHAR(20) PRIMARY KEY,\n name VARCHAR(255),\n category_id INT,\n price_ht DECIMAL(10,2),\n tax_rate DECIMAL(5,2)\n);\n
Générer le fichier de chargement CSV avec un header exactement aligné avec les colonnes de la table. products_transformed.csv| sku | name | category_id | price_ht | tax_rate |\n.
Limiter les tailles de lot Batch de 10 000 lignes max pour éviter les_TIMEOUT du serveur MySQL. INSERT INTO dim_product ... VALUES (...), (...), ...;
Gestion des erreurs Enrichir le log avec la ligne fautive, corriger les données errantes, ré‑essayer ou basculer en mode “rejet”. ON DUPLICATE KEY UPDATE ... pour gérer les doublons.
Post‑chargement Mettre à jour les index, exécuter ANALYZE TABLE, vérifier les comptages (SELECT COUNT(*) FROM dim_product). echo "Post‑load validation: $(mysql -N -s -e \"SELECT COUNT(*) FROM dim_product\")"

Exemple de chargement avec LOAD DATA INFILE (MySQL)

# 1️⃣ Créer le fichier CSV (déjà généré par le script Python)
# 2️⃣ Exporter vers le serveur cible
scp products_transformed.csv user@target-server:/tmp/
# 3️⃣ Exécution MySQL (via SSH)
ssh user@target-server <<'EOF'
mysql -u etl_user -p${ETL_PASSWORD} -h localhost db_warehouse <<SQL
LOAD DATA LOCAL INFILE '/tmp/products_transformed.csv'
INTO TABLE dim_product
FIELDS TERMINATED BY ',' ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 ROWS
(sku, name, category_id, price_ht, tax_rate);
SQL
EOF
echo "✅ Chargement terminé – vérification :"
ssh user@target-server "mysql -N -s -e \"SELECT COUNT(*) FROM dim_product;\" db_warehouse"

Astuces de performance

  • Désactiver les checks (SET GLOBAL sql_log_bin=0; temporairement si vous avez des réplications).
  • Utiliser les transactions : Envelopper plusieurs lots dans une transaction (START TRANSACTION; … COMMIT;).
  • Coupler avec pt-archiver (Percona Toolkit) pour archiver les lignes supprimées de Dolibarr après migration (optimisation de la table source).


4. Exemples concrets de projets ETL avec Dolibarr

4.1 Migration des contacts clients vers un CRM (ex : HubSpot)

Étape Détail
Extraction GET /api/v1/contact?range=1-2000 → JSON contenant id, email, company, address, tel.
Transformation Ajouter un champ hubspot_owner_id (hash de l’email) ; netoyer les espaces ; convertir les dates (format YYYY-MM-DD).
Chargement Poster chaque contact via l’API HubSpot (POST https://api.hubapi.com/contacts/v1/contact) avec token d’API. Implémenter une file d’attente (RabbitMQ) pour gérer le débit (max 10 req/s).
Vérification Lister les contacts importés via /contacts/v1/contact/de-duplicate et comparer les compteurs (SELECT COUNT(*) FROM llxf_contact).
Nettoyage Dolibarr Archiver les contacts exportés (pt-archiver ou script SQL DELETE FROM llxf_contact WHERE exported = 1).

Script de chargement HubSpot (Python)

import requests, json, time, os
HUBSPOT_TOKEN = os.getenv("HUBSPOT_TOKEN")
endpoint = "https://api.hubapi.com/contacts/v1/contact"
def upsert_contact(payload):
hdr = {
"Authorization": f"Bearer {HUBSPOT_TOKEN}",
"Content-Type": "application/json"
}
resp = requests.post(f"{endpoint}", headers=hdr, json=payload)
if resp.status_code in (200, 201):
return resp.json()
else:
raise RuntimeError(f"Erreur HubSpot {resp.status_code}: {resp.text}")
for c in transformed_contacts:
payload = {
"properties": {
"email": c["email"],
"firstname": c["firstname"],
"lastname": c["lastname"],
"company": c["company"]
}
}
try:
upsert_contact(payload)
print(f"✔️ Importé {payload['properties']['email']}")
except Exception as e:
print(f"❌ Erreur sur {payload['properties']['email']}: {e}")
time.sleep(0.1) # throttling 10 req/s


4.2 Construction d’un Data‑Warehouse des ventes (dimensionnel)

Source Destination Exemple de transformation
llxf_invoice (factures) fact_sales sale_amount = total_price * (1 - discount_rate) ; créer sale_date_key (YYYYMMDD).
llxf_product dim_product Normaliser le champ priceprice_ht ; mapper category_iddim_category_id.
llxf_order dim_time (calendrier) Convertir order_dateorder_timestamp ; enrichir avec holiday_flag.
llxf_stock dim_stock Ajouter stock_level_normalized = stock_quantity / warehouse_capacity.

Modèle de faits fact_sales (exemple MySQL)

CREATE TABLE fact_sales (
invoice_id BIGINT PRIMARY KEY,
product_sku VARCHAR(20),
qty INT,
unit_price_ht DECIMAL(12,2),
total_price_ht DECIMAL(12,2),
sale_timestamp DATETIME,
date_key INT -- YYYYMMDD format integer
);

Exemple de script ETL (Talend Component – « tDolibarrInput »)

Talend fournit un Connector natif pour MySQL qui permet d’extraire directement les tables. Le Job suivant effectue :

  1. tDolibarrInputtFilterRows (filtrer status = 'paid').
  2. tMap → calcul du champ sale_timestamp via TalendDate.parseDate(orderdate, 'yyyy-MM-dd HH:mm:ss').
  3. tMysqlOutput → insertion bulk dans fact_sales.

Le script de mapping généré par Talend ressemble à :

INSERT INTO fact_sales (invoice_id, product_sku, qty, unit_price_ht, total_price_ht, sale_timestamp, date_key)
SELECT
i.id,
i.ref,
i.qty,
i.price,
i.total_price,
i.payment_date,
DATE_FORMAT(i.payment_date, '%Y%m%d')
FROM llxf_invoice i
WHERE i.status = 'paid' AND i.fk_product <> 0;


5. Checklist finale avant le lancement en production

✅ Élément Vérifié
Environnement de test complet (Docker + DB de staging) ✔️
Secrets stockés dans Vault / AWS Secrets Manager, non en clair dans le repo ✔️
Tests unitaires sur chaque transformation (≥ 80 % de couverture) ✔️
Tests d’intégrité : validation des totaux pré/post‑chargement (ex : Σ(price_ht) avant = Σ(price_ht) après ± 0,01 %) ✔️
Plan de rollback : scripts de restauration de la table source (INSERT … SELECT …) ✔️
Documentation à jour : diagramme d’architecture, description des champs, métriques SLA ✔️
Monitorisation : alertes sur latence API Dolibarr, taux d’erreur ETL (> 1 %), taille des lots ✔️
Sécurité : chiffrement des flux (TLS) entre Dolibarr et le serveur ETL ; auth par token, non par Basic Auth ✔️
Performance : temps d’extraction & chargement < 5 min pour 50 k lignes (benchmark) ✔️
Résiliences : gestion des erreurs (retry exponential backoff, DLQ) ✔️

📌 Bonus : Intégrer le pipeline dans votre CI/CD (Gitlab Actions / GitHub Actions) avec les étapes suivantes :

  1. docker-compose up -d (déployer un conteneur Dolibarr de test).
  2. python test_extraction.py.
  3. pytest transforms/.
  4. bash load_to_target.sh (en mode dry‑run).
  5. if [ $? -eq 0 ]; then git tag -a etl-<date> -m "ETL OK"; fi.


6. Conclusion

Déployer Dolibarr dans un flux ETL n’est pas une simple exportation de CSV ; c’est un processus maîtrisé par :

  1. Une API bien configurée (REST, pagination, throttling).
  2. Des transformations explicites et versionnées (mappings, tests).
  3. Un chargement optimisé (bulk, indexes, gestion des erreurs).

En suivant la checklist détaillée ci‑dessus, vous disposez d’un plan d’action opérationnel avec des exemples concrets que vous pouvez copier‑coller et adapter immédiatement à vos environnements (CRM, Data‑Warehouse, migration vers un autre ERP, etc.).

Prochaine étape :

  • Clonez le repository dolibarr-etl-demo (disponible sur GitHub).
  • Lancez le docker-compose up -d.
  • Exécutez le pipeline complet (./run_etl.sh) et observez les logs de validation.

Bonne intégration ! 🎉


À propos de l’auteur :
Ingénieur data & ERP open‑source, spécialisé dans les architectures de données pour PME. Vous pouvez me joindre sur LinkedIn ou via le canal Discord #dolibarr-etl.

Publications similaires