Aller au contenu principal
Making DigitalMaking Digital
Note technique

Remettre à zéro un Dolibarr : vider les données de test sans laisser d'orphelins

En bref

Dolibarr n'a pas de bouton pour effacer les données de test : il faut passer par SQL, après une sauvegarde de la base et du dossier des documents. Sur une version récente, il faut vider aussi les paiements, les lignes bancaires, la table des liens llx_element_element, les attributs supplémentaires et les événements, sinon des orphelins restent. Pour le stock seul d'une base en production, l'inventaire intégré à Dolibarr est la bonne méthode.

Pendant les essais, l'équipe a créé des devis, des commandes, des expéditions, des factures et des mouvements de stock. Avant le démarrage réel, vous voulez repartir d'une base propre en gardant les tiers, les produits et les réglages. Dolibarr n'a pas de bouton pour cela : il faut passer par SQL, en commençant par une sauvegarde, et vider aussi les tables de liens. Sinon les paiements laissent des lignes dans les comptes bancaires et l'interface garde des liens vers des documents qui n'existent plus.

Uniquement sur des données de test

Cette remise à zéro vise une base d'essai ou une base pas encore utilisée pour de vraies ventes. Sur une base qui contient de vraies factures, ne supprimez rien : une facture émise se corrige par un avoir et sa numérotation doit rester continue.

Que faut-il sauvegarder avant de commencer ?

La documentation de Dolibarr distingue deux éléments : la base de données, qui contient tout ce qui a été saisi, et le dossier des documents ($dolibarr_main_data_root dans conf/conf.php), qui contient les PDF et les pièces jointes. Les identifiants de la base sont dans ce même fichier.

Terminal
# Sauvegarde de la base (sur MariaDB récent, la commande s'appelle mariadb-dump)
mysqldump -u utilisateur -p --single-transaction --default-character-set=utf8mb4 nom_de_la_base > dolibarr-avant-remise-a-zero.sql
# Sauvegarde du dossier des documents (ici /var/www/documents)
tar czf documents-avant-remise-a-zero.tar.gz -C /var/www documents
# Retour arrière si besoin
mysql -u utilisateur -p nom_de_la_base < dolibarr-avant-remise-a-zero.sql

Pendant l'opération, vous pouvez empêcher les autres utilisateurs de se connecter : Accueil > Outils d'administration > Sessions utilisateur > Bloquer nouvelles connexions (seul votre identifiant reste accepté).

Dolibarr propose-t-il une remise à zéro intégrée ?

Pas pour l'ensemble des documents. Pour le stock seul, en revanche, il existe une voie propre qui garde l'historique : le module Stocks propose Inventaires. On crée un inventaire pour l'entrepôt, on saisit 0 en quantité réelle (l'aide de Dolibarr le dit : "Définissez la valeur sur 0 pour réinitialiser la quantité"), puis Générer les mouvements et fermer. Pour un seul produit, le bouton Corriger le stock de l'onglet Stock fait la même chose. C'est la bonne méthode sur une base déjà en production.

L'outil Purger des outils d'administration ne convient pas : son option "Effacer tous les fichiers du répertoire" supprime aussi les pièces jointes des tiers, les fichiers de la GED et les sauvegardes.

Quelles tables vider sur une version récente ?

