[Faites le vous même] Module autopricecustomer

Bonjour à tous,
Je vous partage un petit module tout simple que j’ai fait avec Claude AI.
Je voulais un système rapide pour savoir quel était le dernier prix que j’avais mis pour le produit à la dernière commande du client (je n’utilise pas les propal)

AutoPriceCustomer - Module Dolibarr

Description

Module pour Dolibarr 20.x qui enregistre automatiquement le prix de vente par client à chaque validation de commande.

Comportement :

  • Lors de la validation d’une commande client (ORDER_VALIDATE), le module parcourt chaque ligne produit.
  • Pour chaque produit (y compris les variantes), il enregistre le prix unitaire HT dans la table llx_product_customer_price.
  • Le numéro de la commande (ex: CO2401-0042) est stocké comme libellé du prix.
  • Si un prix par client existe déjà pour ce produit, il est mis à jour. Sinon, un nouveau prix est créé.
  • L’historique est conservé dans llx_product_customer_price_log (comportement natif de Dolibarr).

Prérequis

  • Dolibarr 18.0+ (testé sur 20.0.0)
  • Le module Produits et Commandes doivent être activés.
  • L’option « Prix différents par client » doit être activée dans :
    Configuration > Modules > Produits > Prix par client
    (constante PRODUIT_CUSTOMER_PRICES = 1)

Installation

  1. Copier le dossier autopricecustomer dans htdocs/custom/
  2. Vérifier que $dolibarr_main_url_root_alt contient /custom dans conf.php
  3. Activer le module dans Accueil > Configuration > Modules > Autres

Fonctionnement avec les variantes

Les variantes de produit sont traitées comme des produits à part entière (chaque variante a son propre fk_product). Le prix unitaire HT saisi sur la ligne de commande est enregistré tel quel pour la variante spécifique.

Logs

Le module écrit dans les logs Dolibarr (syslog) avec le préfixe AutoPriceCustomer: pour faciliter le diagnostic.

Licence

GPLv3+

A placer dans htdoc/custom/


Fichier modAutoPriceCustomer.class.php

<?php
/* Copyright (C) 2024-2026 Patrice
 *
 * Module AutoPriceCustomer
 * Enregistre automatiquement le prix de vente par client
 * à la validation d'une commande client.
 */

include_once DOL_DOCUMENT_ROOT.'/core/modules/DolibarrModules.class.php';

class modAutoPriceCustomer extends DolibarrModules
{
	public function __construct($db)
	{
		global $langs, $conf;

		$this->db = $db;

		$this->numero = 610200; // Numéro unique du module
		$this->rights_class = 'autopricecustomer';
		$this->family = "other";
		$this->module_position = '90';
		$this->name = preg_replace('/^mod/i', '', get_class($this));
		$this->description = "Enregistre automatiquement le prix par client à la validation d'une commande";
		$this->descriptionlong = "Lors de la validation d'une commande client, le module enregistre le prix unitaire de chaque ligne produit dans la table des prix par client. Le numéro de commande est utilisé comme libellé du prix.";
		$this->editor_name = 'DIAMANT INDUSTRIE';
		$this->editor_url = 'https://www.diamant-industrie.com';
		$this->version = '1.0.0';
		$this->const_name = 'MAIN_MODULE_'.strtoupper($this->name);
		$this->picto = 'object_list-alt';

		$this->module_parts = array(
			'triggers' => 1,
		);

		$this->dirs = array();
		$this->config_page_url = array();
		$this->hidden = false;
		$this->depends = array('modCommande', 'modProduct');
		$this->requiredby = array();
		$this->conflictwith = array();
		$this->langfiles = array("autopricecustomer@autopricecustomer");

		$this->tabs = array();
		$this->dictionaries = array();
		$this->boxes = array();
		$this->cronjobs = array();
		$this->rights = array();
		$this->menu = array();
	}

	public function init($options = '')
	{
		$sql = array();
		return $this->_init($sql, $options);
	}

