Aller au contenu principal
Making DigitalMaking Digital
Note technique

Requêtes SQL utiles pour Dolibarr : expéditions, doublons de stock, contacts, impayés

En bref

Ces requêtes en lecture seule répondent à des besoins que l'interface de Dolibarr couvre mal : exporter les expéditions avec leur commande, repérer une commande fournisseur réceptionnée deux fois, lister les contacts d'une catégorie et les factures en retard par commercial. Elles utilisent les noms de tables et de colonnes actuels (llx_receptiondet_batch, ref au lieu de facnumber), filtrent par référence plutôt que par identifiant et passent le mode ONLY_FULL_GROUP_BY de MySQL 8.

Un export que l'interface ne propose pas, un contrôle de stock, une liste de relance : quand Dolibarr ne donne pas directement la réponse, une requête dans phpMyAdmin (ou tout autre client MySQL) la donne souvent. Voici cinq requêtes en lecture seule (SELECT), mises à jour pour le schéma actuel et vérifiées avec le mode ONLY_FULL_GROUP_BY, actif par défaut sur MySQL 8. Les anciennes versions qui circulent échouent pour la plupart : colonne renommée, GROUP BY incomplet, mauvaise table.

Les valeurs entre apostrophes (référence d'entrepôt, dates, libellé de catégorie) viennent de la base de démonstration de Dolibarr : remplacez-les par les vôtres. Aucune requête ne dépend d'un identifiant écrit en dur.

Pourquoi pas des variables SET @... ?

Nous avions d'abord placé ces valeurs dans des variables (SET @entrepot = '...'). Sur MariaDB, la comparaison avec une colonne a échoué : ERROR 1267 (HY000): Illegal mix of collations (utf8mb3_unicode_ci,IMPLICIT) and (utf8mb3_general_ci,IMPLICIT). Une variable prend la collation de la connexion, alors qu'une valeur écrite directement s'adapte à celle de la colonne. Nous gardons donc des valeurs directes.

Comment exporter les expéditions avec leur commande d'origine ?

Une expédition n'a pas de colonne "commande" : le lien est dans llx_element_element (source commande, cible shipping), la jointure que Dolibarr fait lui-même dans la classe Expedition. L'entrepôt est porté par chaque ligne d'expédition. Pour trouver la référence d'un entrepôt :

SQL
SELECT rowid, ref, lieu FROM llx_entrepot ORDER BY ref;

Puis les expéditions validées d'un entrepôt sur une période, avec la commande, le client et la quantité totale :

SQL
SELECT e.ref AS expedition,
       DATE(e.date_valid) AS validee_le,
       c.ref AS commande,
       s.nom AS client,
       SUM(ed.qty) AS quantite
FROM llx_expedition AS e
JOIN llx_expeditiondet AS ed ON ed.fk_expedition = e.rowid
JOIN llx_entrepot AS w ON w.rowid = ed.fk_entrepot
JOIN llx_societe AS s ON s.rowid = e.fk_soc
LEFT JOIN llx_element_element AS el
       ON el.fk_target = e.rowid
      AND el.targettype = 'shipping'
      AND el.sourcetype = 'commande'
LEFT JOIN llx_commande AS c ON c.rowid = el.fk_source
WHERE w.ref = 'WAREHOUSEHOUSTON'  -- réf. de l'entrepôt
  AND e.fk_statut > 0
  AND e.date_valid >= '2025-01-01'
  AND e.date_valid < '2025-12-31' + INTERVAL 1 DAY
GROUP BY e.rowid, e.ref, e.date_valid, c.ref, s.nom
ORDER BY e.ref;

date_valid contient aussi l'heure : BETWEEN '...' AND '2025-12-31' s'arrête à minuit et oublie les expéditions du dernier jour, d'où le < date + INTERVAL 1 DAY. L'ancienne version de cette requête, groupée sur la seule référence, est refusée par MySQL 8 : ERROR 1055 (42000): Expression #1 of SELECT list is not in GROUP BY clause.

Comment repérer une commande fournisseur réceptionnée deux fois ?

Une réception enregistrée deux fois gonfle le stock (pour repartir d'un stock propre, voir remettre à zéro un Dolibarr). Chaque ligne réceptionnée est stockée dans llx_receptiondet_batch (l'ancienne llx_commande_fournisseur_dispatch, renommée par la migration vers Dolibarr 20). Cette requête liste les lignes de commande reçues en plus grande quantité que commandée :

SQL
SELECT cf.ref AS commande_fournisseur,
       p.ref AS produit,
       cfd.qty AS commandee,
       SUM(rb.qty) AS recue,
       COUNT(*) AS nb_lignes_reception
FROM llx_receptiondet_batch AS rb
JOIN llx_commande_fournisseurdet AS cfd ON cfd.rowid = rb.fk_elementdet
JOIN llx_commande_fournisseur AS cf ON cf.rowid = cfd.fk_commande
LEFT JOIN llx_product AS p ON p.rowid = cfd.fk_product
WHERE rb.element_type = 'supplier_order'
GROUP BY cfd.rowid, cf.ref, p.ref, cfd.qty
HAVING SUM(rb.qty) > cfd.qty
ORDER BY cf.ref;

Côté stock, les mouvements portent leur origine : order_supplier quand la ventilation se fait depuis la commande, reception quand le module Réceptions est utilisé. Celle-ci cherche les mouvements identiques (même origine, produit, entrepôt et quantité) :

SQL
SELECT COALESCE(cf.ref, r.ref) AS origine,
       p.ref AS produit,
       w.ref AS entrepot,
       m.value AS quantite,
       COUNT(*) AS nb_mouvements,
       MIN(m.datem) AS premier,
       MAX(m.datem) AS dernier
FROM llx_stock_mouvement AS m
JOIN llx_product AS p ON p.rowid = m.fk_product
JOIN llx_entrepot AS w ON w.rowid = m.fk_entrepot
LEFT JOIN llx_commande_fournisseur AS cf
       ON m.origintype = 'order_supplier' AND cf.rowid = m.fk_origin
LEFT JOIN llx_reception AS r
       ON m.origintype = 'reception' AND r.rowid = m.fk_origin
WHERE m.origintype IN ('order_supplier', 'reception')
GROUP BY m.origintype, m.fk_origin, cf.ref, r.ref, p.ref, w.ref, m.value
HAVING COUNT(*) > 1
ORDER BY origine;

Dans notre test, une commande de 10 unités ventilée deux fois ressort dans les deux requêtes (10 commandées, 20 reçues, 2 mouvements). Deux réceptions partielles de même quantité sont pourtant normales : vérifiez avant de corriger, puis corrigez par Corriger le stock sur la fiche produit plutôt qu'en supprimant des lignes. L'ancienne requête comparait la table à elle-même sans exclure la ligne courante : chaque mouvement se trouvait lui-même, et elle renvoyait les 104 mouvements ayant une origine.

Comment lister les contacts d'une catégorie ?

Les catégories de contacts sont dans llx_categorie_contact, reliée à llx_socpeople (les contacts). L'ancienne requête lisait llx_societe par llx_categorie_societe, c'est-à-dire les catégories de tiers. Nous filtrons sur le libellé et sur le type 4 (contact), car un même libellé peut exister pour les produits ou les tiers :

SQL
SELECT ct.lastname AS nom, ct.firstname AS prenom, ct.email, ct.phone, ct.phone_mobile,
       s.nom AS societe, co.label AS pays
FROM llx_socpeople AS ct
JOIN llx_categorie_contact AS cc ON cc.fk_socpeople = ct.rowid
JOIN llx_categorie AS cat ON cat.rowid = cc.fk_categorie AND cat.type = 4
LEFT JOIN llx_societe AS s ON s.rowid = ct.fk_soc
LEFT JOIN llx_c_country AS co ON co.rowid = ct.fk_pays
WHERE cat.label = 'Family'  -- libellé de la catégorie
ORDER BY ct.lastname, ct.firstname;

Le pays est celui du contact (fk_pays), pas celui de sa société ; un contact sans société apparaît aussi grâce au LEFT JOIN.

Comment lister les factures en retard par commercial ?

Dolibarr sait déjà le faire : dans la liste des factures, le filtre Tiers ayant pour commercial et la case Alerte (factures validées dont l'échéance est dépassée) donnent la liste à l'écran. La requête sert pour un export ou un tableau de bord :

SQL
SELECT CONCAT_WS(' ', u.firstname, u.lastname) AS commercial,
       s.nom AS client,
       f.ref AS facture,
       f.date_lim_reglement AS echeance,
       DATEDIFF(CURDATE(), f.date_lim_reglement) AS jours_de_retard,
       f.total_ttc,
       COALESCE(pf.deja_paye, 0) AS deja_paye,
       f.total_ttc - COALESCE(pf.deja_paye, 0) AS reste_a_payer
FROM llx_facture AS f
JOIN llx_societe AS s ON s.rowid = f.fk_soc
LEFT JOIN llx_societe_commerciaux AS sc ON sc.fk_soc = s.rowid
LEFT JOIN llx_user AS u ON u.rowid = sc.fk_user
LEFT JOIN (
    SELECT fk_facture, SUM(amount) AS deja_paye
    FROM llx_paiement_facture
    GROUP BY fk_facture
) AS pf ON pf.fk_facture = f.rowid
WHERE f.fk_statut = 1
  AND f.paye = 0
  AND f.type <> 2
  AND f.date_lim_reglement < CURDATE()
ORDER BY commercial, jours_de_retard DESC;
  • facnumber n'existe plus : la colonne s'appelle ref depuis la migration vers Dolibarr 10.
  • CURDATE() remplace la date écrite en dur, et f.type <> 2 écarte les avoirs.
  • Une société suivie par deux commerciaux fait apparaître la facture deux fois, une par commercial ; une société sans commercial apparaît avec un commercial vide.
  • deja_paye ne compte que les paiements : les avoirs et acomptes imputés ne sont pas déduits. Pour le montant exact restant dû, la liste de Dolibarr fait foi.

Ces requêtes servent souvent de point de départ à un export automatique vers un autre outil : c'est le travail de nos connecteurs. Un besoin de reporting sur votre Dolibarr ? Parlons-en.

À retenir
  • Les liens entre documents (commande, expédition, facture) passent par llx_element_element.
  • Les contacts sont dans llx_socpeople, leurs catégories dans llx_categorie_contact (type 4).
  • Filtrer par référence ou libellé plutôt que par identifiant, et mettre toutes les colonnes non agrégées dans le GROUP BY.
  • Pour une date avec heure, utiliser < date + INTERVAL 1 DAY plutôt que BETWEEN.

Testé avec Dolibarr 24.0.2 et MariaDB 11.4.13 ; requêtes également exécutées sur MySQL 8.4.11 et MySQL 5.7.33 avec ONLY_FULL_GROUP_BY.

Questions fréquentes

Où exécuter ces requêtes ?

Dans phpMyAdmin, Adminer ou le client mysql, sur la base de Dolibarr dont les identifiants figurent dans conf/conf.php. Ce sont des SELECT : elles ne modifient rien.

Mes tables ne commencent pas par llx_, est-ce grave ?

Non. llx_ est le préfixe par défaut, choisi à l'installation et visible dans conf/conf.php (variable $dolibarr_main_db_prefix). Remplacez-le dans chaque requête par le vôtre.

Que signifie l'erreur 1055 "not in GROUP BY clause" ?

Le mode ONLY_FULL_GROUP_BY, actif par défaut sur MySQL 8, refuse une colonne affichée qui n'est ni agrégée ni dans le GROUP BY. Il faut ajouter ces colonnes au GROUP BY, comme dans les requêtes de cette note.

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.