L'ancienne liste qui circule (écrite pour Dolibarr 7) oubliait les factures fournisseur, les paiements, les écritures bancaires, les réceptions, les expéditions, la table des liens llx_element_element, les attributs supplémentaires (*_extrafields) et les événements de l'agenda. Le script ci-dessous couvre le schéma actuel. Il utilise DELETE dans une transaction plutôt que TRUNCATE : les clés étrangères restent vérifiées (une table enfant oubliée bloque l'opération au lieu de laisser des orphelins), et en cas d'erreur rien n'est appliqué. Dans notre test, une table manquante a arrêté le script et la base est restée intacte.

SQL
-- remise-a-zero.sql : documents commerciaux, paiements et stock (tiers, produits et réglages conservés)
START TRANSACTION;

-- 1. Écritures bancaires créées par les paiements de factures (clients et fournisseurs)
DELETE FROM llx_category_bankline WHERE lineid IN (SELECT fk_bank FROM llx_bank_url WHERE type IN ('payment', 'payment_supplier'));
DELETE FROM llx_bank WHERE rowid IN (SELECT fk_bank FROM llx_bank_url WHERE type IN ('payment', 'payment_supplier'));
DELETE FROM llx_bank_url WHERE fk_bank NOT IN (SELECT rowid FROM llx_bank);

-- 2. Paiements, avoirs et remises disponibles
DELETE FROM llx_paiement_facture;
DELETE FROM llx_paiement;
DELETE FROM llx_paiement_extrafields;
DELETE FROM llx_paiementfourn_facturefourn;
DELETE FROM llx_paiementfourn;
DELETE FROM llx_societe_remise_except;

-- 3. Catégories posées sur les documents
DELETE FROM llx_categorie_propal;
DELETE FROM llx_categorie_order;
DELETE FROM llx_categorie_invoice;
DELETE FROM llx_categorie_supplier_proposal;
DELETE FROM llx_categorie_supplier_order;
DELETE FROM llx_categorie_supplier_invoice;

-- 4. Ventes : expéditions, bons de livraison, factures, commandes, devis
DELETE FROM llx_expeditiondet_batch;
DELETE FROM llx_expeditiondet;
DELETE FROM llx_expedition;
DELETE FROM llx_deliverydet;
DELETE FROM llx_delivery;
DELETE FROM llx_facturedet;
DELETE FROM llx_facture;
DELETE FROM llx_facturedet_rec;
DELETE FROM llx_facture_rec;
DELETE FROM llx_commandedet;
DELETE FROM llx_commande;
DELETE FROM llx_propaldet;
DELETE FROM llx_propal;
DELETE FROM llx_propal_extrafields; DELETE FROM llx_propaldet_extrafields;
DELETE FROM llx_commande_extrafields; DELETE FROM llx_commandedet_extrafields;
DELETE FROM llx_expedition_extrafields; DELETE FROM llx_expeditiondet_extrafields;
DELETE FROM llx_delivery_extrafields; DELETE FROM llx_deliverydet_extrafields;
DELETE FROM llx_facture_extrafields; DELETE FROM llx_facturedet_extrafields;
DELETE FROM llx_facture_rec_extrafields; DELETE FROM llx_facturedet_rec_extrafields;

-- 5. Achats : réceptions, factures, commandes et demandes de prix fournisseur
DELETE FROM llx_receptiondet_batch;
DELETE FROM llx_reception;
DELETE FROM llx_facture_fourn_det;
DELETE FROM llx_facture_fourn;
DELETE FROM llx_facture_fourn_det_rec;
DELETE FROM llx_facture_fourn_rec;
DELETE FROM llx_commande_fournisseur_log;
DELETE FROM llx_commande_fournisseurdet;
DELETE FROM llx_commande_fournisseur;
DELETE FROM llx_supplier_proposaldet;
DELETE FROM llx_supplier_proposal;
DELETE FROM llx_reception_extrafields; DELETE FROM llx_receptiondet_batch_extrafields;
DELETE FROM llx_facture_fourn_extrafields; DELETE FROM llx_facture_fourn_det_extrafields;
DELETE FROM llx_facture_fourn_rec_extrafields; DELETE FROM llx_facture_fourn_det_rec_extrafields;
DELETE FROM llx_commande_fournisseur_extrafields; DELETE FROM llx_commande_fournisseurdet_extrafields;
DELETE FROM llx_supplier_proposal_extrafields; DELETE FROM llx_supplier_proposaldet_extrafields;

-- 6. Liens, contacts, événements et index des fichiers qui pointaient vers ces documents
DELETE FROM llx_element_element
WHERE sourcetype IN ('propal', 'commande', 'shipping', 'delivery', 'facture', 'supplier_proposal', 'order_supplier', 'reception', 'invoice_supplier', 'facturerec', 'invoice_supplier_rec')
   OR targettype IN ('propal', 'commande', 'shipping', 'delivery', 'facture', 'supplier_proposal', 'order_supplier', 'reception', 'invoice_supplier', 'facturerec', 'invoice_supplier_rec');
DELETE ec FROM llx_element_contact AS ec
JOIN llx_c_type_contact AS tc ON tc.rowid = ec.fk_c_type_contact
WHERE tc.element IN ('propal', 'commande', 'shipping', 'facture', 'supplier_proposal', 'order_supplier', 'invoice_supplier');
DELETE r FROM llx_actioncomm_resources AS r
JOIN llx_actioncomm AS a ON a.id = r.fk_actioncomm
WHERE a.elementtype IN ('propal', 'order', 'shipping', 'invoice', 'supplier_proposal', 'order_supplier', 'invoice_supplier', 'reception', 'facturerec');
DELETE FROM llx_actioncomm
WHERE elementtype IN ('propal', 'order', 'shipping', 'invoice', 'supplier_proposal', 'order_supplier', 'invoice_supplier', 'reception', 'facturerec');
DELETE FROM llx_ecm_files
WHERE filepath LIKE 'propale/%' OR filepath LIKE 'commande/%' OR filepath LIKE 'expedition/%'
   OR filepath LIKE 'facture/%' OR filepath LIKE 'fournisseur/%' OR filepath LIKE 'supplier_proposal/%'
   OR filepath LIKE 'reception/%';

-- 7. Stock : inventaires, mouvements, stock par entrepôt et par lot
DELETE FROM llx_inventorydet;
DELETE FROM llx_inventory;
UPDATE llx_mrp_production SET fk_stock_movement = NULL;
DELETE FROM llx_stock_mouvement;
DELETE FROM llx_stock_mouvement_extrafields; DELETE FROM llx_inventory_extrafields;
DELETE FROM llx_product_batch;
DELETE FROM llx_product_stock;
UPDATE llx_product AS p
SET p.stock = (SELECT SUM(ps.reel) FROM llx_product_stock AS ps WHERE ps.fk_product = p.rowid),
    p.pmp = 0;

COMMIT;

Trois points à connaître :

  • Certaines tables n'existent que si le module a été activé : l'installation de Dolibarr ne crée pas les fichiers de schéma dont le nom contient un tiret (llx_inventory-stock.sql, llx_mrp_production-mrp.sql, llx_categorie_propal-propal.sql...), ils le sont à l'activation du module. Si MySQL répond "Table doesn't exist", retirez la ligne concernée et relancez.
  • Les lignes bancaires retrouvées par llx_bank_url sont celles des paiements de factures ; les salaires, emprunts ou virements internes restent. Dans notre base de test, se contenter de llx_paiement.fk_bank laissait 38 lignes de paiement dans les comptes.
  • Le stock total affiché sur la fiche produit (llx_product.stock) est une copie de la somme de llx_product_stock. Dolibarr le recalcule avec la même sous-requête à chaque mouvement ; le prix moyen pondéré (pmp) est remis à 0 car il venait des réceptions de test.

Comment remettre à zéro le stock d'un seul entrepôt ?

Si l'inventaire ne convient pas (des milliers de produits de test, par exemple), voici la version limitée à un entrepôt, désigné par sa référence et non par un identifiant :

SQL
-- remise-a-zero-entrepot.sql : un seul entrepôt, désigné par sa référence
START TRANSACTION;
UPDATE llx_mrp_production AS mp
JOIN llx_stock_mouvement AS m ON m.rowid = mp.fk_stock_movement
JOIN llx_entrepot AS w ON w.rowid = m.fk_entrepot
SET mp.fk_stock_movement = NULL
WHERE w.ref = 'WAREHOUSEPARIS';
DELETE m FROM llx_stock_mouvement AS m
JOIN llx_entrepot AS w ON w.rowid = m.fk_entrepot
WHERE w.ref = 'WAREHOUSEPARIS';
DELETE b FROM llx_product_batch AS b
JOIN llx_product_stock AS ps ON ps.rowid = b.fk_product_stock
JOIN llx_entrepot AS w ON w.rowid = ps.fk_entrepot
WHERE w.ref = 'WAREHOUSEPARIS';
DELETE ps FROM llx_product_stock AS ps
JOIN llx_entrepot AS w ON w.rowid = ps.fk_entrepot
WHERE w.ref = 'WAREHOUSEPARIS';
DELETE e FROM llx_stock_mouvement_extrafields AS e
LEFT JOIN llx_stock_mouvement AS m ON m.rowid = e.fk_object
WHERE m.rowid IS NULL;
UPDATE llx_product AS p
SET p.stock = (SELECT SUM(ps.reel) FROM llx_product_stock AS ps WHERE ps.fk_product = p.rowid);
COMMIT;

WAREHOUSEPARIS est un entrepôt de la base de démonstration : remplacez-le par la référence affichée dans Produits > Entrepôts > Liste. Dans notre test, l'autre entrepôt a gardé ses 52 mouvements et le stock total des produits a été recalculé.

Et les PDF déjà générés ?

Ils restent dans le dossier des documents. Supprimez le contenu des dossiers des documents commerciaux, sans toucher à doctemplates (modèles ODT) ni à mycompany (logos) :

Terminal
cd /var/www/documents
rm -rf propale/* commande/* expedition/sending/* expedition/receipt/* facture/* fournisseur/commande/* fournisseur/facture/* fournisseur/payment/* supplier_proposal/* reception/*

Dolibarr recrée les sous-dossiers à la génération suivante : dans notre test, le premier devis validé ensuite a bien produit son PDF.

Comment vérifier que rien ne traîne ?

SQL
SELECT 'factures' AS reste, COUNT(*) AS n FROM llx_facture
UNION ALL SELECT 'factures fournisseur', COUNT(*) FROM llx_facture_fourn
UNION ALL SELECT 'liens paiement / banque', COUNT(*) FROM llx_bank_url WHERE type IN ('payment', 'payment_supplier')
UNION ALL SELECT 'liens entre documents', COUNT(*) FROM llx_element_element WHERE sourcetype IN ('propal', 'commande', 'facture', 'shipping', 'order_supplier') OR targettype IN ('propal', 'commande', 'facture', 'shipping', 'order_supplier')
UNION ALL SELECT 'mouvements de stock', COUNT(*) FROM llx_stock_mouvement
UNION ALL SELECT 'produits avec un stock', COUNT(*) FROM llx_product WHERE stock IS NOT NULL;

Toutes les lignes doivent afficher 0. Pour d'autres contrôles, nos requêtes SQL utiles pour Dolibarr servent de point de départ. Créez ensuite un devis et une facture de test : avec les modèles de numérotation par défaut, qui cherchent le plus grand numéro existant, la numérotation repart à 0001 (PR2610-0001 et FA2610-0001 dans notre test). Les listes des factures, des devis, des stocks et des comptes bancaires s'affichaient sans erreur.

Nous faisons ce type de préparation avant la mise en route d'un ERP, avec les connecteurs qui l'alimentent, et nous hébergeons et maintenons des instances au quotidien. Une base à nettoyer avant un démarrage ? Parlons-en.

À retenir
  • Sauvegarder la base et le dossier des documents avant tout, et bloquer les nouvelles connexions.
  • Pour le stock d'une base en production, préférer l'inventaire de Dolibarr, qui garde l'historique.
  • Vider aussi les paiements, les lignes bancaires, llx_element_element, les *_extrafields et les événements liés.
  • DELETE dans une transaction plutôt que TRUNCATE : une erreur n'applique rien.

Testé avec Dolibarr 24.0.2 et MariaDB 11.4.13 (scripts également exécutés sur MySQL 8.4.11 et MySQL 5.7.33).

Questions fréquentes

La numérotation des factures repart-elle à 1 après la remise à zéro ?

Oui avec les modèles de numérotation par défaut : ils cherchent le plus grand numéro existant dans la table. Une fois les tables vidées, la facture suivante a reçu le numéro 0001 du mois dans notre test.

Pourquoi ne pas utiliser TRUNCATE, plus rapide ?

TRUNCATE valide immédiatement et ne peut pas être annulé, et il est refusé sur une table référencée par une clé étrangère. DELETE dans une transaction garde les contrôles de clés étrangères et n'applique rien si une erreur survient.

Les tiers, produits et utilisateurs sont-ils conservés ?

Oui. Le script ne touche qu'aux documents commerciaux, aux paiements, aux liens qui pointaient vers eux et au stock. Le stock et le prix moyen pondéré des produits sont remis à zéro, leurs fiches restent.

Que faire si MySQL répond "Table doesn't exist" ?

Certaines tables ne sont créées qu'à l'activation d'un module (inventaire, fabrication, catégories de documents). Retirez la ligne concernée et relancez : grâce à la transaction, la première tentative n'a rien modifié.

Making Digital

Nous connectons les logiciels des PME, automatisons leurs tâches répétitives et créons des assistants IA, depuis 2010. Nos articles partent de nos projets réels.

Notre parcours
Une question sur ce sujet ?

Parlons-en 30 minutes.

Vous saurez si votre problème se règle, comment, et en combien de temps. Sans engagement.

Un échange de 30 minutes, en visio ou par téléphone

Gratuit, sans engagement. On parle de votre problème, pas de technique.