	public function remove($options = '')
	{
		$sql = array();
		return $this->_remove($sql, $options);
	}
}

Bien penser à changer au besoin $this->numero = 610200; // Numéro unique du module

Fichier interface_99_modAutoPriceCustomer_AutoPriceCustomerTriggers.class.php

<?php
/* Copyright (C) 2024-2026 Patrice
 *
 * Trigger AutoPriceCustomer
 * Enregistre le prix par client à la validation de commande.
 */

require_once DOL_DOCUMENT_ROOT.'/core/triggers/dolibarrtriggers.class.php';

class InterfaceAutoPriceCustomerTriggers extends DolibarrTriggers
{
	public function __construct($db)
	{
		$this->db = $db;

		$this->name = preg_replace('/^Interface/i', '', get_class($this));
		$this->family = "product";
		$this->description = "Enregistre automatiquement le prix par client à la validation d'une commande";
		$this->version = '1.0.0';
		$this->picto = 'object_list-alt';
	}

	/**
	 * Fonction appelée lors du déclenchement d'un événement Dolibarr.
	 *
	 * @param string       $action     Code de l'événement
	 * @param CommonObject $object     Objet concerné
	 * @param User         $user       Utilisateur
	 * @param Translate    $langs      Traductions
	 * @param Conf         $conf       Configuration
	 * @return int                     0 = OK, <0 = KO
	 */
	public function runTrigger($action, $object, User $user, Translate $langs, Conf $conf)
	{
		if ($action !== 'ORDER_VALIDATE') {
			return 0;
		}

		// Vérifier que le module "prix par client" est activé
		if (!getDolGlobalInt('PRODUIT_CUSTOMER_PRICES')) {
			dol_syslog("AutoPriceCustomer: PRODUIT_CUSTOMER_PRICES non activé, trigger ignoré.", LOG_WARNING);
			return 0;
		}

		dol_syslog("AutoPriceCustomer: ORDER_VALIDATE déclenché pour commande ".$object->ref." (socid=".$object->socid.")", LOG_INFO);

		require_once DOL_DOCUMENT_ROOT.'/product/class/productcustomerprice.class.php';

		$order = $object;
		$error = 0;

		// S'assurer que les lignes sont chargées
		if (empty($order->lines)) {
			$order->fetch_lines();
		}

		foreach ($order->lines as $line) {
			// Ignorer les lignes sans produit (lignes libres, sous-totaux, etc.)
			if (empty($line->fk_product) || $line->fk_product <= 0) {
				continue;
			}

			$res = $this->recordCustomerPrice($order, $line, $user);
			if ($res < 0) {
				$error++;
			}
		}

		if ($error) {
			dol_syslog("AutoPriceCustomer: ".$error." erreur(s) lors de l'enregistrement des prix.", LOG_ERR);
			return -1;
		}

		return 0;
	}

