From ef2619eabaa53cb19bb49f41de1b69f9d00574c1 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:07:29 +0100 Subject: [PATCH 01/53] =?UTF-8?q?feat:=20proposer=20la=20mise=20=C3=A0=20j?= =?UTF-8?q?our=20des=20prix=20fournisseur=20=C3=A0=20l'envoi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/setup.php | 1 + core/class/actions_dynamicsprices.class.php | 345 ++++++++++++++++++++ core/modules/modDynamicsPrices.class.php | 13 +- langs/de_DE/dynamicsprices.lang | 8 + langs/en_US/dynamicsprices.lang | 8 + langs/es_ES/dynamicsprices.lang | 8 + langs/fr_FR/dynamicsprices.lang | 100 +++--- langs/it_IT/dynamicsprices.lang | 8 + 8 files changed, 436 insertions(+), 55 deletions(-) create mode 100644 core/class/actions_dynamicsprices.class.php diff --git a/admin/setup.php b/admin/setup.php index 953a661..5520bc0 100644 --- a/admin/setup.php +++ b/admin/setup.php @@ -235,6 +235,7 @@ function ($matches) use ($form, $options) { setup_print_title($langs->trans("LMDB_UpdateOptions")); setup_print_on_off('LMDB_COST_PRICE_ONLY'); setup_print_on_off('LMDB_SUPPLIER_BUYPRICE_ALTERED'); +setup_print_on_off('LMDB_ADD_UPDATE_SUPPLIER_PRICE_ON_SUBMIT'); setup_print_on_off('LMDB_KIT_PRICE_FROM_COMPONENTS'); print ''; diff --git a/core/class/actions_dynamicsprices.class.php b/core/class/actions_dynamicsprices.class.php new file mode 100644 index 0000000..113ebce --- /dev/null +++ b/core/class/actions_dynamicsprices.class.php @@ -0,0 +1,345 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +require_once DOL_DOCUMENT_ROOT.'/core/class/commonhookactions.class.php'; +require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.commande.class.php'; + +/** + * Hooks for DynamicsPrices module. + */ +class ActionsDynamicsPrices extends CommonHookActions +{ + /** @var DoliDB */ + public $db; + + /** + * Constructor. + * + * @param DoliDB $db Database handler + */ + public function __construct($db) + { + $this->db = $db; + } + + /** + * Execute hook actions. + * + * @param array $parameters Hook parameters + * @param CommonObject $object Current object + * @param string $action Current action + * @param HookManager $hookmanager Hook manager + * @return int + */ + public function doActions($parameters, &$object, &$action, $hookmanager) + { + if (empty($parameters['context']) || strpos($parameters['context'], 'ordersuppliercard') === false) { + return 0; + } + + if (!getDolGlobalInt('LMDB_ADD_UPDATE_SUPPLIER_PRICE_ON_SUBMIT')) { + return 0; + } + + if ($action !== 'confirm_commande') { + return 0; + } + + $selectedRows = GETPOST('dynamicsprices_apply_line', 'array'); + if (!is_array($selectedRows)) { + $selectedRows = array(); + } + + $differences = $this->getOrderSupplierPriceDifferences($object); + if (empty($differences)) { + return 0; + } + + $updatedLines = 0; + foreach ($differences as $lineId => $diff) { + if (empty($selectedRows[$lineId])) { + continue; + } + + $res = $this->upsertSupplierPriceFromDiff($diff); + if ($res < 0) { + setEventMessages($this->error, $this->errors, 'errors'); + return -1; + } + + $updatedLines++; + } + + if ($updatedLines > 0) { + global $langs; + $langs->load('dynamicsprices@dynamicsprices'); + setEventMessages($langs->trans('LMDB_SupplierPriceUpdatedCount', $updatedLines), null, 'mesgs'); + } + + return 0; + } + + /** + * Build a confirmation modal with supplier prices to add/update. + * + * @param array $parameters Hook parameters + * @param CommonObject $object Current object + * @param string $action Current action + * @param HookManager $hookmanager Hook manager + * @return int + */ + public function formConfirm($parameters, &$object, &$action, $hookmanager) + { + global $langs; + + if (empty($parameters['context']) || strpos($parameters['context'], 'ordersuppliercard') === false) { + return 0; + } + + if (!getDolGlobalInt('LMDB_ADD_UPDATE_SUPPLIER_PRICE_ON_SUBMIT')) { + return 0; + } + + if ($action !== 'commande') { + return 0; + } + + $differences = $this->getOrderSupplierPriceDifferences($object); + if (empty($differences)) { + return 0; + } + + $langs->load('dynamicsprices@dynamicsprices'); + $url = $_SERVER['PHP_SELF'].'?id='.(int) $object->id; + $url .= '&datecommande='.(int) GETPOST('datecommande', 'int'); + $url .= '&methode='.urlencode(GETPOST('methodecommande', 'alpha')); + $url .= '&comment='.urlencode(GETPOST('comment', 'alphanohtml')); + + $html = '
'; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + + foreach ($differences as $lineId => $diff) { + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + $html .= '
'.$langs->trans('LMDB_AddOrUpdate').''.$langs->trans('Ref').''.$langs->trans('Description').''.$langs->trans('QtyMin').''.$langs->trans('LMDB_QuantityPackaging').''.$langs->trans('VATRate').''.$langs->trans('LMDB_MinQtyPriceHT').''.$langs->trans('Discount').''.$langs->trans('DeliveryDelay').''.$langs->trans('LMDB_SupplierReputation').'
'.dol_escape_htmltag($diff['ref']).''.dol_escape_htmltag($diff['label']).''.price($diff['qty']).''.price($diff['unitquantity']).''.price($diff['vat']).''.price($diff['unitprice']).''.price($diff['discount']).''.((int) $diff['delivery_time_days']).''.price($diff['supplier_reputation']).'
'; + $html .= '
'; + + require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php'; + $form = new Form($this->db); + $formquestion = array( + array('type' => 'other', 'name' => 'dynamicsprices_diff_table', 'label' => '', 'value' => $html), + ); + + $this->resPrint = $form->formconfirm($url, $langs->trans('LMDB_SupplierPriceModalTitle'), $langs->trans('LMDB_SupplierPriceModalDescription'), 'confirm_commande', $formquestion, 1, 1, 600, '90%'); + return 1; + } + + /** + * Get supplier price differences between order lines and current supplier prices. + * + * @param CommandeFournisseur $object Supplier order + * @return array> + */ + private function getOrderSupplierPriceDifferences($object) + { + $differences = array(); + if (empty($object->id) || empty($object->socid)) { + return $differences; + } + + if (empty($object->lines) || !is_array($object->lines)) { + $object->fetch_lines(); + } + + foreach ($object->lines as $line) { + if (empty($line->fk_product)) { + continue; + } + + $qty = price2num((float) $line->qty, 'MS'); + $unitquantity = price2num((float) (empty($line->unitquantity) ? $line->qty : $line->unitquantity), 'MS'); + $vat = price2num((float) $line->tva_tx, 'MS'); + $discount = price2num((float) $line->remise_percent, 'MS'); + $unitprice = $this->getLineUnitPrice($line); + $delivery = isset($line->fk_availability) ? (int) $line->fk_availability : 0; + $reputation = isset($line->supplier_reputation) ? price2num((float) $line->supplier_reputation, 'MS') : 0; + + $current = $this->getCurrentSupplierPrice((int) $line->fk_product, (int) $object->socid, $qty); + $isDifferent = empty($current) + || price2num((float) $current['unitprice'], 'MS') !== $unitprice + || price2num((float) $current['tva_tx'], 'MS') !== $vat + || price2num((float) $current['remise_percent'], 'MS') !== $discount + || (int) $current['fk_availability'] !== $delivery + || price2num((float) $current['supplier_reputation'], 'MS') !== $reputation; + + if (!$isDifferent) { + continue; + } + + $differences[(int) $line->id] = array( + 'lineid' => (int) $line->id, + 'fk_product' => (int) $line->fk_product, + 'fk_soc' => (int) $object->socid, + 'qty' => $qty, + 'unitquantity' => $unitquantity, + 'vat' => $vat, + 'unitprice' => $unitprice, + 'discount' => $discount, + 'delivery_time_days' => $delivery, + 'supplier_reputation' => $reputation, + 'current_rowid' => !empty($current['rowid']) ? (int) $current['rowid'] : 0, + 'ref' => isset($line->ref) ? $line->ref : '', + 'label' => isset($line->product_label) ? $line->product_label : (isset($line->desc) ? $line->desc : ''), + ); + } + + return $differences; + } + + /** + * Return current supplier price line for a product/supplier/minimum quantity. + * + * @param int $fkProduct Product id + * @param int $fkSoc Supplier thirdparty id + * @param float $qty Minimum quantity + * @return array + */ + private function getCurrentSupplierPrice($fkProduct, $fkSoc, $qty) + { + global $conf; + $sql = 'SELECT rowid, unitprice, tva_tx, remise_percent, fk_availability, supplier_reputation'; + $sql .= ' FROM '.MAIN_DB_PREFIX.'product_fournisseur_price'; + $sql .= ' WHERE fk_product = '.((int) $fkProduct); + $sql .= ' AND fk_soc = '.((int) $fkSoc); + $sql .= ' AND quantity = '.price2num((float) $qty, 'MS'); + $sql .= ' AND entity = '.((int) $conf->entity); + $sql .= ' ORDER BY rowid DESC'; + + $resql = $this->db->query($sql); + if (!$resql || !$this->db->num_rows($resql)) { + return array(); + } + + $obj = $this->db->fetch_object($resql); + if (!$obj) { + return array(); + } + + return array( + 'rowid' => (int) $obj->rowid, + 'unitprice' => (float) $obj->unitprice, + 'tva_tx' => (float) $obj->tva_tx, + 'remise_percent' => (float) $obj->remise_percent, + 'fk_availability' => (int) $obj->fk_availability, + 'supplier_reputation' => (float) $obj->supplier_reputation, + ); + } + + /** + * Get unit purchase price from supplier order line. + * + * @param CommonObjectLine $line Supplier order line + * @return float + */ + private function getLineUnitPrice($line) + { + if (isset($line->subprice)) { + return price2num((float) $line->subprice, 'MS'); + } + + if (isset($line->pu_ht)) { + return price2num((float) $line->pu_ht, 'MS'); + } + + return 0.0; + } + + /** + * Insert or update supplier price line from a difference. + * + * @param array $diff Difference payload + * @return int + */ + private function upsertSupplierPriceFromDiff(array $diff) + { + global $conf, $user; + + if (!empty($diff['current_rowid'])) { + $sql = 'UPDATE '.MAIN_DB_PREFIX.'product_fournisseur_price'; + $sql .= ' SET unitprice = '.price2num((float) $diff['unitprice'], 'MS'); + $sql .= ', price = '.price2num((float) $diff['unitprice'], 'MS'); + $sql .= ', tva_tx = '.price2num((float) $diff['vat'], 'MS'); + $sql .= ', remise_percent = '.price2num((float) $diff['discount'], 'MS'); + $sql .= ', fk_availability = '.((int) $diff['delivery_time_days']); + $sql .= ', supplier_reputation = '.price2num((float) $diff['supplier_reputation'], 'MS'); + $sql .= ', fk_user = '.((int) $user->id); + $sql .= ', tms = '.$this->db->idate(dol_now()); + $sql .= ' WHERE rowid = '.((int) $diff['current_rowid']); + $sql .= ' AND entity = '.((int) $conf->entity); + } else { + $sql = 'INSERT INTO '.MAIN_DB_PREFIX.'product_fournisseur_price('; + $sql .= 'entity, fk_product, fk_soc, quantity, unitquantity, unitprice, price, tva_tx, remise_percent, fk_availability, supplier_reputation, fk_user, datec'; + $sql .= ') VALUES ('; + $sql .= ((int) $conf->entity).', '; + $sql .= ((int) $diff['fk_product']).', '; + $sql .= ((int) $diff['fk_soc']).', '; + $sql .= price2num((float) $diff['qty'], 'MS').', '; + $sql .= price2num((float) $diff['unitquantity'], 'MS').', '; + $sql .= price2num((float) $diff['unitprice'], 'MS').', '; + $sql .= price2num((float) $diff['unitprice'], 'MS').', '; + $sql .= price2num((float) $diff['vat'], 'MS').', '; + $sql .= price2num((float) $diff['discount'], 'MS').', '; + $sql .= ((int) $diff['delivery_time_days']).', '; + $sql .= price2num((float) $diff['supplier_reputation'], 'MS').', '; + $sql .= ((int) $user->id).', '; + $sql .= $this->db->idate(dol_now()); + $sql .= ')'; + } + + $resql = $this->db->query($sql); + if (!$resql) { + $this->error = $this->db->lasterror(); + $this->errors[] = $this->error; + return -1; + } + + return 1; + } +} diff --git a/core/modules/modDynamicsPrices.class.php b/core/modules/modDynamicsPrices.class.php index 168758c..6ecb81a 100644 --- a/core/modules/modDynamicsPrices.class.php +++ b/core/modules/modDynamicsPrices.class.php @@ -119,13 +119,12 @@ public function __construct($db) ), // Set here all hooks context managed by module. To find available hook context, make a "grep -r '>initHooks(' *" on source code. You can also set hook context to 'all' /* BEGIN MODULEBUILDER HOOKSCONTEXTS */ - 'hooks' => array( - // 'data' => array( - // 'hookcontext1', - // 'hookcontext2', - // ), - // 'entity' => '0', - ), + 'hooks' => array( + 'data' => array( + 'ordersuppliercard', + ), + 'entity' => '0', + ), /* END MODULEBUILDER HOOKSCONTEXTS */ // Set this to 1 if features of module are opened to external users 'moduleforexternal' => 0, diff --git a/langs/de_DE/dynamicsprices.lang b/langs/de_DE/dynamicsprices.lang index 11cc50f..08b45db 100644 --- a/langs/de_DE/dynamicsprices.lang +++ b/langs/de_DE/dynamicsprices.lang @@ -56,3 +56,11 @@ LMDB_commercialcategories = Kommerzielle Kategorien LMDB_CommercialCategoryExtrafield = Kommerzielle Kategorie LMDB_LabelTooltipHelp = Bezeichnung des Wörterbucheintrags LMDB_ActiveTooltipHelp = Aktivstatus des Wörterbucheintrags +LMDB_ADD_UPDATE_SUPPLIER_PRICE_ON_SUBMIT = Beim Absenden einer Lieferantenbestellung das Hinzufügen/Aktualisieren von Lieferanten-Einkaufspreisen aus Bestellzeilen vorschlagen. +LMDB_SupplierPriceModalTitle = Aktualisierung der Lieferanten-Einkaufspreise +LMDB_SupplierPriceModalDescription = Unterschiede bei Einkaufspreisen wurden erkannt. Wählen Sie die zu speichernden Zeilen aus. +LMDB_AddOrUpdate = Hinzufügen/Aktualisieren +LMDB_QuantityPackaging = Mengenverpackung +LMDB_MinQtyPriceHT = Mindestmengenpreis (o. MwSt.) +LMDB_SupplierReputation = Reputation +LMDB_SupplierPriceUpdatedCount = %s Lieferanten-Einkaufspreise hinzugefügt/aktualisiert. diff --git a/langs/en_US/dynamicsprices.lang b/langs/en_US/dynamicsprices.lang index f707e6a..93c0c75 100644 --- a/langs/en_US/dynamicsprices.lang +++ b/langs/en_US/dynamicsprices.lang @@ -56,3 +56,11 @@ LMDB_commercialcategories = Commercial categories LMDB_CommercialCategoryExtrafield = Commercial category LMDB_LabelTooltipHelp = Dictionary entry label LMDB_ActiveTooltipHelp = Dictionary entry active status +LMDB_ADD_UPDATE_SUPPLIER_PRICE_ON_SUBMIT = On supplier order submission, propose adding/updating supplier buy prices from order lines. +LMDB_SupplierPriceModalTitle = Supplier purchase price update +LMDB_SupplierPriceModalDescription = Purchase price differences were detected. Select lines to save. +LMDB_AddOrUpdate = Add/Update +LMDB_QuantityPackaging = Quantity packaging +LMDB_MinQtyPriceHT = Min quantity price (excl. tax) +LMDB_SupplierReputation = Reputation +LMDB_SupplierPriceUpdatedCount = %s supplier purchase prices added/updated. diff --git a/langs/es_ES/dynamicsprices.lang b/langs/es_ES/dynamicsprices.lang index 94572c1..0fdccf7 100644 --- a/langs/es_ES/dynamicsprices.lang +++ b/langs/es_ES/dynamicsprices.lang @@ -56,3 +56,11 @@ LMDB_commercialcategories = Categorías comerciales LMDB_CommercialCategoryExtrafield = Categoría comercial LMDB_LabelTooltipHelp = Etiqueta del registro del diccionario LMDB_ActiveTooltipHelp = Estado activo del registro del diccionario +LMDB_ADD_UPDATE_SUPPLIER_PRICE_ON_SUBMIT = Al enviar un pedido a proveedor, proponer añadir/actualizar precios de compra del proveedor desde las líneas del pedido. +LMDB_SupplierPriceModalTitle = Actualización de precios de compra del proveedor +LMDB_SupplierPriceModalDescription = Se detectaron diferencias de precios de compra. Seleccione las líneas a guardar. +LMDB_AddOrUpdate = Añadir/Actualizar +LMDB_QuantityPackaging = Envasado de cantidades +LMDB_MinQtyPriceHT = Precio cantidad mínima (sin IVA) +LMDB_SupplierReputation = Reputación +LMDB_SupplierPriceUpdatedCount = %s precios de compra de proveedor añadidos/actualizados. diff --git a/langs/fr_FR/dynamicsprices.lang b/langs/fr_FR/dynamicsprices.lang index 17e494c..ecce593 100644 --- a/langs/fr_FR/dynamicsprices.lang +++ b/langs/fr_FR/dynamicsprices.lang @@ -1,52 +1,52 @@ -# Translation file - -# -# Generic -# - -# Module label 'ModuleDynamicsPricesName' -ModuleDynamicsPricesName = Prix de vente dynamiques -# Module description 'ModuleDynamicsPricesDesc' -ModuleDynamicsPricesDesc = Ce module permet de mettre à jour les prix de vente en fonction du prix d'achat moyen unitaire chez les fournisseurs et des coefficients de prix définis dans un dictionnaire dédié. - -# -# Admin page -# -DynamicsPricesSetup = Réglages des prix de vente dynamiques -DynamicsPricesSetupPage = Page de réglage du module de Prix de vente dynamiques -LMDB_UpdateOptions=Options de mise à jour des prix de vente -LMDB_KIT_PRICE_FROM_COMPONENTS = Calculer les prix des Kits à partir de vente des composants plutot que le prix de revient - -LMDB_COST_PRICE_ONLY = Ne mettre à jour les prix de vente que sur la base des prix de revient. -LMDB_SUPPLIER_BUYPRICE_ALTERED = Actualisation de prix de vente à la création/mise à jour/suppression d'un prix d'achat ou l'actualisation du prix de revient si le calcul sur le prix de revient est activé. - - - -# -# Autres -# -DynamicsPricesArea = Home DynamicsPrices -LMDB_ErrorUpdate = Erreur lors de la mise à jour : -LMDB_NbLinesUpdated = Nombre de prix de vente mis à jour : +# Translation file + +# +# Generic +# + +# Module label 'ModuleDynamicsPricesName' +ModuleDynamicsPricesName = Prix de vente dynamiques +# Module description 'ModuleDynamicsPricesDesc' +ModuleDynamicsPricesDesc = Ce module permet de mettre à jour les prix de vente en fonction du prix d'achat moyen unitaire chez les fournisseurs et des coefficients de prix définis dans un dictionnaire dédié. + +# +# Admin page +# +DynamicsPricesSetup = Réglages des prix de vente dynamiques +DynamicsPricesSetupPage = Page de réglage du module de Prix de vente dynamiques +LMDB_UpdateOptions=Options de mise à jour des prix de vente +LMDB_KIT_PRICE_FROM_COMPONENTS = Calculer les prix des Kits à partir de vente des composants plutot que le prix de revient + +LMDB_COST_PRICE_ONLY = Ne mettre à jour les prix de vente que sur la base des prix de revient. +LMDB_SUPPLIER_BUYPRICE_ALTERED = Actualisation de prix de vente à la création/mise à jour/suppression d'un prix d'achat ou l'actualisation du prix de revient si le calcul sur le prix de revient est activé. + + + +# +# Autres +# +DynamicsPricesArea = Home DynamicsPrices +LMDB_ErrorUpdate = Erreur lors de la mise à jour : +LMDB_NbLinesUpdated = Nombre de prix de vente mis à jour : Fk_nature = Catégorie commerciale Fk_commercial_category = Catégorie commerciale Code_commercial_category = Catégorie commerciale -Margin_on_cost_percent = Taux de marge sur le prix de revient +Margin_on_cost_percent = Taux de marge sur le prix de revient Code_nature = Catégorie commerciale -Pricelevel = ID du Niveau de prix -Targetrate = Taux de marge cible (en %) -Minrate = Taux de marge minimum (en %) -Entity = Entité - -LMDB_CommentAutoUpdateSellPrice = Les prix de vente sont mis à jour en fonction des coefficients de prix donnés dans le dictionnaire, et de la moyenne des prix unitaires d'achats fournisseurs. -LMDB_LabelAutoUpdateSellPrice = Mise à jour automatique des prix de vente -LMDB_coefprice = Taux de marges sur les prix de vente -LMDB_marginoncost = Taux de marge sur les prix de revient -DynamicsPrices = Prix de vente dynamiques +Pricelevel = ID du Niveau de prix +Targetrate = Taux de marge cible (en %) +Minrate = Taux de marge minimum (en %) +Entity = Entité + +LMDB_CommentAutoUpdateSellPrice = Les prix de vente sont mis à jour en fonction des coefficients de prix donnés dans le dictionnaire, et de la moyenne des prix unitaires d'achats fournisseurs. +LMDB_LabelAutoUpdateSellPrice = Mise à jour automatique des prix de vente +LMDB_coefprice = Taux de marges sur les prix de vente +LMDB_marginoncost = Taux de marge sur les prix de revient +DynamicsPrices = Prix de vente dynamiques LMDB_CodeNatureTooltipHelp = Catégorie commerciale liée -LMDB_MarginOnCostTooltipHelp = Taux de marge appliqué sur le prix de revient moyen d'achat -LMDB_CodeTooltipHelp = Code de l'entrée du dictionnaire -LMDB_ENtityTooltipHelp = Identifiant de l'entité +LMDB_MarginOnCostTooltipHelp = Taux de marge appliqué sur le prix de revient moyen d'achat +LMDB_CodeTooltipHelp = Code de l'entrée du dictionnaire +LMDB_ENtityTooltipHelp = Identifiant de l'entité LMDB_FkNatureTooltipHelp = Identifiant de la catégorie commerciale LMDB_FkCommercialCategoryTooltipHelp = Identifiant de la catégorie commerciale LMDB_CodeCommercialCategoryTooltipHelp = Code de la catégorie commerciale @@ -58,7 +58,11 @@ LMDB_commercialcategories = Catégories commerciales LMDB_CommercialCategoryExtrafield = Catégorie commerciale LMDB_LabelTooltipHelp = Libellé de l'entrée du dictionnaire LMDB_ActiveTooltipHelp = Statut actif de l'entrée du dictionnaire - - - - +LMDB_ADD_UPDATE_SUPPLIER_PRICE_ON_SUBMIT = Lors de l'envoi d'une commande fournisseur, proposer l'ajout/la mise à jour des prix d'achat fournisseur depuis les lignes de commande. +LMDB_SupplierPriceModalTitle = Mise à jour des prix d'achat fournisseur +LMDB_SupplierPriceModalDescription = Des différences de prix d'achat ont été détectées. Cochez les lignes à enregistrer. +LMDB_AddOrUpdate = Ajouter/Mettre à jour +LMDB_QuantityPackaging = Conditionnement des quantités +LMDB_MinQtyPriceHT = Prix quantité min. (HT) +LMDB_SupplierReputation = Réputation +LMDB_SupplierPriceUpdatedCount = %s prix d'achat fournisseur ajoutés/mis à jour. diff --git a/langs/it_IT/dynamicsprices.lang b/langs/it_IT/dynamicsprices.lang index 8d306d4..980a93b 100644 --- a/langs/it_IT/dynamicsprices.lang +++ b/langs/it_IT/dynamicsprices.lang @@ -56,3 +56,11 @@ LMDB_commercialcategories = Categorie commerciali LMDB_CommercialCategoryExtrafield = Categoria commerciale LMDB_LabelTooltipHelp = Etichetta della voce del dizionario LMDB_ActiveTooltipHelp = Stato attivo della voce del dizionario +LMDB_ADD_UPDATE_SUPPLIER_PRICE_ON_SUBMIT = All'invio di un ordine fornitore, proporre l'aggiunta/aggiornamento dei prezzi di acquisto fornitore dalle righe ordine. +LMDB_SupplierPriceModalTitle = Aggiornamento prezzi di acquisto fornitore +LMDB_SupplierPriceModalDescription = Sono state rilevate differenze nei prezzi di acquisto. Selezionare le righe da salvare. +LMDB_AddOrUpdate = Aggiungi/Aggiorna +LMDB_QuantityPackaging = Confezionamento quantità +LMDB_MinQtyPriceHT = Prezzo quantità minima (IVA esclusa) +LMDB_SupplierReputation = Reputazione +LMDB_SupplierPriceUpdatedCount = %s prezzi di acquisto fornitore aggiunti/aggiornati. From f6b15754b98c89c81c01909344a931af2030e404 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:17:25 +0100 Subject: [PATCH 02/53] chore: ajouter des logs de suivi sur la soumission fournisseur --- core/class/actions_dynamicsprices.class.php | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/core/class/actions_dynamicsprices.class.php b/core/class/actions_dynamicsprices.class.php index 113ebce..c7175fa 100644 --- a/core/class/actions_dynamicsprices.class.php +++ b/core/class/actions_dynamicsprices.class.php @@ -47,15 +47,20 @@ public function __construct($db) */ public function doActions($parameters, &$object, &$action, $hookmanager) { + dol_syslog(__METHOD__.' - Start doActions with action='.$action, LOG_DEBUG); + if (empty($parameters['context']) || strpos($parameters['context'], 'ordersuppliercard') === false) { + dol_syslog(__METHOD__.' - Skip: unsupported context', LOG_DEBUG); return 0; } if (!getDolGlobalInt('LMDB_ADD_UPDATE_SUPPLIER_PRICE_ON_SUBMIT')) { + dol_syslog(__METHOD__.' - Skip: option LMDB_ADD_UPDATE_SUPPLIER_PRICE_ON_SUBMIT disabled', LOG_DEBUG); return 0; } if ($action !== 'confirm_commande') { + dol_syslog(__METHOD__.' - Skip: action is not confirm_commande', LOG_DEBUG); return 0; } @@ -66,17 +71,22 @@ public function doActions($parameters, &$object, &$action, $hookmanager) $differences = $this->getOrderSupplierPriceDifferences($object); if (empty($differences)) { + dol_syslog(__METHOD__.' - No supplier price difference found, nothing to update', LOG_DEBUG); return 0; } + dol_syslog(__METHOD__.' - Found '.count($differences).' differing line(s)', LOG_DEBUG); $updatedLines = 0; foreach ($differences as $lineId => $diff) { if (empty($selectedRows[$lineId])) { + dol_syslog(__METHOD__.' - Skip line '.$lineId.' (unchecked)', LOG_DEBUG); continue; } + dol_syslog(__METHOD__.' - Upsert supplier price for line '.$lineId.' (product '.$diff['fk_product'].')', LOG_DEBUG); $res = $this->upsertSupplierPriceFromDiff($diff); if ($res < 0) { + dol_syslog(__METHOD__.' - Error while upserting supplier price for line '.$lineId.': '.$this->error, LOG_ERR); setEventMessages($this->error, $this->errors, 'errors'); return -1; } @@ -89,6 +99,7 @@ public function doActions($parameters, &$object, &$action, $hookmanager) $langs->load('dynamicsprices@dynamicsprices'); setEventMessages($langs->trans('LMDB_SupplierPriceUpdatedCount', $updatedLines), null, 'mesgs'); } + dol_syslog(__METHOD__.' - End doActions with '.$updatedLines.' line(s) updated', LOG_DEBUG); return 0; } @@ -105,23 +116,29 @@ public function doActions($parameters, &$object, &$action, $hookmanager) public function formConfirm($parameters, &$object, &$action, $hookmanager) { global $langs; + dol_syslog(__METHOD__.' - Start formConfirm with action='.$action, LOG_DEBUG); if (empty($parameters['context']) || strpos($parameters['context'], 'ordersuppliercard') === false) { + dol_syslog(__METHOD__.' - Skip: unsupported context', LOG_DEBUG); return 0; } if (!getDolGlobalInt('LMDB_ADD_UPDATE_SUPPLIER_PRICE_ON_SUBMIT')) { + dol_syslog(__METHOD__.' - Skip: option LMDB_ADD_UPDATE_SUPPLIER_PRICE_ON_SUBMIT disabled', LOG_DEBUG); return 0; } if ($action !== 'commande') { + dol_syslog(__METHOD__.' - Skip: action is not commande', LOG_DEBUG); return 0; } $differences = $this->getOrderSupplierPriceDifferences($object); if (empty($differences)) { + dol_syslog(__METHOD__.' - No supplier price difference found, native confirmation will be used', LOG_DEBUG); return 0; } + dol_syslog(__METHOD__.' - Prepare modal for '.count($differences).' differing line(s)', LOG_DEBUG); $langs->load('dynamicsprices@dynamicsprices'); $url = $_SERVER['PHP_SELF'].'?id='.(int) $object->id; @@ -168,6 +185,7 @@ public function formConfirm($parameters, &$object, &$action, $hookmanager) ); $this->resPrint = $form->formconfirm($url, $langs->trans('LMDB_SupplierPriceModalTitle'), $langs->trans('LMDB_SupplierPriceModalDescription'), 'confirm_commande', $formquestion, 1, 1, 600, '90%'); + dol_syslog(__METHOD__.' - Custom confirmation modal rendered', LOG_DEBUG); return 1; } @@ -181,6 +199,7 @@ private function getOrderSupplierPriceDifferences($object) { $differences = array(); if (empty($object->id) || empty($object->socid)) { + dol_syslog(__METHOD__.' - Skip comparison: missing order id or supplier id', LOG_DEBUG); return $differences; } @@ -190,6 +209,7 @@ private function getOrderSupplierPriceDifferences($object) foreach ($object->lines as $line) { if (empty($line->fk_product)) { + dol_syslog(__METHOD__.' - Skip line without product id', LOG_DEBUG); continue; } @@ -229,6 +249,7 @@ private function getOrderSupplierPriceDifferences($object) 'label' => isset($line->product_label) ? $line->product_label : (isset($line->desc) ? $line->desc : ''), ); } + dol_syslog(__METHOD__.' - Comparison completed with '.count($differences).' difference(s)', LOG_DEBUG); return $differences; } @@ -302,6 +323,7 @@ private function upsertSupplierPriceFromDiff(array $diff) global $conf, $user; if (!empty($diff['current_rowid'])) { + dol_syslog(__METHOD__.' - Update existing supplier price rowid='.$diff['current_rowid'], LOG_DEBUG); $sql = 'UPDATE '.MAIN_DB_PREFIX.'product_fournisseur_price'; $sql .= ' SET unitprice = '.price2num((float) $diff['unitprice'], 'MS'); $sql .= ', price = '.price2num((float) $diff['unitprice'], 'MS'); @@ -314,6 +336,7 @@ private function upsertSupplierPriceFromDiff(array $diff) $sql .= ' WHERE rowid = '.((int) $diff['current_rowid']); $sql .= ' AND entity = '.((int) $conf->entity); } else { + dol_syslog(__METHOD__.' - Insert new supplier price for product='.$diff['fk_product'].' supplier='.$diff['fk_soc'], LOG_DEBUG); $sql = 'INSERT INTO '.MAIN_DB_PREFIX.'product_fournisseur_price('; $sql .= 'entity, fk_product, fk_soc, quantity, unitquantity, unitprice, price, tva_tx, remise_percent, fk_availability, supplier_reputation, fk_user, datec'; $sql .= ') VALUES ('; @@ -337,8 +360,10 @@ private function upsertSupplierPriceFromDiff(array $diff) if (!$resql) { $this->error = $this->db->lasterror(); $this->errors[] = $this->error; + dol_syslog(__METHOD__.' - SQL error: '.$this->error, LOG_ERR); return -1; } + dol_syslog(__METHOD__.' - SQL upsert successful', LOG_DEBUG); return 1; } From a719400ec02702c91d524424fb29fc68fb9c4210 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:43:59 +0100 Subject: [PATCH 03/53] =?UTF-8?q?fix:=20d=C3=A9placer=20la=20classe=20hook?= =?UTF-8?q?=20dans=20/class=20pour=20chargement=20Dolibarr?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- {core/class => class}/actions_dynamicsprices.class.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {core/class => class}/actions_dynamicsprices.class.php (100%) diff --git a/core/class/actions_dynamicsprices.class.php b/class/actions_dynamicsprices.class.php similarity index 100% rename from core/class/actions_dynamicsprices.class.php rename to class/actions_dynamicsprices.class.php From 2afb91deaebfe86bbbba42bb9cf85cc1db7ef25b Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:48:09 +0100 Subject: [PATCH 04/53] =?UTF-8?q?fix:=20d=C3=A9clarer=20la=20propri=C3=A9t?= =?UTF-8?q?=C3=A9=20resPrint=20pour=20PHP=208.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- class/actions_dynamicsprices.class.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/class/actions_dynamicsprices.class.php b/class/actions_dynamicsprices.class.php index c7175fa..040ba76 100644 --- a/class/actions_dynamicsprices.class.php +++ b/class/actions_dynamicsprices.class.php @@ -26,6 +26,9 @@ class ActionsDynamicsPrices extends CommonHookActions /** @var DoliDB */ public $db; + /** @var string */ + public $resPrint = ''; + /** * Constructor. * From 5304edb69d197c865777c28d27a904eee3801796 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:57:21 +0100 Subject: [PATCH 05/53] fix: utiliser resprints pour afficher le modal de confirmation --- class/actions_dynamicsprices.class.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/class/actions_dynamicsprices.class.php b/class/actions_dynamicsprices.class.php index 040ba76..664ad79 100644 --- a/class/actions_dynamicsprices.class.php +++ b/class/actions_dynamicsprices.class.php @@ -26,9 +26,6 @@ class ActionsDynamicsPrices extends CommonHookActions /** @var DoliDB */ public $db; - /** @var string */ - public $resPrint = ''; - /** * Constructor. * @@ -187,7 +184,7 @@ public function formConfirm($parameters, &$object, &$action, $hookmanager) array('type' => 'other', 'name' => 'dynamicsprices_diff_table', 'label' => '', 'value' => $html), ); - $this->resPrint = $form->formconfirm($url, $langs->trans('LMDB_SupplierPriceModalTitle'), $langs->trans('LMDB_SupplierPriceModalDescription'), 'confirm_commande', $formquestion, 1, 1, 600, '90%'); + $this->resprints = $form->formconfirm($url, $langs->trans('LMDB_SupplierPriceModalTitle'), $langs->trans('LMDB_SupplierPriceModalDescription'), 'confirm_commande', $formquestion, 1, 1, 600, '90%'); dol_syslog(__METHOD__.' - Custom confirmation modal rendered', LOG_DEBUG); return 1; } From 0ab2bc5406222eb7d5401e8e66bf8b0befcca3cf Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:01:22 +0100 Subject: [PATCH 06/53] uiux: rendre les colonnes du modal modifiables avant validation --- class/actions_dynamicsprices.class.php | 60 +++++++++++++++++++++----- 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/class/actions_dynamicsprices.class.php b/class/actions_dynamicsprices.class.php index 664ad79..eb0290f 100644 --- a/class/actions_dynamicsprices.class.php +++ b/class/actions_dynamicsprices.class.php @@ -68,6 +68,10 @@ public function doActions($parameters, &$object, &$action, $hookmanager) if (!is_array($selectedRows)) { $selectedRows = array(); } + $postedRowsData = GETPOST('dynamicsprices_data', 'array'); + if (!is_array($postedRowsData)) { + $postedRowsData = array(); + } $differences = $this->getOrderSupplierPriceDifferences($object); if (empty($differences)) { @@ -83,8 +87,9 @@ public function doActions($parameters, &$object, &$action, $hookmanager) continue; } - dol_syslog(__METHOD__.' - Upsert supplier price for line '.$lineId.' (product '.$diff['fk_product'].')', LOG_DEBUG); - $res = $this->upsertSupplierPriceFromDiff($diff); + $preparedDiff = $this->applyPostedValuesToDiff($lineId, $diff, $postedRowsData); + dol_syslog(__METHOD__.' - Upsert supplier price for line '.$lineId.' (product '.$preparedDiff['fk_product'].')', LOG_DEBUG); + $res = $this->upsertSupplierPriceFromDiff($preparedDiff); if ($res < 0) { dol_syslog(__METHOD__.' - Error while upserting supplier price for line '.$lineId.': '.$this->error, LOG_ERR); setEventMessages($this->error, $this->errors, 'errors'); @@ -164,15 +169,18 @@ public function formConfirm($parameters, &$object, &$action, $hookmanager) foreach ($differences as $lineId => $diff) { $html .= ''; $html .= ''; - $html .= ''.dol_escape_htmltag($diff['ref']).''; - $html .= ''.dol_escape_htmltag($diff['label']).''; - $html .= ''.price($diff['qty']).''; - $html .= ''.price($diff['unitquantity']).''; - $html .= ''.price($diff['vat']).''; - $html .= ''.price($diff['unitprice']).''; - $html .= ''.price($diff['discount']).''; - $html .= ''.((int) $diff['delivery_time_days']).''; - $html .= ''.price($diff['supplier_reputation']).''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; $html .= ''; } $html .= ''; @@ -367,4 +375,34 @@ private function upsertSupplierPriceFromDiff(array $diff) return 1; } + + /** + * Merge posted form values into difference payload. + * + * @param int $lineId Line id + * @param array $diff Computed difference + * @param array $postedRowsData Submitted rows data + * @return array + */ + private function applyPostedValuesToDiff($lineId, array $diff, array $postedRowsData) + { + if (empty($postedRowsData[$lineId]) || !is_array($postedRowsData[$lineId])) { + return $diff; + } + + $rowData = $postedRowsData[$lineId]; + + $diff['qty'] = isset($rowData['qty']) ? price2num($rowData['qty'], 'MS') : $diff['qty']; + $diff['unitquantity'] = isset($rowData['unitquantity']) ? price2num($rowData['unitquantity'], 'MS') : $diff['unitquantity']; + $diff['vat'] = isset($rowData['vat']) ? price2num($rowData['vat'], 'MS') : $diff['vat']; + $diff['unitprice'] = isset($rowData['unitprice']) ? price2num($rowData['unitprice'], 'MS') : $diff['unitprice']; + $diff['discount'] = isset($rowData['discount']) ? price2num($rowData['discount'], 'MS') : $diff['discount']; + $diff['delivery_time_days'] = isset($rowData['delivery_time_days']) ? (int) $rowData['delivery_time_days'] : $diff['delivery_time_days']; + $diff['supplier_reputation'] = isset($rowData['supplier_reputation']) ? price2num($rowData['supplier_reputation'], 'MS') : $diff['supplier_reputation']; + $diff['fk_product'] = isset($rowData['fk_product']) ? (int) $rowData['fk_product'] : $diff['fk_product']; + $diff['fk_soc'] = isset($rowData['fk_soc']) ? (int) $rowData['fk_soc'] : $diff['fk_soc']; + $diff['current_rowid'] = isset($rowData['current_rowid']) ? (int) $rowData['current_rowid'] : $diff['current_rowid']; + + return $diff; + } } From 94b76c1d60d1fd3989b3525907dde43d610fd393 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:08:07 +0100 Subject: [PATCH 07/53] fix: ajouter Valider/Ignorer et poursuivre la soumission sur fermeture --- class/actions_dynamicsprices.class.php | 27 +++++++++++++++++++++++++- langs/de_DE/dynamicsprices.lang | 1 + langs/en_US/dynamicsprices.lang | 1 + langs/es_ES/dynamicsprices.lang | 1 + langs/fr_FR/dynamicsprices.lang | 1 + langs/it_IT/dynamicsprices.lang | 1 + 6 files changed, 31 insertions(+), 1 deletion(-) diff --git a/class/actions_dynamicsprices.class.php b/class/actions_dynamicsprices.class.php index eb0290f..da0ce1a 100644 --- a/class/actions_dynamicsprices.class.php +++ b/class/actions_dynamicsprices.class.php @@ -59,11 +59,27 @@ public function doActions($parameters, &$object, &$action, $hookmanager) return 0; } + if ($action === 'commande' && GETPOST('confirm', 'alpha') === 'no') { + dol_syslog(__METHOD__.' - Convert cancel flow to confirm_commande with skip update', LOG_DEBUG); + $_POST['action'] = 'confirm_commande'; + $_REQUEST['action'] = 'confirm_commande'; + $_POST['confirm'] = 'yes'; + $_REQUEST['confirm'] = 'yes'; + $_POST['dynamicsprices_skip_update'] = '1'; + $_REQUEST['dynamicsprices_skip_update'] = '1'; + $action = 'confirm_commande'; + } + if ($action !== 'confirm_commande') { dol_syslog(__METHOD__.' - Skip: action is not confirm_commande', LOG_DEBUG); return 0; } + if (GETPOST('dynamicsprices_skip_update', 'alpha') === '1') { + dol_syslog(__METHOD__.' - Skip update requested, continue supplier order submission', LOG_DEBUG); + return 0; + } + $selectedRows = GETPOST('dynamicsprices_apply_line', 'array'); if (!is_array($selectedRows)) { $selectedRows = array(); @@ -192,7 +208,16 @@ public function formConfirm($parameters, &$object, &$action, $hookmanager) array('type' => 'other', 'name' => 'dynamicsprices_diff_table', 'label' => '', 'value' => $html), ); - $this->resprints = $form->formconfirm($url, $langs->trans('LMDB_SupplierPriceModalTitle'), $langs->trans('LMDB_SupplierPriceModalDescription'), 'confirm_commande', $formquestion, 1, 1, 600, '90%'); + $ignoreUrl = $url.'&action=confirm_commande&confirm=yes&dynamicsprices_skip_update=1'; + $this->resprints = $form->formconfirm($url, $langs->trans('LMDB_SupplierPriceModalTitle'), $langs->trans('LMDB_SupplierPriceModalDescription'), 'confirm_commande', $formquestion, 1, 1, 600, '90%', '', $langs->trans('Validate'), $langs->trans('LMDB_Ignore')); + $this->resprints .= ''; dol_syslog(__METHOD__.' - Custom confirmation modal rendered', LOG_DEBUG); return 1; } diff --git a/langs/de_DE/dynamicsprices.lang b/langs/de_DE/dynamicsprices.lang index 08b45db..3d94f5f 100644 --- a/langs/de_DE/dynamicsprices.lang +++ b/langs/de_DE/dynamicsprices.lang @@ -64,3 +64,4 @@ LMDB_QuantityPackaging = Mengenverpackung LMDB_MinQtyPriceHT = Mindestmengenpreis (o. MwSt.) LMDB_SupplierReputation = Reputation LMDB_SupplierPriceUpdatedCount = %s Lieferanten-Einkaufspreise hinzugefügt/aktualisiert. +LMDB_Ignore = Ignorieren diff --git a/langs/en_US/dynamicsprices.lang b/langs/en_US/dynamicsprices.lang index 93c0c75..881996a 100644 --- a/langs/en_US/dynamicsprices.lang +++ b/langs/en_US/dynamicsprices.lang @@ -64,3 +64,4 @@ LMDB_QuantityPackaging = Quantity packaging LMDB_MinQtyPriceHT = Min quantity price (excl. tax) LMDB_SupplierReputation = Reputation LMDB_SupplierPriceUpdatedCount = %s supplier purchase prices added/updated. +LMDB_Ignore = Ignore diff --git a/langs/es_ES/dynamicsprices.lang b/langs/es_ES/dynamicsprices.lang index 0fdccf7..3f37d85 100644 --- a/langs/es_ES/dynamicsprices.lang +++ b/langs/es_ES/dynamicsprices.lang @@ -64,3 +64,4 @@ LMDB_QuantityPackaging = Envasado de cantidades LMDB_MinQtyPriceHT = Precio cantidad mínima (sin IVA) LMDB_SupplierReputation = Reputación LMDB_SupplierPriceUpdatedCount = %s precios de compra de proveedor añadidos/actualizados. +LMDB_Ignore = Ignorar diff --git a/langs/fr_FR/dynamicsprices.lang b/langs/fr_FR/dynamicsprices.lang index ecce593..f907cfe 100644 --- a/langs/fr_FR/dynamicsprices.lang +++ b/langs/fr_FR/dynamicsprices.lang @@ -66,3 +66,4 @@ LMDB_QuantityPackaging = Conditionnement des quantités LMDB_MinQtyPriceHT = Prix quantité min. (HT) LMDB_SupplierReputation = Réputation LMDB_SupplierPriceUpdatedCount = %s prix d'achat fournisseur ajoutés/mis à jour. +LMDB_Ignore = Ignorer diff --git a/langs/it_IT/dynamicsprices.lang b/langs/it_IT/dynamicsprices.lang index 980a93b..1d84ae0 100644 --- a/langs/it_IT/dynamicsprices.lang +++ b/langs/it_IT/dynamicsprices.lang @@ -64,3 +64,4 @@ LMDB_QuantityPackaging = Confezionamento quantità LMDB_MinQtyPriceHT = Prezzo quantità minima (IVA esclusa) LMDB_SupplierReputation = Reputazione LMDB_SupplierPriceUpdatedCount = %s prezzi di acquisto fornitore aggiunti/aggiornati. +LMDB_Ignore = Ignora From 4be5e57a16fec361208970d48ed8b3ff61d566a8 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:11:54 +0100 Subject: [PATCH 08/53] fix: ajouter le token CSRF sur la redirection Ignorer/croix --- class/actions_dynamicsprices.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/class/actions_dynamicsprices.class.php b/class/actions_dynamicsprices.class.php index da0ce1a..1984ccb 100644 --- a/class/actions_dynamicsprices.class.php +++ b/class/actions_dynamicsprices.class.php @@ -208,7 +208,7 @@ public function formConfirm($parameters, &$object, &$action, $hookmanager) array('type' => 'other', 'name' => 'dynamicsprices_diff_table', 'label' => '', 'value' => $html), ); - $ignoreUrl = $url.'&action=confirm_commande&confirm=yes&dynamicsprices_skip_update=1'; + $ignoreUrl = $url.'&action=confirm_commande&confirm=yes&dynamicsprices_skip_update=1&token='.newToken(); $this->resprints = $form->formconfirm($url, $langs->trans('LMDB_SupplierPriceModalTitle'), $langs->trans('LMDB_SupplierPriceModalDescription'), 'confirm_commande', $formquestion, 1, 1, 600, '90%', '', $langs->trans('Validate'), $langs->trans('LMDB_Ignore')); $this->resprints .= ''; @@ -275,13 +287,24 @@ private function getOrderSupplierPriceDifferences($object) $delivery = isset($line->fk_availability) ? (int) $line->fk_availability : 0; $reputation = isset($line->supplier_reputation) ? price2num((float) $line->supplier_reputation, 'MS') : 0; - $current = $this->getCurrentSupplierPrice((int) $line->fk_product, (int) $object->socid, $qty); - $isDifferent = empty($current) - || price2num((float) $current['unitprice'], 'MS') !== $unitprice + $linkedSupplierPriceId = $this->getSupplierPriceIdFromOrderLine($line, (int) $object->id); + $current = $this->getCurrentSupplierPrice((int) $line->fk_product, (int) $object->socid, $qty, $linkedSupplierPriceId); + $currentUnitprice = !empty($current) ? price2num((float) $current['unitprice'], 'MS') : 0; + $newUnitprice = price2num((float) $unitprice, 'MS'); + $priceDelta = price2num($newUnitprice - $currentUnitprice, 'MS'); + $priceDirection = $this->getPriceDirection($currentUnitprice, $newUnitprice); + $priceDeltaPercent = null; + if ($currentUnitprice != 0) { + $priceDeltaPercent = price2num(($priceDelta / $currentUnitprice) * 100, 'MS'); + } + + $isPriceDifferent = empty($current) || $priceDirection !== 'same'; + $isOtherFieldsDifferent = empty($current) || price2num((float) $current['tva_tx'], 'MS') !== $vat || price2num((float) $current['remise_percent'], 'MS') !== $discount || (int) $current['fk_availability'] !== $delivery || price2num((float) $current['supplier_reputation'], 'MS') !== $reputation; + $isDifferent = $isPriceDifferent || $isOtherFieldsDifferent; if (!$isDifferent) { continue; @@ -294,7 +317,14 @@ private function getOrderSupplierPriceDifferences($object) 'qty' => $qty, 'unitquantity' => $unitquantity, 'vat' => $vat, - 'unitprice' => $unitprice, + 'unitprice' => $newUnitprice, + 'current_unitprice' => $currentUnitprice, + 'new_unitprice' => $newUnitprice, + 'price_delta' => $priceDelta, + 'price_delta_percent' => $priceDeltaPercent, + 'price_direction' => $priceDirection, + 'is_price_different' => $isPriceDifferent ? 1 : 0, + 'is_other_fields_different' => $isOtherFieldsDifferent ? 1 : 0, 'discount' => $discount, 'delivery_time_days' => $delivery, 'supplier_reputation' => $reputation, @@ -303,6 +333,7 @@ private function getOrderSupplierPriceDifferences($object) 'supplier_ref' => $this->getSupplierReferenceFromLine($line, (int) $object->socid), 'label' => isset($line->product_label) ? $line->product_label : (isset($line->desc) ? $line->desc : ''), ); + dol_syslog(__METHOD__.' - Supplier price diff detected order='.(int) $object->id.' line='.(int) $line->id.' product='.(int) $line->fk_product.' supplier='.(int) $object->socid.' current='.$currentUnitprice.' proposed='.$newUnitprice.' delta='.$priceDelta.' direction='.$priceDirection, LOG_DEBUG); } dol_syslog(__METHOD__.' - Comparison completed with '.count($differences).' difference(s)', LOG_DEBUG); @@ -315,11 +346,37 @@ private function getOrderSupplierPriceDifferences($object) * @param int $fkProduct Product id * @param int $fkSoc Supplier thirdparty id * @param float $qty Minimum quantity + * @param int $preferredRowid Preferred supplier price rowid linked to the order line * @return array */ - private function getCurrentSupplierPrice($fkProduct, $fkSoc, $qty) + private function getCurrentSupplierPrice($fkProduct, $fkSoc, $qty, $preferredRowid = 0) { global $conf; + if (!empty($preferredRowid)) { + $sql = 'SELECT rowid, unitprice, tva_tx, remise_percent, fk_availability, supplier_reputation'; + $sql .= ' FROM '.MAIN_DB_PREFIX.'product_fournisseur_price'; + $sql .= ' WHERE rowid = '.((int) $preferredRowid); + $sql .= ' AND fk_product = '.((int) $fkProduct); + $sql .= ' AND fk_soc = '.((int) $fkSoc); + $sql .= ' AND entity = '.((int) $conf->entity); + $sql .= ' LIMIT 1'; + + $resql = $this->db->query($sql); + if ($resql && $this->db->num_rows($resql) > 0) { + $obj = $this->db->fetch_object($resql); + if ($obj) { + return array( + 'rowid' => (int) $obj->rowid, + 'unitprice' => (float) $obj->unitprice, + 'tva_tx' => (float) $obj->tva_tx, + 'remise_percent' => (float) $obj->remise_percent, + 'fk_availability' => (int) $obj->fk_availability, + 'supplier_reputation' => (float) $obj->supplier_reputation, + ); + } + } + } + $sql = 'SELECT rowid, unitprice, tva_tx, remise_percent, fk_availability, supplier_reputation'; $sql .= ' FROM '.MAIN_DB_PREFIX.'product_fournisseur_price'; $sql .= ' WHERE fk_product = '.((int) $fkProduct); @@ -348,6 +405,75 @@ private function getCurrentSupplierPrice($fkProduct, $fkSoc, $qty) ); } + /** + * Resolve supplier price rowid linked to a supplier order line. + * + * @param CommonObjectLine $line Supplier order line + * @param int $orderId Supplier order id + * @return int + */ + private function getSupplierPriceIdFromOrderLine($line, $orderId) + { + $propertyCandidates = array('fk_prod_fourn_price', 'fk_fournprice'); + foreach ($propertyCandidates as $propertyName) { + if (!empty($line->{$propertyName})) { + return (int) $line->{$propertyName}; + } + } + + $lineId = !empty($line->id) ? (int) $line->id : 0; + if (empty($lineId) || empty($orderId)) { + return 0; + } + + $lineSupplierPriceField = $this->getSupplierPriceFieldNameOnOrderLineTable(); + if (empty($lineSupplierPriceField)) { + return 0; + } + + $sql = 'SELECT '.$lineSupplierPriceField.' as linked_supplier_price_id'; + $sql .= ' FROM '.MAIN_DB_PREFIX.'commande_fournisseurdet'; + $sql .= ' WHERE rowid = '.$lineId; + $sql .= ' AND fk_commande = '.((int) $orderId); + $sql .= ' LIMIT 1'; + + $resql = $this->db->query($sql); + if ($resql && $this->db->num_rows($resql) > 0) { + $obj = $this->db->fetch_object($resql); + if ($obj && !empty($obj->linked_supplier_price_id)) { + return (int) $obj->linked_supplier_price_id; + } + } + + return 0; + } + + /** + * Get supplier price field name on commande_fournisseurdet table. + * + * @return string + */ + private function getSupplierPriceFieldNameOnOrderLineTable() + { + static $cachedFieldName = null; + if ($cachedFieldName !== null) { + return $cachedFieldName; + } + + $fieldCandidates = array('fk_prod_fourn_price', 'fk_fournprice'); + foreach ($fieldCandidates as $fieldName) { + $sql = "SHOW COLUMNS FROM ".MAIN_DB_PREFIX."commande_fournisseurdet LIKE '".$this->db->escape($fieldName)."'"; + $resql = $this->db->query($sql); + if ($resql && $this->db->num_rows($resql) > 0) { + $cachedFieldName = $fieldName; + return $cachedFieldName; + } + } + + $cachedFieldName = ''; + return $cachedFieldName; + } + /** * Get unit purchase price from supplier order line. * @@ -367,6 +493,69 @@ private function getLineUnitPrice($line) return 0.0; } + /** + * Get price direction between current supplier price and order line price. + * + * @param float $currentUnitprice Current supplier unit price + * @param float $newUnitprice New unit price from order line + * @return string + */ + private function getPriceDirection($currentUnitprice, $newUnitprice) + { + $delta = price2num((float) $newUnitprice - (float) $currentUnitprice, 'MS'); + if (abs($delta) < 0.000001) { + return 'same'; + } + + return ($delta > 0 ? 'up' : 'down'); + } + + /** + * Build display label for the price delta. + * + * @param array $diff Difference payload + * @return string + */ + private function getPriceDeltaLabel(array $diff) + { + $delta = isset($diff['price_delta']) ? price2num($diff['price_delta'], 'MS') : 0; + $prefix = ($delta > 0 ? '+' : ''); + $label = $prefix.$delta; + + if (isset($diff['price_delta_percent']) && $diff['price_delta_percent'] !== null && $diff['price_delta_percent'] !== '') { + $percent = price2num($diff['price_delta_percent'], 'MS'); + $label .= ' ('.$prefix.$percent.'%)'; + } + + return $label; + } + + /** + * Build badge HTML for the price direction. + * + * @param string $direction Direction key up|down|same + * @return string + */ + private function getPriceDirectionBadgeHtml($direction) + { + global $langs; + + $direction = (string) $direction; + $cssClass = 'badge'; + $labelKey = 'LMDB_PriceDirectionSame'; + if ($direction === 'up') { + $cssClass .= ' badge-status4'; + $labelKey = 'LMDB_PriceDirectionUp'; + } elseif ($direction === 'down') { + $cssClass .= ' badge-status1'; + $labelKey = 'LMDB_PriceDirectionDown'; + } else { + $cssClass .= ' badge-status6'; + } + + return ''.$langs->trans($labelKey).''; + } + /** * Insert or update supplier price line from a difference. * @@ -375,53 +564,61 @@ private function getLineUnitPrice($line) */ private function upsertSupplierPriceFromDiff(array $diff) { - global $conf, $user; - - if (!empty($diff['current_rowid'])) { - dol_syslog(__METHOD__.' - Update existing supplier price rowid='.$diff['current_rowid'], LOG_DEBUG); - $sql = 'UPDATE '.MAIN_DB_PREFIX.'product_fournisseur_price'; - $sql .= ' SET unitprice = '.price2num((float) $diff['unitprice'], 'MS'); - $sql .= ', price = '.price2num((float) $diff['unitprice'], 'MS'); - $sql .= ', tva_tx = '.price2num((float) $diff['vat'], 'MS'); - $sql .= ', remise_percent = '.price2num((float) $diff['discount'], 'MS'); - $sql .= ', fk_availability = '.((int) $diff['delivery_time_days']); - $sql .= ', supplier_reputation = '.price2num((float) $diff['supplier_reputation'], 'MS'); - $sql .= ', ref_fourn = '.(empty($diff['supplier_ref']) ? "''" : "'".$this->db->escape($diff['supplier_ref'])."'"); - $sql .= ', fk_user = '.((int) $user->id); - $sql .= ', tms = '.$this->db->idate(dol_now()); - $sql .= ' WHERE rowid = '.((int) $diff['current_rowid']); - $sql .= ' AND entity = '.((int) $conf->entity); - } else { - dol_syslog(__METHOD__.' - Insert new supplier price for product='.$diff['fk_product'].' supplier='.$diff['fk_soc'], LOG_DEBUG); - $sql = 'INSERT INTO '.MAIN_DB_PREFIX.'product_fournisseur_price('; - $sql .= 'entity, fk_product, fk_soc, ref_fourn, quantity, unitquantity, unitprice, price, tva_tx, remise_percent, fk_availability, supplier_reputation, fk_user, datec'; - $sql .= ') VALUES ('; - $sql .= ((int) $conf->entity).', '; - $sql .= ((int) $diff['fk_product']).', '; - $sql .= ((int) $diff['fk_soc']).', '; - $sql .= (empty($diff['supplier_ref']) ? "''" : "'".$this->db->escape($diff['supplier_ref'])."'").', '; - $sql .= price2num((float) $diff['qty'], 'MS').', '; - $sql .= price2num((float) $diff['unitquantity'], 'MS').', '; - $sql .= price2num((float) $diff['unitprice'], 'MS').', '; - $sql .= price2num((float) $diff['unitprice'], 'MS').', '; - $sql .= price2num((float) $diff['vat'], 'MS').', '; - $sql .= price2num((float) $diff['discount'], 'MS').', '; - $sql .= ((int) $diff['delivery_time_days']).', '; - $sql .= price2num((float) $diff['supplier_reputation'], 'MS').', '; - $sql .= ((int) $user->id).', '; - $sql .= $this->db->idate(dol_now()); - $sql .= ')'; - } - dol_syslog(__METHOD__.' - WARNING trace SQL upsert='.$sql, LOG_WARNING); + global $user; + $targetSupplierPriceRowId = !empty($diff['current_rowid']) ? (int) $diff['current_rowid'] : 0; + if (!empty($targetSupplierPriceRowId)) { + $targetSupplierPrice = $this->getCurrentSupplierPrice((int) $diff['fk_product'], (int) $diff['fk_soc'], (float) $diff['qty'], $targetSupplierPriceRowId); + if (empty($targetSupplierPrice)) { + $targetSupplierPriceRowId = 0; + } + } - $resql = $this->db->query($sql); - if (!$resql) { - $this->error = $this->db->lasterror(); + $productFournisseur = new ProductFournisseur($this->db); + $resultFetchProduct = $productFournisseur->fetch((int) $diff['fk_product']); + if ($resultFetchProduct <= 0) { + $this->error = 'Unable to load product id='.((int) $diff['fk_product']).' to update supplier price.'; + $this->errors[] = $this->error; + dol_syslog(__METHOD__.' - '.$this->error, LOG_ERR); + return -1; + } + + $supplier = new Societe($this->db); + $resultFetchSupplier = $supplier->fetch((int) $diff['fk_soc']); + if ($resultFetchSupplier <= 0) { + $this->error = 'Unable to load supplier id='.((int) $diff['fk_soc']).' to update supplier price.'; + $this->errors[] = $this->error; + dol_syslog(__METHOD__.' - '.$this->error, LOG_ERR); + return -1; + } + + $productFournisseur->product_fourn_price_id = $targetSupplierPriceRowId; + dol_syslog(__METHOD__.' - Upsert supplier price through ProductFournisseur::update_buyprice product='.(int) $diff['fk_product'].' supplier='.(int) $diff['fk_soc'].' target_rowid='.$targetSupplierPriceRowId, LOG_DEBUG); + + $resultUpdate = $productFournisseur->update_buyprice( + price2num((float) $diff['qty'], 'MS'), + price2num((float) $diff['unitprice'], 'MS'), + $user, + 'HT', + $supplier, + ((int) $diff['delivery_time_days']), + (isset($diff['supplier_ref']) ? (string) $diff['supplier_ref'] : ''), + price2num((float) $diff['vat'], 'MS'), + 0, + price2num((float) $diff['discount'], 'MS'), + 0, + 0, + ((int) $diff['delivery_time_days']), + price2num((float) $diff['supplier_reputation'], 'MS') + ); + if ($resultUpdate <= 0) { + $this->error = !empty($productFournisseur->error) ? $productFournisseur->error : 'Error while updating supplier price through ProductFournisseur::update_buyprice'; + $this->errors = !empty($productFournisseur->errors) ? $productFournisseur->errors : $this->errors; $this->errors[] = $this->error; - dol_syslog(__METHOD__.' - SQL error: '.$this->error, LOG_ERR); + dol_syslog(__METHOD__.' - Business API error: '.$this->error, LOG_ERR); return -1; } - dol_syslog(__METHOD__.' - SQL upsert successful', LOG_DEBUG); + + dol_syslog(__METHOD__.' - Business API supplier price upsert successful with rowid='.$resultUpdate, LOG_DEBUG); return 1; } @@ -454,6 +651,18 @@ private function applyPostedValuesToDiff($lineId, array $diff, array $postedRows $diff['current_rowid'] = isset($rowData['current_rowid']) ? (int) $rowData['current_rowid'] : $diff['current_rowid']; $diff['supplier_ref'] = isset($rowData['supplier_ref']) ? $rowData['supplier_ref'] : $diff['supplier_ref']; $diff['label'] = isset($rowData['label']) ? $rowData['label'] : $diff['label']; + $diff['new_unitprice'] = $diff['unitprice']; + + if (isset($diff['current_unitprice'])) { + $currentUnitprice = price2num((float) $diff['current_unitprice'], 'MS'); + $newUnitprice = price2num((float) $diff['new_unitprice'], 'MS'); + $diff['price_delta'] = price2num($newUnitprice - $currentUnitprice, 'MS'); + $diff['price_direction'] = $this->getPriceDirection($currentUnitprice, $newUnitprice); + $diff['price_delta_percent'] = null; + if ($currentUnitprice != 0) { + $diff['price_delta_percent'] = price2num(($diff['price_delta'] / $currentUnitprice) * 100, 'MS'); + } + } return $diff; } diff --git a/langs/de_DE/dynamicsprices.lang b/langs/de_DE/dynamicsprices.lang index 19db443..ce47554 100644 --- a/langs/de_DE/dynamicsprices.lang +++ b/langs/de_DE/dynamicsprices.lang @@ -64,5 +64,13 @@ LMDB_QuantityPackaging = Mengenverpackung LMDB_MinQtyPriceHT = Mindestmengenpreis (o. MwSt.) LMDB_SupplierReputation = Reputation LMDB_SupplierPriceUpdatedCount = %s Lieferanten-Einkaufspreise hinzugefügt/aktualisiert. +LMDB_SupplierPriceUpdatedCountWithDirection = %s Lieferanten-Einkaufspreise aktualisiert (%s Erhöhung(en), %s Senkung(en), %s unverändert). LMDB_Ignore = Ignorieren LMDB_SupplierRef = Lieferanten-Ref. +LMDB_CurrentUnitPriceHT = Aktueller Stückpreis (o. Steuern) +LMDB_ProposedUnitPriceHT = Vorgeschlagener Stückpreis (o. Steuern) +LMDB_PriceDeltaHT = Abweichung (o. Steuern) +LMDB_PriceDirection = Entwicklung +LMDB_PriceDirectionUp = ERHÖHUNG +LMDB_PriceDirectionDown = SENKUNG +LMDB_PriceDirectionSame = UNVERÄNDERT diff --git a/langs/en_US/dynamicsprices.lang b/langs/en_US/dynamicsprices.lang index e83af66..c035cf4 100644 --- a/langs/en_US/dynamicsprices.lang +++ b/langs/en_US/dynamicsprices.lang @@ -64,5 +64,13 @@ LMDB_QuantityPackaging = Quantity packaging LMDB_MinQtyPriceHT = Min quantity price (excl. tax) LMDB_SupplierReputation = Reputation LMDB_SupplierPriceUpdatedCount = %s supplier purchase prices added/updated. +LMDB_SupplierPriceUpdatedCountWithDirection = %s supplier purchase prices updated (%s increase(s), %s decrease(s), %s unchanged). LMDB_Ignore = Ignore LMDB_SupplierRef = Supplier ref +LMDB_CurrentUnitPriceHT = Current unit price (excl. tax) +LMDB_ProposedUnitPriceHT = Proposed unit price (excl. tax) +LMDB_PriceDeltaHT = Delta (excl. tax) +LMDB_PriceDirection = Trend +LMDB_PriceDirectionUp = INCREASE +LMDB_PriceDirectionDown = DECREASE +LMDB_PriceDirectionSame = UNCHANGED diff --git a/langs/es_ES/dynamicsprices.lang b/langs/es_ES/dynamicsprices.lang index 721a4b8..8eecf07 100644 --- a/langs/es_ES/dynamicsprices.lang +++ b/langs/es_ES/dynamicsprices.lang @@ -64,5 +64,13 @@ LMDB_QuantityPackaging = Envasado de cantidades LMDB_MinQtyPriceHT = Precio cantidad mínima (sin IVA) LMDB_SupplierReputation = Reputación LMDB_SupplierPriceUpdatedCount = %s precios de compra de proveedor añadidos/actualizados. +LMDB_SupplierPriceUpdatedCountWithDirection = %s precios de proveedor actualizados (%s subida(s), %s bajada(s), %s sin cambios). LMDB_Ignore = Ignorar LMDB_SupplierRef = Ref. proveedor +LMDB_CurrentUnitPriceHT = Precio unitario actual (sin IVA) +LMDB_ProposedUnitPriceHT = Precio unitario propuesto (sin IVA) +LMDB_PriceDeltaHT = Diferencia (sin IVA) +LMDB_PriceDirection = Evolución +LMDB_PriceDirectionUp = SUBIDA +LMDB_PriceDirectionDown = BAJADA +LMDB_PriceDirectionSame = SIN CAMBIOS diff --git a/langs/fr_FR/dynamicsprices.lang b/langs/fr_FR/dynamicsprices.lang index 4b3fb43..b1a5d49 100644 --- a/langs/fr_FR/dynamicsprices.lang +++ b/langs/fr_FR/dynamicsprices.lang @@ -66,5 +66,13 @@ LMDB_QuantityPackaging = Conditionnement des quantités LMDB_MinQtyPriceHT = Prix quantité min. (HT) LMDB_SupplierReputation = Réputation LMDB_SupplierPriceUpdatedCount = %s prix d'achat fournisseur ajoutés/mis à jour. +LMDB_SupplierPriceUpdatedCountWithDirection = %s prix fournisseur mis à jour (%s hausse(s), %s baisse(s), %s inchangé(s)). LMDB_Ignore = Ignorer LMDB_SupplierRef = Réf. fournisseur +LMDB_CurrentUnitPriceHT = Prix unitaire actuel (HT) +LMDB_ProposedUnitPriceHT = Prix unitaire proposé (HT) +LMDB_PriceDeltaHT = Écart (HT) +LMDB_PriceDirection = Évolution +LMDB_PriceDirectionUp = HAUSSE +LMDB_PriceDirectionDown = BAISSE +LMDB_PriceDirectionSame = INCHANGÉ diff --git a/langs/it_IT/dynamicsprices.lang b/langs/it_IT/dynamicsprices.lang index a2be292..8a18132 100644 --- a/langs/it_IT/dynamicsprices.lang +++ b/langs/it_IT/dynamicsprices.lang @@ -64,5 +64,13 @@ LMDB_QuantityPackaging = Confezionamento quantità LMDB_MinQtyPriceHT = Prezzo quantità minima (IVA esclusa) LMDB_SupplierReputation = Reputazione LMDB_SupplierPriceUpdatedCount = %s prezzi di acquisto fornitore aggiunti/aggiornati. +LMDB_SupplierPriceUpdatedCountWithDirection = %s prezzi fornitore aggiornati (%s aumento/i, %s diminuzione/i, %s invariato/i). LMDB_Ignore = Ignora LMDB_SupplierRef = Rif. fornitore +LMDB_CurrentUnitPriceHT = Prezzo unitario attuale (IVA esclusa) +LMDB_ProposedUnitPriceHT = Prezzo unitario proposto (IVA esclusa) +LMDB_PriceDeltaHT = Scostamento (IVA esclusa) +LMDB_PriceDirection = Variazione +LMDB_PriceDirectionUp = AUMENTO +LMDB_PriceDirectionDown = DIMINUZIONE +LMDB_PriceDirectionSame = INVARIATO From 46a7c8e34b2aaa574d31a1854c5cdf640e3b3976 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Fri, 27 Mar 2026 10:40:29 +0100 Subject: [PATCH 14/53] Fix buyprice value sent to update_buyprice API --- class/actions_dynamicsprices.class.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/class/actions_dynamicsprices.class.php b/class/actions_dynamicsprices.class.php index bfb37db..7c216fa 100644 --- a/class/actions_dynamicsprices.class.php +++ b/class/actions_dynamicsprices.class.php @@ -593,10 +593,14 @@ private function upsertSupplierPriceFromDiff(array $diff) $productFournisseur->product_fourn_price_id = $targetSupplierPriceRowId; dol_syslog(__METHOD__.' - Upsert supplier price through ProductFournisseur::update_buyprice product='.(int) $diff['fk_product'].' supplier='.(int) $diff['fk_soc'].' target_rowid='.$targetSupplierPriceRowId, LOG_DEBUG); + $qtyForApi = price2num((float) $diff['qty'], 'MS'); + $unitpriceForApi = price2num((float) $diff['unitprice'], 'MS'); + $buypriceForApi = price2num($unitpriceForApi * $qtyForApi, 'MS'); + dol_syslog(__METHOD__.' - update_buyprice payload qty='.$qtyForApi.' unitprice='.$unitpriceForApi.' buyprice_for_api='.$buypriceForApi.' current_rowid='.$targetSupplierPriceRowId, LOG_DEBUG); $resultUpdate = $productFournisseur->update_buyprice( - price2num((float) $diff['qty'], 'MS'), - price2num((float) $diff['unitprice'], 'MS'), + $qtyForApi, + $buypriceForApi, $user, 'HT', $supplier, From 18ae7f16c88c99a187d774427ec5ff41fed372ec Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Fri, 27 Mar 2026 10:41:13 +0100 Subject: [PATCH 15/53] Preserve supplier order date fields in modal confirm flow --- class/actions_dynamicsprices.class.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/class/actions_dynamicsprices.class.php b/class/actions_dynamicsprices.class.php index 7c216fa..7addb73 100644 --- a/class/actions_dynamicsprices.class.php +++ b/class/actions_dynamicsprices.class.php @@ -187,9 +187,9 @@ public function formConfirm($parameters, &$object, &$action, $hookmanager) $langs->load('dynamicsprices@dynamicsprices'); $url = $_SERVER['PHP_SELF'].'?id='.(int) $object->id; - $url .= '&datecommande='.(int) GETPOST('datecommande', 'int'); - $url .= '&methode='.urlencode(GETPOST('methodecommande', 'alpha')); - $url .= '&comment='.urlencode(GETPOST('comment', 'alphanohtml')); + $datecommande = GETPOST('datecommande', 'alphanohtml'); + $methodecommande = GETPOST('methodecommande', 'alpha'); + $comment = GETPOST('comment', 'alphanohtml'); $csrfToken = newToken(); $url .= '&token='.$csrfToken; @@ -240,9 +240,15 @@ public function formConfirm($parameters, &$object, &$action, $hookmanager) $formquestion = array( array('type' => 'other', 'name' => 'dynamicsprices_diff_table', 'label' => '', 'value' => $html), array('type' => 'hidden', 'name' => 'dynamicsprices_modal', 'value' => '1'), + array('type' => 'hidden', 'name' => 'datecommande', 'value' => $datecommande), + array('type' => 'hidden', 'name' => 'methodecommande', 'value' => $methodecommande), + array('type' => 'hidden', 'name' => 'comment', 'value' => $comment), ); $ignoreUrl = $url.'&action=dynamicsprices_confirm_commande&confirm=no&dynamicsprices_modal=1'; + $ignoreUrl .= '&datecommande='.urlencode($datecommande); + $ignoreUrl .= '&methodecommande='.urlencode($methodecommande); + $ignoreUrl .= '&comment='.urlencode($comment); $this->resprints = $form->formconfirm($url, $langs->trans('LMDB_SupplierPriceModalTitle'), $langs->trans('LMDB_SupplierPriceModalDescription'), 'dynamicsprices_confirm_commande', $formquestion, 1, 1, 600, '90%', '', $langs->trans('Validate'), $langs->trans('LMDB_Ignore')); $this->resprints .= ''; + dol_syslog(__METHOD__.' - Custom confirmation modal rendered', LOG_DEBUG); return 1; } From 0713d251eafa6d5a09063b2137c43a5385146e62 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Sat, 28 Mar 2026 15:01:34 +0100 Subject: [PATCH 52/53] Update actions_dynamicsprices.class.php --- class/actions_dynamicsprices.class.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/class/actions_dynamicsprices.class.php b/class/actions_dynamicsprices.class.php index dea90ea..d455a18 100644 --- a/class/actions_dynamicsprices.class.php +++ b/class/actions_dynamicsprices.class.php @@ -210,7 +210,14 @@ public function formConfirm($parameters, &$object, &$action, $hookmanager) $langs->load('dynamicsprices@dynamicsprices'); $url = $_SERVER['PHP_SELF'].'?id='.(int) $object->id; - $datecommande = GETPOST('datecommande', 'alphanohtml'); + $datecommande = dol_mktime( + GETPOSTINT('rehour'), + GETPOSTINT('remin'), + GETPOSTINT('resec'), + GETPOSTINT('remonth'), + GETPOSTINT('reday'), + GETPOSTINT('reyear') + ); $methodecommande = GETPOST('methodecommande', 'alpha'); $comment = GETPOST('comment', 'alphanohtml'); $csrfToken = newToken(); From 2329d02d73173acc5f37ac56722646117e6428c1 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Sat, 28 Mar 2026 15:13:56 +0100 Subject: [PATCH 53/53] docs: ajouter le changelog de la version 2.1 --- ChangeLog.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index f3f9c99..d2fb285 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,11 @@ # Changelog DynamicsPrices +## 2.1.0 +- Refonte de la gestion des dictionnaires avec l'ajout du dictionnaire "Catégories commerciales" pour remplacer l'usage du dictionnaire "Nature de produit" dans les calculs métier. / Refactored dictionary management by adding the "Commercial categories" dictionary to replace the "Product nature" dictionary in business calculations. +- Ajout de l'extrafield "Catégorie commerciale" sur les produits et services afin de piloter les règles tarifaires depuis une donnée dédiée. / Added the "Commercial category" extrafield on products and services to drive pricing rules from a dedicated field. +- Mise en place de la migration des données depuis l'ancien dictionnaire vers le nouveau et routage de tous les calculs de DynamicPrices vers ce nouveau référentiel. / Implemented data migration from the legacy dictionary to the new one and rerouted all DynamicPrices computations to this new reference. +- Ajout d'une confirmation à l'envoi de commande fournisseur pour proposer la mise à jour des prix d'achat via une modale affichant les écarts et les choix utilisateur (valider/ignorer). / Added a confirmation step on supplier-order submission to propose purchase-price updates through a modal showing differences and user choices (apply/ignore). + ## 2.0.1 - Correction du déclenchement des recalculs de prix lors des événements de prix d'achat/vente quand l'identifiant produit n'est pas directement porté par l'objet trigger. / Fixed price recalculation trigger execution on buy/sell price events when the product identifier is not directly available on the trigger object.