From b2927a68e02181f43db2ef5c87bdcb3879e417dc Mon Sep 17 00:00:00 2001 From: Dolicraft Date: Fri, 19 Jun 2026 09:42:53 +0200 Subject: [PATCH 01/13] Fix #37661 require price_base_type on product CSV import The import_fields_array entry for p.price_base_type accepted a missing/empty value because the CSV importer skips the regex check when newval is empty (import_csv.modules.php:682). Marking the field as mandatory with the trailing star forces the user to map a column and provide HT or TTC, so the row no longer ends up with an empty/NULL value that breaks price displays. Signed-off-by: Dolicraft --- htdocs/core/modules/modProduct.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/core/modules/modProduct.class.php b/htdocs/core/modules/modProduct.class.php index e344fc54b98..9909b2f4811 100644 --- a/htdocs/core/modules/modProduct.class.php +++ b/htdocs/core/modules/modProduct.class.php @@ -514,7 +514,7 @@ class modProduct extends DolibarrModules 'p.price_min' => "MinPrice", 'p.price_ttc' => "SellingPriceTTC", //with tax 'p.price_min_ttc' => "SellingMinPriceTTC", - 'p.price_base_type' => "PriceBaseType", //price base: with-tax (TTC) or without (HT) tax. Displays accordingly in Product card + 'p.price_base_type' => "PriceBaseType*", //price base: with-tax (TTC) or without (HT) tax. Displays accordingly in Product card 'p.tva_tx' => 'VATRate', 'p.datec' => 'DateCreation', 'p.cost_price' => "CostPrice" From b726e3b853e1a11a03bac9ee75ea685cc1e19685 Mon Sep 17 00:00:00 2001 From: ThomasNgr-OpenDSI Date: Tue, 23 Jun 2026 20:45:50 +0200 Subject: [PATCH 02/13] SEC: Sanitize SQL query parameters (#38942) (#38955) Sanitize SQL query parameters. Replace urlencode with explicit type casting to int for fk_statut and signed_status parameters. Co-authored-by: MDW --- htdocs/fichinter/list.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/htdocs/fichinter/list.php b/htdocs/fichinter/list.php index ebe4239dbde..639d5c13a2c 100644 --- a/htdocs/fichinter/list.php +++ b/htdocs/fichinter/list.php @@ -8,6 +8,7 @@ * Copyright (C) 2018 Ferran Marcet * Copyright (C) 2021-2023 Frédéric France * Copyright (C) 2022 Charlène Benke + * Copyright (C) 2024-2026 MDW * * 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 @@ -304,7 +305,7 @@ if ($search_desc) { } } if ($search_status != '' && $search_status >= 0) { - $sql .= ' AND f.fk_statut = '.urlencode($search_status); + $sql .= ' AND f.fk_statut = '.((int) ($search_status)); } if (empty($user->rights->societe->client->voir) && empty($socid)) { $sql .= " AND s.rowid = sc.fk_soc AND sc.fk_user = ".((int) $user->id); From 429d3354e2569964daa5dadf09e8da8b3482b5b4 Mon Sep 17 00:00:00 2001 From: w3houde Date: Sat, 27 Jun 2026 18:24:32 +0200 Subject: [PATCH 03/13] Fix #31471 translate CostPrice and PriceBaseType on import field list (#38922) import.php loadLangs only loaded exports/compta/errors, so labels coming from modProduct / modService import_fields_array that resolve to keys in products.lang (PriceBaseType) and margins.lang (CostPrice) stayed in English on a French install. Add 'products' and 'margins' to the loaded set so the import field picker shows the translated label everywhere else the user sees the product card. Signed-off-by: Dolicraft Co-authored-by: Dolicraft --- htdocs/imports/import.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/imports/import.php b/htdocs/imports/import.php index 77346fd195e..f4c77bd44b6 100644 --- a/htdocs/imports/import.php +++ b/htdocs/imports/import.php @@ -34,7 +34,7 @@ require_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php'; require_once DOL_DOCUMENT_ROOT.'/core/lib/import.lib.php'; // Load translation files required by the page -$langs->loadLangs(array('exports', 'compta', 'errors')); +$langs->loadLangs(array('exports', 'compta', 'errors', 'products', 'margins')); // Security check $result = restrictedArea($user, 'import'); From 60db2cbfe4698ab07b560f97b05dc17cedc80078 Mon Sep 17 00:00:00 2001 From: w3houde Date: Sat, 27 Jun 2026 18:28:39 +0200 Subject: [PATCH 04/13] Fix #34720 do not crash on duplicate bank_url link for same target (#38916) * Fix #36238 keep supplier filter when sorting product list print_liste_field_titre signature is (name, file, field, begin, moreparam, ...) but the seven calls in fourn/product/list.php passed $param at position 4 (begin) instead of 5 (moreparam), so the URL was rebuilt without the fourn_id filter and the list reset when the user clicked a column header. Swap to the correct slot, matching the fix already in the 22.0 line. Signed-off-by: Dolicraft * Fix #34720 do not crash on duplicate bank_url link for same target llx_bank_url has a UNIQUE key on (fk_bank, url_id, type) so dispatching two distinct credit transfers for the same employee on the same bank line raised a Duplicate entry crash that left the operation in an unfinished state. Pre-check the row and reuse the existing rowid silently, keeping the historical link without breaking the calling transaction. Signed-off-by: Dolicraft --------- Signed-off-by: Dolicraft Co-authored-by: Dolicraft --- htdocs/compta/bank/class/account.class.php | 16 ++++++++++++++++ htdocs/fourn/product/list.php | 14 +++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/htdocs/compta/bank/class/account.class.php b/htdocs/compta/bank/class/account.class.php index 26ce53400f9..375ed0c6d46 100644 --- a/htdocs/compta/bank/class/account.class.php +++ b/htdocs/compta/bank/class/account.class.php @@ -463,6 +463,22 @@ class Account extends CommonObject public function add_url_line($line_id, $url_id, $url, $label, $type) { // phpcs:enable + // Avoid uk_bank_url collision when the same target (line_id, url_id, type) is linked twice + // e.g. two distinct credit transfers for the same employee dispatched on the same bank line. + $sqlcheck = "SELECT rowid FROM ".MAIN_DB_PREFIX."bank_url"; + $sqlcheck .= " WHERE fk_bank = ".((int) $line_id); + $sqlcheck .= " AND url_id = ".((int) $url_id); + $sqlcheck .= " AND type = '".$this->db->escape($type)."'"; + $resqlcheck = $this->db->query($sqlcheck); + if ($resqlcheck) { + $obj = $this->db->fetch_object($resqlcheck); + if ($obj) { + $this->db->free($resqlcheck); + return (int) $obj->rowid; + } + $this->db->free($resqlcheck); + } + $sql = "INSERT INTO ".MAIN_DB_PREFIX."bank_url ("; $sql .= "fk_bank"; $sql .= ", url_id"; diff --git a/htdocs/fourn/product/list.php b/htdocs/fourn/product/list.php index 851c1d13ea5..fc03b31b45b 100644 --- a/htdocs/fourn/product/list.php +++ b/htdocs/fourn/product/list.php @@ -315,13 +315,13 @@ if ($resql) { // Line for title print ''; - print_liste_field_titre("Ref", $_SERVER["PHP_SELF"], "p.ref", $param, "", "", $sortfield, $sortorder); - print_liste_field_titre("RefSupplierShort", $_SERVER["PHP_SELF"], "ppf.ref_fourn", $param, "", "", $sortfield, $sortorder); - print_liste_field_titre("Label", $_SERVER["PHP_SELF"], "p.label", $param, "", "", $sortfield, $sortorder); - print_liste_field_titre("Supplier", $_SERVER["PHP_SELF"], "ppf.fk_soc", $param, "", "", $sortfield, $sortorder); - print_liste_field_titre("BuyingPrice", $_SERVER["PHP_SELF"], "ppf.price", $param, "", '', $sortfield, $sortorder, 'right '); - print_liste_field_titre("QtyMin", $_SERVER["PHP_SELF"], "ppf.quantity", $param, "", '', $sortfield, $sortorder, 'right '); - print_liste_field_titre("UnitPrice", $_SERVER["PHP_SELF"], "ppf.unitprice", $param, "", '', $sortfield, $sortorder, 'right '); + print_liste_field_titre("Ref", $_SERVER["PHP_SELF"], "p.ref", "", $param, "", $sortfield, $sortorder); + print_liste_field_titre("RefSupplierShort", $_SERVER["PHP_SELF"], "ppf.ref_fourn", "", $param, "", $sortfield, $sortorder); + print_liste_field_titre("Label", $_SERVER["PHP_SELF"], "p.label", "", $param, "", $sortfield, $sortorder); + print_liste_field_titre("Supplier", $_SERVER["PHP_SELF"], "ppf.fk_soc", "", $param, "", $sortfield, $sortorder); + print_liste_field_titre("BuyingPrice", $_SERVER["PHP_SELF"], "ppf.price", "", $param, '', $sortfield, $sortorder, 'right '); + print_liste_field_titre("QtyMin", $_SERVER["PHP_SELF"], "ppf.quantity", "", $param, '', $sortfield, $sortorder, 'right '); + print_liste_field_titre("UnitPrice", $_SERVER["PHP_SELF"], "ppf.unitprice", "", $param, '', $sortfield, $sortorder, 'right '); // add header cells from hooks $parameters = array(); $reshook = $hookmanager->executeHooks('printFieldListTitle', $parameters, $object, $action); From 9425b899f2eb1f5addad3d78574b2ea3fc01ff24 Mon Sep 17 00:00:00 2001 From: w3houde Date: Sat, 27 Jun 2026 18:57:06 +0200 Subject: [PATCH 05/13] Fix #36238 keep supplier filter when sorting product list (#38915) print_liste_field_titre signature is (name, file, field, begin, moreparam, ...) but the seven calls in fourn/product/list.php passed $param at position 4 (begin) instead of 5 (moreparam), so the URL was rebuilt without the fourn_id filter and the list reset when the user clicked a column header. Swap to the correct slot, matching the fix already in the 22.0 line. Signed-off-by: Dolicraft Co-authored-by: Dolicraft From 155a84d980eedbab38cee819e5bd9c6a92608d07 Mon Sep 17 00:00:00 2001 From: lvessiller-opendsi Date: Thu, 2 Jul 2026 09:53:10 +0200 Subject: [PATCH 06/13] FIX #38957 - Supplier invoice : Multi-currency code/rate and amount not saved when converting EXCESS PAID, CREDIT_NOTE or DEPOSIT to discount (#38971) * FIX #38957 - Supplier invoice : Multi-currency code/rate and amount not saved when converting EXCESS PAID, CREDIT_NOTE or DEPOSIT to discount * Add author --- htdocs/core/class/discount.class.php | 9 ++++++--- htdocs/fourn/facture/card.php | 23 ++++++++++++++++++++--- htdocs/societe/class/societe.class.php | 23 +++++++++++++++++------ 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/htdocs/core/class/discount.class.php b/htdocs/core/class/discount.class.php index fb56965db09..49d43ce4a28 100644 --- a/htdocs/core/class/discount.class.php +++ b/htdocs/core/class/discount.class.php @@ -22,11 +22,12 @@ * \brief File of class to manage absolute discounts */ +require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php'; /** * Class to manage absolute discounts */ -class DiscountAbsolute +class DiscountAbsolute extends CommonObject { /** * @var DoliDB Database handler. @@ -272,13 +273,15 @@ class DiscountAbsolute $sql .= " (entity, datec, fk_soc, discount_type, fk_user, description,"; $sql .= " amount_ht, amount_tva, amount_ttc, tva_tx, vat_src_code,"; $sql .= " multicurrency_amount_ht, multicurrency_amount_tva, multicurrency_amount_ttc,"; - $sql .= " fk_facture_source, fk_invoice_supplier_source"; + $sql .= " fk_facture_source, fk_invoice_supplier_source, multicurrency_code, multicurrency_tx"; $sql .= ")"; $sql .= " VALUES (".$conf->entity.", '".$this->db->idate($this->datec != '' ? $this->datec : dol_now())."', ".((int) $this->fk_soc).", ".(empty($this->discount_type) ? 0 : intval($this->discount_type)).", ".((int) $userid).", '".$this->db->escape($this->description)."',"; $sql .= " ".price2num($this->amount_ht).", ".price2num($this->amount_tva).", ".price2num($this->amount_ttc).", ".price2num($this->tva_tx).", '".$this->db->escape($this->vat_src_code)."',"; $sql .= " ".price2num($this->multicurrency_amount_ht).", ".price2num($this->multicurrency_amount_tva).", ".price2num($this->multicurrency_amount_ttc).", "; $sql .= " ".($this->fk_facture_source ? ((int) $this->fk_facture_source) : "null").","; - $sql .= " ".($this->fk_invoice_supplier_source ? ((int) $this->fk_invoice_supplier_source) : "null"); + $sql .= " ".($this->fk_invoice_supplier_source ? ((int) $this->fk_invoice_supplier_source) : "null").","; + $sql .= " ".($this->multicurrency_code ? "'".$this->db->escape($this->multicurrency_code)."'" : "null").","; + $sql .= " ".($this->multicurrency_tx ? price2num($this->multicurrency_tx) : "null"); $sql .= ")"; dol_syslog(get_class($this)."::create", LOG_DEBUG); diff --git a/htdocs/fourn/facture/card.php b/htdocs/fourn/facture/card.php index b09244a027d..e2b22e2369e 100644 --- a/htdocs/fourn/facture/card.php +++ b/htdocs/fourn/facture/card.php @@ -12,6 +12,7 @@ * Copyright (C) 2018-2023 Frédéric France * Copyright (C) 2019 Ferran Marcet * Copyright (C) 2022 Gauthier VERDOL + * Copyright (C) 2026 Lionel Vessiller * * 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 @@ -643,6 +644,15 @@ if (empty($reshook)) { $discount->tva_tx = 0; $discount->vat_src_code = ''; + // multi-currency + $discount->multicurrency_code = $object->multicurrency_code; + $discount->multicurrency_tx = $object->multicurrency_tx; + $discount->multicurrency_total_ht = $discount->multicurrency_total_ttc = (float) price2num((float) $discount->amount_ttc * (float) $object->multicurrency_tx, 'MT'); + $discount->multicurrency_total_tva = 0; + // keep compatibility + $discount->multicurrency_amount_ht = $discount->multicurrency_amount_ttc = $discount->multicurrency_total_ttc; + $discount->multicurrency_amount_tva = 0; + $result = $discount->create($user); if ($result < 0) { $error++; @@ -653,9 +663,16 @@ if (empty($reshook)) { $discount->amount_ht = abs($amount_ht[$tva_tx]); $discount->amount_tva = abs($amount_tva[$tva_tx]); $discount->amount_ttc = abs($amount_ttc[$tva_tx]); - $discount->multicurrency_amount_ht = abs($multicurrency_amount_ht[$tva_tx]); - $discount->multicurrency_amount_tva = abs($multicurrency_amount_tva[$tva_tx]); - $discount->multicurrency_amount_ttc = abs($multicurrency_amount_ttc[$tva_tx]); + // multi-currency + $discount->multicurrency_code = $object->multicurrency_code; + $discount->multicurrency_tx = $object->multicurrency_tx; + $discount->multicurrency_total_ht = abs((float) $multicurrency_amount_ht[$tva_tx]); + $discount->multicurrency_total_tva = abs((float) $multicurrency_amount_tva[$tva_tx]); + $discount->multicurrency_total_ttc = abs((float) $multicurrency_amount_ttc[$tva_tx]); + // keep compatibility + $discount->multicurrency_amount_ht = $discount->multicurrency_total_ht; + $discount->multicurrency_amount_tva = $discount->multicurrency_total_tva; + $discount->multicurrency_amount_ttc = $discount->multicurrency_total_ttc; // Clean vat code $reg = array(); diff --git a/htdocs/societe/class/societe.class.php b/htdocs/societe/class/societe.class.php index bab3719a902..09add8b5d77 100644 --- a/htdocs/societe/class/societe.class.php +++ b/htdocs/societe/class/societe.class.php @@ -2366,15 +2366,26 @@ class Societe extends CommonObject $discount->fk_soc = $this->id; $discount->discount_type = $discount_type; + $discount->multicurrency_code = $this->multicurrency_code; + list($this->fk_multicurrency, $this->multicurrency_tx) = MultiCurrency::getIdAndTxFromCode($this->db, $this->multicurrency_code); + $discount->multicurrency_tx = $this->multicurrency_tx; if ($price_base_type == 'TTC') { - $discount->amount_ttc = $discount->multicurrency_amount_ttc = price2num($remise, 'MT'); - $discount->amount_ht = $discount->multicurrency_amount_ht = price2num($remise / (1 + $vatrate / 100), 'MT'); - $discount->amount_tva = $discount->multicurrency_amount_tva = price2num($discount->amount_ttc - $discount->amount_ht, 'MT'); + $discount->multicurrency_amount_ttc = price2num($remise * (float) $discount->multicurrency_tx, 'MT'); + $discount->multicurrency_amount_ht = price2num(((float) $remise / (1 + (float) $vatrate / 100)) * (float) $discount->multicurrency_tx, 'MT'); + $discount->multicurrency_amount_tva = price2num(((float) $discount->amount_ttc - (float) $discount->amount_ht) * (float) $discount->multicurrency_tx, 'MT'); + + $discount->amount_ttc = price2num($remise, 'MT'); + $discount->amount_ht = price2num((float) $remise / (1 + (float) $vatrate / 100), 'MT'); + $discount->amount_tva = price2num((float) $discount->amount_ttc - (float) $discount->amount_ht, 'MT'); } else { - $discount->amount_ht = $discount->multicurrency_amount_ht = price2num($remise, 'MT'); - $discount->amount_tva = $discount->multicurrency_amount_tva = price2num($remise * $vatrate / 100, 'MT'); - $discount->amount_ttc = $discount->multicurrency_amount_ttc = price2num($discount->amount_ht + $discount->amount_tva, 'MT'); + $discount->amount_ht = price2num($remise, 'MT'); + $discount->amount_tva = price2num((float) $remise * (float) $vatrate / 100, 'MT'); + $discount->amount_ttc = price2num((float) $discount->amount_ht + (float) $discount->amount_tva, 'MT'); + + $discount->multicurrency_amount_ht = price2num($remise * (float) $discount->multicurrency_tx, 'MT'); + $discount->multicurrency_amount_tva = price2num(((float) $remise * (float) $vatrate / 100) * (float) $discount->multicurrency_tx, 'MT'); + $discount->multicurrency_amount_ttc = price2num(((float) $discount->amount_ht + (float) $discount->amount_tva) * (float) $discount->multicurrency_tx, 'MT'); } $discount->tva_tx = price2num($vatrate); From 20b4edcf33052a91da30cd69222b5ae5308105ac Mon Sep 17 00:00:00 2001 From: w3houde Date: Thu, 2 Jul 2026 14:56:59 +0700 Subject: [PATCH 07/13] Fix #32407 translate prospection status libelle in dictionaries (#38923) The dict.php renderer translated 'label' for some tables (c_units, c_product_nature, c_productbatch_qcstatus) but left the 'libelle' column of c_stcomm and c_stcommcontact as the raw English seed text. Map the row to its StatusProspect{id} translation key when present so the French (or any other locale) shows the expected label instead of 'Do not contact' / 'Never contacted' etc. Signed-off-by: Dolicraft Co-authored-by: Dolicraft --- htdocs/admin/dict.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/htdocs/admin/dict.php b/htdocs/admin/dict.php index 9eab23d2835..ea86e623689 100644 --- a/htdocs/admin/dict.php +++ b/htdocs/admin/dict.php @@ -2234,6 +2234,10 @@ if ($id > 0) { $valuetoshow = $langs->trans($valuetoshow ? $valuetoshow : $tmpid); } elseif ($tabname[$id] == 'c_exp_tax_cat') { $valuetoshow = $langs->trans($valuetoshow); + } elseif ($value == 'libelle' && ($tabname[$id] == 'c_stcomm' || $tabname[$id] == 'c_stcommcontact')) { + $key = 'StatusProspect'.$obj->rowid; + $trans = $langs->trans($key); + $valuetoshow = ($trans != $key ? $trans : $obj->{$value}); } elseif ($value == 'label' && $tabname[$id] == 'c_units') { $langs->load('other'); $key = $langs->trans($obj->label); From 32d74594be010d32664878521062235840257701 Mon Sep 17 00:00:00 2001 From: Dolicraft Date: Tue, 7 Jul 2026 23:19:58 +0700 Subject: [PATCH 08/13] Fix #39063 enforce user write right on other users notes The note editor set $permissionnote from user->self->write only, so a user holding user->user->read plus user->self->write (but not user->user->write) could edit the public/private notes of any user, admins included: the setnote actions are not in restrictedArea's write-check list, so $permissionnote was the only remaining guard. It now requires user->user->write to edit another user's notes, matching caneditfield in user/card.php. Reported by Abderrahmane Aksoum. Signed-off-by: Dolicraft --- htdocs/user/note.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/htdocs/user/note.php b/htdocs/user/note.php index 541cf8c06e8..9ff297e3456 100644 --- a/htdocs/user/note.php +++ b/htdocs/user/note.php @@ -50,7 +50,9 @@ if (($object->id != $user->id) && (!$user->hasRight("user", "user", "read"))) { } // Permissions -$permissionnote = $user->hasRight("user", "self", "write"); // Used by the include of actions_setnotes.inc.php +// Writing notes on own record needs the "self" write right, writing on another user's record needs the "user" write right. +// Without this distinction a user holding only "self" write could edit the notes of any user, including admins. +$permissionnote = ((($object->id == $user->id) && $user->hasRight("user", "self", "write")) || (($object->id != $user->id) && $user->hasRight("user", "user", "write"))); // Used by the include of actions_setnotes.inc.php // Security check $socid = 0; From 64ab2f006bdf556ce7d7bfde9a7b91ff3ac351cf Mon Sep 17 00:00:00 2001 From: ThomasNgr-OpenDSI Date: Thu, 9 Jul 2026 09:20:16 +0200 Subject: [PATCH 09/13] Fix sql injection in sqlfilters API param - reported by sangkilp (#38794) (#39070) Switch four list endpoints that still used the legacy DolibarrApi::_checkFilters + bare preg_replace_callback path to the hardened forgeSQLFromUniversalSearchCriteria() already used by the rest of the same files. The legacy path only validated parenthesis balance and concatenated non-matching text verbatim into the WHERE clause, while the hardened path applies the dummy-criteria syntax check first. Endpoints touched: - htdocs/api/class/api_setup.class.php getListOfRegions - htdocs/api/class/api_setup.class.php getListOfIncoterms - htdocs/api/class/api_setup.class.php getListOfVAT - htdocs/multicurrency/class/api_multicurrencies.class.php index See #38768 for the report. Signed-off-by: Dolicraft Co-authored-by: w3houde Co-authored-by: Dolicraft --- htdocs/api/class/api_setup.class.php | 10 ++++------ .../multicurrency/class/api_multicurrencies.class.php | 7 +++---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/htdocs/api/class/api_setup.class.php b/htdocs/api/class/api_setup.class.php index 2010e41fbb4..83c4a5858f2 100644 --- a/htdocs/api/class/api_setup.class.php +++ b/htdocs/api/class/api_setup.class.php @@ -268,11 +268,10 @@ class Setup extends DolibarrApi // Add sql filters if ($sqlfilters) { $errormessage = ''; - if (!DolibarrApi::_checkFilters($sqlfilters, $errormessage)) { + $sql .= forgeSQLFromUniversalSearchCriteria($sqlfilters, $errormessage); + if ($errormessage) { throw new RestException(400, 'Error when validating parameter sqlfilters -> '.$errormessage); } - $regexstring = '\(([^:\'\(\)]+:[^:\'\(\)]+:[^\(\)]+)\)'; - $sql .= " AND (".preg_replace_callback('/'.$regexstring.'/', 'DolibarrApi::_forge_criteria_callback', $sqlfilters).")"; } $sql .= $this->db->order($sortfield, $sortorder); @@ -1811,11 +1810,10 @@ class Setup extends DolibarrApi // Add sql filters if ($sqlfilters) { $errormessage = ''; - if (!DolibarrApi::_checkFilters($sqlfilters, $errormessage)) { + $sql .= forgeSQLFromUniversalSearchCriteria($sqlfilters, $errormessage); + if ($errormessage) { throw new RestException(400, 'Error when validating parameter sqlfilters -> '.$errormessage); } - $regexstring = '\(([^:\'\(\)]+:[^:\'\(\)]+:[^\(\)]+)\)'; - $sql .= " AND (".preg_replace_callback('/'.$regexstring.'/', 'DolibarrApi::_forge_criteria_callback', $sqlfilters).")"; } diff --git a/htdocs/multicurrency/class/api_multicurrencies.class.php b/htdocs/multicurrency/class/api_multicurrencies.class.php index 6cb66c9c3ac..c45f4ac9187 100644 --- a/htdocs/multicurrency/class/api_multicurrencies.class.php +++ b/htdocs/multicurrency/class/api_multicurrencies.class.php @@ -63,11 +63,10 @@ class MultiCurrencies extends DolibarrApi // Add sql filters if ($sqlfilters) { $errormessage = ''; - if (!DolibarrApi::_checkFilters($sqlfilters, $errormessage)) { - throw new RestException(503, 'Error when validating parameter sqlfilters -> '.$errormessage); + $sql .= forgeSQLFromUniversalSearchCriteria($sqlfilters, $errormessage); + if ($errormessage) { + throw new RestException(400, 'Error when validating parameter sqlfilters -> '.$errormessage); } - $regexstring = '\(([^:\'\(\)]+:[^:\'\(\)]+:[^\(\)]+)\)'; - $sql .= " AND (".preg_replace_callback('/'.$regexstring.'/', 'DolibarrApi::_forge_criteria_callback', $sqlfilters).")"; } $sql .= $this->db->order($sortfield, $sortorder); From 8992ce8704da947b6abe7b65a6fe59aed736bb81 Mon Sep 17 00:00:00 2001 From: w3houde Date: Thu, 9 Jul 2026 14:23:22 +0700 Subject: [PATCH 10/13] Fix #38949 enforce takepos editlines permission on all line actions (#38999) * Fix #38949 enforce takepos editlines permission on all line actions addline, freezone, addnote, deleteline, updateqty, updateprice, updatereduction and update_reduction_global all mutated the invoice content but only updateqty, updateprice and updatereduction had a server-side editlines check. A user with only takepos.run could therefore add free-text lines, change prices, delete lines and apply discounts before payment, bypassing the editlines permission that was supposed to gate exactly those operations. Refuse the request at the top of the action dispatcher when editlines is missing. The editlines right is enabled by default at module activation so existing setups are unaffected. The editorderedlines per-line check kept by updateqty/updateprice/updatereduction still applies after this gate, so the special_code = 4 override path is not broken. Signed-off-by: Dolicraft * Fix #38949 move editlines guard after hook and use hasRight Signed-off-by: Dolicraft * Fix #38949 limit editlines guard to line-edit actions only Adding a line, free zone or note stays under the run permission as in the current stable branch, so a cashier without editlines can still ring up sales. The guard now enforces editlines only on delete/qty/price/discount actions, which is exactly what the report targets. Signed-off-by: Dolicraft --------- Signed-off-by: Dolicraft Co-authored-by: Dolicraft --- htdocs/takepos/invoice.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/htdocs/takepos/invoice.php b/htdocs/takepos/invoice.php index 17470c59b45..c8150fe4d1e 100644 --- a/htdocs/takepos/invoice.php +++ b/htdocs/takepos/invoice.php @@ -180,6 +180,14 @@ $parameters=array(); $reshook=$hookmanager->executeHooks('doActions', $parameters, $invoice, $action); // Note that $action and $object may have been modified by some hooks if ($reshook < 0) setEventMessages($hookmanager->error, $hookmanager->errors, 'errors'); +// Enforce the "edit lines" permission on every action that modifies an existing line +// (delete, quantity, price, discount). Adding a line, a free zone or a note is gated by +// the "run" permission elsewhere and must stay available to a plain cashier (#38949). +if (in_array($action, array('deleteline', 'updateqty', 'updateprice', 'updatereduction', 'update_reduction_global')) && !$user->hasRight('takepos', 'editlines')) { + dol_htmloutput_errors($langs->trans("NotEnoughPermissions", "TakePos"), null, 1); + $action = ''; +} + if (empty($reshook)) { // Action to record a payment on a TakePOS invoice if ($action == 'valid' && $user->hasRight('facture', 'creer')) { From 2ab9bbf8770d5b92400b7679f043c9e1f9fac6e5 Mon Sep 17 00:00:00 2001 From: lvessiller-opendsi Date: Thu, 9 Jul 2026 09:24:34 +0200 Subject: [PATCH 11/13] FIX #38959 - Supplier payment : multi-currency code/rate not saved on auto-created discount when a DEPOSIT supplier invoice is fully paid (#38973) * FIX #38959 - Supplier payment : multi-currency code/rate not saved on auto-created discount when a DEPOSIT supplier invoice is fully paid * Move set of multicurrency_code like in PR #38966 merged --- htdocs/fourn/class/paiementfourn.class.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/htdocs/fourn/class/paiementfourn.class.php b/htdocs/fourn/class/paiementfourn.class.php index 678c86a4b0f..c5fbac2623e 100644 --- a/htdocs/fourn/class/paiementfourn.class.php +++ b/htdocs/fourn/class/paiementfourn.class.php @@ -9,6 +9,7 @@ * Copyright (C) 2018 Frédéric France * Copyright (C) 2023 Joachim Kueter * Copyright (C) 2023 Sylvain Legrand + * Copyright (C) 2026 Lionel Vessiller * * 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 @@ -300,6 +301,8 @@ class PaiementFourn extends Paiement $discount->description = '(DEPOSIT)'; $discount->fk_soc = $invoice->socid; $discount->fk_invoice_supplier_source = $invoice->id; + $discount->multicurrency_code = $invoice->multicurrency_code; + $discount->multicurrency_tx = $invoice->multicurrency_tx; // Loop on each vat rate $i = 0; @@ -319,9 +322,14 @@ class PaiementFourn extends Paiement $discount->amount_ht = abs($amount_ht[$tva_tx]); $discount->amount_tva = abs($amount_tva[$tva_tx]); $discount->amount_ttc = abs($amount_ttc[$tva_tx]); - $discount->multicurrency_amount_ht = abs($multicurrency_amount_ht[$tva_tx]); - $discount->multicurrency_amount_tva = abs($multicurrency_amount_tva[$tva_tx]); - $discount->multicurrency_amount_ttc = abs($multicurrency_amount_ttc[$tva_tx]); + // multi-currency + $discount->multicurrency_total_ht = abs($multicurrency_amount_ht[$tva_tx]); + $discount->multicurrency_total_tva = abs($multicurrency_amount_tva[$tva_tx]); + $discount->multicurrency_total_ttc = abs($multicurrency_amount_ttc[$tva_tx]); + // keep compatibility + $discount->multicurrency_amount_ht = $discount->multicurrency_total_ht; + $discount->multicurrency_amount_tva = $discount->multicurrency_total_tva; + $discount->multicurrency_amount_ttc = $discount->multicurrency_total_ttc; $discount->tva_tx = abs($tva_tx); $result = $discount->create($user); From 7cb02d021400b9eae119b24abd5b253cfa08d427 Mon Sep 17 00:00:00 2001 From: lvessiller-opendsi Date: Thu, 9 Jul 2026 09:25:30 +0200 Subject: [PATCH 12/13] FIX #38958 - Supplier payment : multi-currency code/rate not saved on payment-invoice link when caller does not fill multicurrency arrays (#38972) --- htdocs/fourn/class/paiementfourn.class.php | 23 ++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/htdocs/fourn/class/paiementfourn.class.php b/htdocs/fourn/class/paiementfourn.class.php index c5fbac2623e..9eac1187602 100644 --- a/htdocs/fourn/class/paiementfourn.class.php +++ b/htdocs/fourn/class/paiementfourn.class.php @@ -202,16 +202,31 @@ class PaiementFourn extends Paiement $this->error = $langs->trans('FailedToFoundTheConversionRateForInvoice'); return -1; } - if (empty($currencyofpayment)) { - $currencyofpayment = $this->multicurrency_code[$key]; + // Fallback: read invoice multicurrency code/tx if caller did not fill the arrays + $invoice_multicurrency_code = $this->multicurrency_code[$key] ?? ''; + $invoice_multicurrency_tx = $this->multicurrency_tx[$key] ?? ''; + if (empty($invoice_multicurrency_code) || empty($invoice_multicurrency_tx)) { + $tmparray = MultiCurrency::getInvoiceRate($key, 'facture_fourn'); + if ($tmparray !== false) { + if (empty($invoice_multicurrency_code)) { + $invoice_multicurrency_code = $tmparray['invoice_multicurrency_code']; + } + if (empty($invoice_multicurrency_tx)) { + $invoice_multicurrency_tx = $tmparray['invoice_multicurrency_tx']; + } + } } - if ($currencyofpayment != $this->multicurrency_code[$key]) { + + if (empty($currencyofpayment)) { + $currencyofpayment = $invoice_multicurrency_code; + } + if ($currencyofpayment != $invoice_multicurrency_code) { // If we have invoices with different currencies in the payment, we stop here $this->error = 'ErrorYouTryToPayInvoicesWithDifferentCurrenciesInSamePayment'; return -1; } if (empty($currencytxofpayment)) { - $currencytxofpayment = $this->multicurrency_tx[$key]; + $currencytxofpayment = $invoice_multicurrency_tx; } $totalamount_converted += $value_converted; From d41fd4193bc807596613101858fe58e87297716f Mon Sep 17 00:00:00 2001 From: ThomasNgr-OpenDSI Date: Fri, 10 Jul 2026 17:18:43 +0200 Subject: [PATCH 13/13] FIX Odt generation : create missing temp directory (#39105) --- htdocs/includes/odtphp/odf.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/htdocs/includes/odtphp/odf.php b/htdocs/includes/odtphp/odf.php index 4fd29562183..30fa0ee28d6 100644 --- a/htdocs/includes/odtphp/odf.php +++ b/htdocs/includes/odtphp/odf.php @@ -76,6 +76,10 @@ class Odf $this->tmpfile = $this->tmpdir.'/'.$md5uniqid.'.odt'; // We keep .odt extension to allow OpenOffice usage during debug. // A working directory is required for some zip proxy like PclZipProxy + if (in_array($this->config['ZIP_PROXY'], array('PclZipProxy')) && ! is_dir($this->config['PATH_TO_TMP'])) { + $result = mkdir($this->config['PATH_TO_TMP']); + } + // Check the dir has been created if (in_array($this->config['ZIP_PROXY'], array('PclZipProxy')) && ! is_dir($this->config['PATH_TO_TMP'])) { throw new OdfException('Temporary directory '.$this->config['PATH_TO_TMP'].' must exists'); }