	/**
	 * Enregistre ou met à jour le prix par client pour une ligne de commande.
	 *
	 * @param  Commande     $order  La commande validée
	 * @param  OrderLine    $line   La ligne de commande
	 * @param  User         $user   L'utilisateur courant
	 * @return int                  1 = OK, <0 = KO
	 */
	private function recordCustomerPrice($order, $line, $user)
	{
		$prodcustprice = new ProductCustomerPrice($this->db);

		// Rechercher un prix existant pour ce produit / ce client
		$filter = array(
			't.fk_product' => (int) $line->fk_product,
			't.fk_soc'     => (int) $order->socid,
		);

		$result = $prodcustprice->fetchAll('', '', 0, 0, $filter);

		if ($result < 0) {
			dol_syslog("AutoPriceCustomer: Erreur fetchAll pour produit ".$line->fk_product." / client ".$order->socid, LOG_ERR);
			return -1;
		}

		// Prix unitaire HT depuis la ligne de commande
		$unitprice_ht  = (float) $line->subprice;
		$tva_tx        = (float) $line->tva_tx;
		$unitprice_ttc = $unitprice_ht * (1 + $tva_tx / 100);

		// Libellé = référence de la commande
		$price_label = $order->ref;

		if (!empty($prodcustprice->lines) && count($prodcustprice->lines) > 0) {
			// --- Mise à jour du prix existant ---
			$existing = $prodcustprice->lines[0];

			// Vérifier si le prix a changé (éviter les écritures inutiles)
			if (round($existing->price, 5) == round($unitprice_ht, 5)
				&& round($existing->tva_tx, 3) == round($tva_tx, 3)
				&& $existing->price_label == $price_label
			) {
				dol_syslog("AutoPriceCustomer: Prix inchangé pour produit ".$line->fk_product." / client ".$order->socid.", skip.", LOG_DEBUG);
				return 1;
			}

			$prodcustprice->id              = $existing->id;
			$prodcustprice->entity          = $existing->entity;
			$prodcustprice->fk_product      = (int) $line->fk_product;
			$prodcustprice->fk_soc          = (int) $order->socid;
			$prodcustprice->ref_customer    = $existing->ref_customer;
			$prodcustprice->price           = $unitprice_ht;
			$prodcustprice->price_ttc       = $unitprice_ttc;
			$prodcustprice->price_min       = $unitprice_ht;
			$prodcustprice->price_min_ttc   = $unitprice_ttc;
			$prodcustprice->price_base_type = 'HT';
			$prodcustprice->tva_tx          = $tva_tx;
			$prodcustprice->recuperableonly = $existing->recuperableonly;
			$prodcustprice->price_label     = $price_label;

			$res = $prodcustprice->update($user, 0, $this);

			if ($res < 0) {
				dol_syslog("AutoPriceCustomer: Erreur update prix produit ".$line->fk_product." / client ".$order->socid.": ".implode(', ', $prodcustprice->errors), LOG_ERR);
				return -1;
			}

			dol_syslog("AutoPriceCustomer: Prix MIS A JOUR - produit ".$line->fk_product." / client ".$order->socid." = ".$unitprice_ht." HT (ref: ".$price_label.")", LOG_INFO);

		} else {
			// --- Création d'un nouveau prix client ---
			$prodcustprice->entity          = $order->entity;
			$prodcustprice->fk_product      = (int) $line->fk_product;
			$prodcustprice->fk_soc          = (int) $order->socid;
			$prodcustprice->ref_customer    = '';
			$prodcustprice->price           = $unitprice_ht;
			$prodcustprice->price_ttc       = $unitprice_ttc;
			$prodcustprice->price_min       = $unitprice_ht;
			$prodcustprice->price_min_ttc   = $unitprice_ttc;
			$prodcustprice->price_base_type = 'HT';
			$prodcustprice->tva_tx          = $tva_tx;
			$prodcustprice->recuperableonly = 0;
			$prodcustprice->price_label     = $price_label;

			$res = $prodcustprice->create($user, 0, $this);

			if ($res < 0) {
				dol_syslog("AutoPriceCustomer: Erreur create prix produit ".$line->fk_product." / client ".$order->socid.": ".implode(', ', $prodcustprice->errors), LOG_ERR);
				return -1;
			}

			dol_syslog("AutoPriceCustomer: Prix CREE - produit ".$line->fk_product." / client ".$order->socid." = ".$unitprice_ht." HT (ref: ".$price_label.")", LOG_INFO);
		}

		return 1;
	}
}

Fichier autopricecustomer.lang

Module610200Name = AutoPriceCustomer
Module610200Desc = Enregistre automatiquement le prix par client à la validation d'une commande
AutoPriceCustomerDescription = Lors de la validation d'une commande client, ce module enregistre le prix unitaire de chaque ligne produit dans les prix par client. Le numéro de commande est utilisé comme libellé du prix.

J’ai testé ça sous Dolibarr 20.0.0 en php 8.2 et ça fonctionne très bien.