From 9b23a7d79b962d0208e2b66951afcc5b0fc303eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Houssin?= Date: Sat, 25 Jul 2026 11:31:01 +0200 Subject: [PATCH 01/16] FIX remain to pay is 0 if invoice has paid status (#33191) * FIX remain to pay is 0 if invoice has paid status * FIX show the amount with strike and grey color --------- Co-authored-by: Laurent Destailleur --- htdocs/compta/facture/list.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/htdocs/compta/facture/list.php b/htdocs/compta/facture/list.php index 5cda4841905..db694ed7d1e 100644 --- a/htdocs/compta/facture/list.php +++ b/htdocs/compta/facture/list.php @@ -2399,7 +2399,8 @@ if ($resql) { // Pending amount if (!empty($arrayfields['rtp']['checked'])) { - print ''; + $cssamountorstrike = (empty($obj->paye) ? 'amount' : 'colorgrey strikefordisabled'); + print ''; print (!empty($remaintopay) ? price($remaintopay, 0, $langs) : ' '); print ''; // TODO Use a denormalized field if (!$i) { @@ -2408,6 +2409,9 @@ if ($resql) { if (!$i) { $totalarray['pos'][$totalarray['nbfield']] = 'rtp'; } + if (!empty($obj->paye)) { + $remaintopay = 0; // remove from total + } $totalarray['val']['rtp'] += $remaintopay; } @@ -2465,7 +2469,8 @@ if ($resql) { // Pending amount if (!empty($arrayfields['multicurrency_rtp']['checked'])) { - print ''; + $cssamountorstrike = (empty($obj->paye) ? '' : ' colorgrey strikefordisabled'); + print ''; print (!empty($multicurrency_remaintopay) ? price($multicurrency_remaintopay, 0, $langs) : ' '); print ''; // TODO Use a denormalized field ? if (!$i) { From a884cb1aeca12c11b9bc85d9d80ba3f6bbe3e1ae Mon Sep 17 00:00:00 2001 From: MDW Date: Mon, 27 Jul 2026 12:58:22 +0200 Subject: [PATCH 02/16] FIX: Do not quote NULL for fk_user_author & fk_element on insert (#39291) --- htdocs/core/class/comment.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/htdocs/core/class/comment.class.php b/htdocs/core/class/comment.class.php index 4a743e5b070..e04fffe9354 100644 --- a/htdocs/core/class/comment.class.php +++ b/htdocs/core/class/comment.class.php @@ -125,9 +125,9 @@ class Comment extends CommonObject $sql .= ") VALUES ("; $sql .= "'".$this->db->escape($this->description)."'"; $sql .= ", ".($this->datec != '' ? "'".$this->db->idate($this->datec)."'" : 'null'); - $sql .= ", '".(isset($this->fk_element) ? $this->fk_element : "null")."'"; + $sql .= ", ".(isset($this->fk_element) ? ((int) $this->fk_element) : "null"); $sql .= ", '".$this->db->escape($this->element_type)."'"; - $sql .= ", '".(isset($this->fk_user_author) ? $this->fk_user_author : "null")."'"; + $sql .= ", ".(isset($this->fk_user_author) ? ((int) $this->fk_user_author) : "null"); $sql .= ", ".((int) $user->id); $sql .= ", ".(!empty($this->entity) ? $this->entity : '1'); $sql .= ", ".(!empty($this->import_key) ? "'".$this->db->escape($this->import_key)."'" : "null"); From 793abd010dbc62781f8aa67d6c6fae81b8c6e02d Mon Sep 17 00:00:00 2001 From: MDW Date: Mon, 27 Jul 2026 12:59:37 +0200 Subject: [PATCH 03/16] fix(project): Add quotes around date values in SQL query (#39287) idate() call was missing quotes --- htdocs/core/lib/project.lib.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/htdocs/core/lib/project.lib.php b/htdocs/core/lib/project.lib.php index baa71408fd6..47c051416c8 100644 --- a/htdocs/core/lib/project.lib.php +++ b/htdocs/core/lib/project.lib.php @@ -2557,8 +2557,8 @@ function print_projecttasks_array($db, $form, $socid, $projectsListId, $mytasks if (empty($project_year_filter) || !ctype_digit($project_year_filter)) { $project_year_filter = date("Y"); } - $sql .= " AND (p.dateo IS NULL OR p.dateo <= ".$db->idate(dol_get_last_day($project_year_filter, 12, false)).")"; - $sql .= " AND (p.datee IS NULL OR p.datee >= ".$db->idate(dol_get_first_day($project_year_filter, 1, false)).")"; + $sql .= " AND (p.dateo IS NULL OR p.dateo <= '".$db->idate(dol_get_last_day((int) $project_year_filter, 12, false))."')"; + $sql .= " AND (p.datee IS NULL OR p.datee >= '".$db->idate(dol_get_first_day((int) $project_year_filter, 1, false))."')"; } } From c32b5441df8f6d677f9d69666c290eec9a6f9b53 Mon Sep 17 00:00:00 2001 From: Pichinov-Jose Date: Mon, 27 Jul 2026 14:15:21 +0200 Subject: [PATCH 04/16] FIX Accountancy: truncated doc ref not readable in ledger by account (#39242) In accountancy/bookkeeping/listbyaccount.php, the "Piece" column cell uses class tdoverflowmax250, which clips its content to a single line with an ellipsis. For bank entries, the part of the label after the bank line ref ($bank_ref) is printed as a bare text node, with no title attribute and no classfortooltip. As a result the truncated text cannot be read at all: no tooltip appears on hover. This is visible on bank entries whose label references many invoices, e.g. "1234 - Supplier payment INV-0001 INV-0002 ... INV-0025": everything past the first few refs is unreachable. Before 19.0 the cell used class maxwidth400, which only sets a max-width: the label simply wrapped onto several lines and stayed fully readable. The switch to tdoverflowmax250 in 19.0 introduced the clipping without adding a matching tooltip. The neighbouring "Label" column already does this correctly (classfortooltip + title). Wrap $bank_ref in a span carrying classfortooltip and the full doc_ref as title, so hovering shows the complete label. Rendering is unchanged. Also escape the output, which was previously printed unescaped. Co-authored-by: Pichinov-Jose <225026364+Pichinov-Jose@users.noreply.github.com> --- htdocs/accountancy/bookkeeping/listbyaccount.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/htdocs/accountancy/bookkeeping/listbyaccount.php b/htdocs/accountancy/bookkeeping/listbyaccount.php index 6db683085f7..5a0c939c98c 100644 --- a/htdocs/accountancy/bookkeeping/listbyaccount.php +++ b/htdocs/accountancy/bookkeeping/listbyaccount.php @@ -4,6 +4,7 @@ * Copyright (C) 2013-2020 Florian Henry * Copyright (C) 2013-2024 Alexandre Spangaro * Copyright (C) 2018 Frédéric France + * Copyright (C) 2026 Jose Martinez * * 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 @@ -1223,7 +1224,7 @@ while ($i < min($num, $limit)) { } elseif ($line->doc_type == 'bank') { print $objectstatic->getNomUrl(1); $bank_ref = strstr($line->doc_ref, '-'); - print " " . $bank_ref; + print ' '.dol_escape_htmltag($bank_ref).''; } else { print $line->doc_ref; } From 05e94534e18a2091a97d6c4e735e5b2a9595174f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnter=20Lukas?= Date: Fri, 31 Jul 2026 18:27:32 +0200 Subject: [PATCH 05/16] Fix #39345 task extrafield columns not shown/filterable in timespent list (#39346) * Fix #39345 task extrafield columns never show values in timespent list The timespent list (projet/tasks/time.php) let users enable task extrafield columns via the column selector, but the include that prints their value was commented out at all three rendering spots (normal row, split1, split2). Additionally the SQL query never selected the task extrafield values (no join on llx_projet_task_extrafields), so even printing them would have shown nothing. - Add LEFT JOIN on llx_projet_task_extrafields and select its options_* columns, same pattern as Task::getTasksArray() - Re-enable the extrafields_list_print_fields.tpl.php include at the three rendering spots, setting $obj/$extrafieldsobjectkey/ $extrafieldsobjectprefix as the template expects * Fix #39345 task extrafield search/filter and header/param not working Follow-up to the previous fix: the column-selector and value display now worked, but the extrafield's search filter, column header and its contribution to the sort/filter param string were still missing, because more extrafields_list_* template includes were commented out or never wired up in this file: - getOptionalsFromPost() was never called, so search_options_aufgaben from the request never reached $search_array_options - extrafields_list_search_sql.tpl.php was never included, so the filter value never reached the SQL WHERE clause - extrafields_list_search_input.tpl.php (filter input) and extrafields_list_search_title.tpl.php (column header) were commented out, same pattern as the print include fixed earlier - extrafields_list_search_param.tpl.php was commented out, so an active extrafield filter was dropped from sort/pagination links Note the prefix distinction: the SQL WHERE template needs the real SQL table alias ('efpt.'), while the array/header/input/print templates need the $arrayfields array-key prefix ('ef.'). --- htdocs/projet/tasks/time.php | 62 ++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/htdocs/projet/tasks/time.php b/htdocs/projet/tasks/time.php index dbb687094c9..5aaa7b9aeb5 100644 --- a/htdocs/projet/tasks/time.php +++ b/htdocs/projet/tasks/time.php @@ -125,6 +125,8 @@ $projectstatic = new Project($db); $extrafields->fetch_name_optionals_label($projectstatic->table_element); $extrafields->fetch_name_optionals_label($object->table_element); +$search_array_options = $extrafields->getOptionalsFromPost($object->table_element, '', 'search_'); + // Load task if ($id > 0 || $ref) { $object->fetch($id, $ref); @@ -1376,10 +1378,8 @@ if (($id > 0 || !empty($ref)) || $projectidforalltimes > 0 || $allprojectforuser $param .= '&search_timespent_duration_endmin=' . urlencode((string) ($search_timespent_endmin)); } - /* - // Add $param from extra fields - include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_list_search_param.tpl.php'; - */ + // Add $param from extra fields + include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_list_search_param.tpl.php'; if ($id) { $param .= '&id=' . urlencode((string) ($id)); } @@ -1585,6 +1585,11 @@ if (($id > 0 || !empty($ref)) || $projectidforalltimes > 0 || $allprojectforuser $sql .= " il.fk_facture as invoice_id, inv.fk_statut,"; $sql .= " p.fk_soc,s.name_alias,"; $sql .= " t.invoice_line_id"; + if (!empty($extrafields->attributes['projet_task']['label'])) { + foreach ($extrafields->attributes['projet_task']['label'] as $key => $val) { + $sql .= ($extrafields->attributes['projet_task']['type'][$key] != 'separate' ? ",efpt.".$key." as options_".$key : ''); + } + } // Add fields from hooks $parameters = array(); $reshook = $hookmanager->executeHooks('printFieldListSelect', $parameters, $object, $action); // Note that $action and $object may have been modified by hook @@ -1598,6 +1603,7 @@ if (($id > 0 || !empty($ref)) || $projectidforalltimes > 0 || $allprojectforuser $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."facture as inv ON inv.rowid = il.fk_facture"; $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as prod ON prod.rowid = t.fk_product"; $sql .= " INNER JOIN ".MAIN_DB_PREFIX."projet_task as pt ON pt.rowid = t.fk_element"; + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."projet_task_extrafields as efpt ON pt.rowid = efpt.fk_object"; $sql .= " INNER JOIN ".MAIN_DB_PREFIX."projet as p ON p.rowid = pt.fk_projet"; $sql .= " INNER JOIN ".MAIN_DB_PREFIX."user as u ON t.fk_user = u.rowid"; $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as s ON s.rowid = p.fk_soc"; @@ -1692,6 +1698,11 @@ if (($id > 0 || !empty($ref)) || $projectidforalltimes > 0 || $allprojectforuser $sql .= dolSqlDateFilter('t.element_datehour', $search_day, $search_month, $search_year); + // Add where from extra fields + $extrafieldsobjectkey = 'projet_task'; + $extrafieldsobjectprefix = 'efpt.'; + include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_list_search_sql.tpl.php'; + // Add where from hooks $parameters = array(); $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $object, $action); // Note that $action and $object may have been modified by hook @@ -2004,10 +2015,10 @@ if (($id > 0 || !empty($ref)) || $projectidforalltimes > 0 || $allprojectforuser print '' . $form->selectyesno('search_valuebilled', $search_valuebilled, 1, false, 1) . ''; } - /* - // Extra fields - include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_list_search_input.tpl.php'; - */ + // Extra fields + $extrafieldsobjectkey = 'projet_task'; + $extrafieldsobjectprefix = 'ef.'; + include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_list_search_input.tpl.php'; // Fields from hook $parameters = array('arrayfields' => $arrayfields); $reshook = $hookmanager->executeHooks('printFieldListOption', $parameters, $object, $action); // Note that $action and $object may have been modified by hook @@ -2089,10 +2100,10 @@ if (($id > 0 || !empty($ref)) || $projectidforalltimes > 0 || $allprojectforuser print_liste_field_titre($arrayfields['valuebilled']['label'], $_SERVER['PHP_SELF'], 'il.total_ht', '', $param, '', $sortfield, $sortorder, 'center ', $langs->trans("SelectLinesOfTimeSpentToInvoice")); $totalarray['nbfield']++; } - /* - // Extra fields - include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_list_search_title.tpl.php'; - */ + // Extra fields + $extrafieldsobjectkey = 'projet_task'; + $extrafieldsobjectprefix = 'ef.'; + include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_list_search_title.tpl.php'; // Hook fields $parameters = array('arrayfields' => $arrayfields, 'param' => $param, 'sortfield' => $sortfield, 'sortorder' => $sortorder); $reshook = $hookmanager->executeHooks('printFieldListTitle', $parameters, $object, $action); // Note that $action and $object may have been modified by hook @@ -2463,10 +2474,11 @@ if (($id > 0 || !empty($ref)) || $projectidforalltimes > 0 || $allprojectforuser } } - /* - // Extra fields - include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_list_print_fields.tpl.php'; - */ + // Extra fields + $obj = $task_time; + $extrafieldsobjectkey = 'projet_task'; + $extrafieldsobjectprefix = 'ef.'; + include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_list_print_fields.tpl.php'; // Fields from hook $parameters = array('arrayfields' => $arrayfields, 'obj' => $task_time, 'i' => $i, 'totalarray' => &$totalarray); @@ -2663,10 +2675,11 @@ if (($id > 0 || !empty($ref)) || $projectidforalltimes > 0 || $allprojectforuser print ''; } - /* - // Extra fields - include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_list_print_fields.tpl.php'; - */ + // Extra fields + $obj = $task_time; + $extrafieldsobjectkey = 'projet_task'; + $extrafieldsobjectprefix = 'ef.'; + include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_list_print_fields.tpl.php'; // Fields from hook $parameters = array('arrayfields' => $arrayfields, 'obj' => $task_time, 'mode' => 'split1'); @@ -2830,10 +2843,11 @@ if (($id > 0 || !empty($ref)) || $projectidforalltimes > 0 || $allprojectforuser print ''; } - /* - // Extra fields - include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_list_print_fields.tpl.php'; - */ + // Extra fields + $obj = $task_time; + $extrafieldsobjectkey = 'projet_task'; + $extrafieldsobjectprefix = 'ef.'; + include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_list_print_fields.tpl.php'; // Fields from hook $parameters = array('arrayfields' => $arrayfields, 'obj' => $task_time, 'mode' => 'split2'); From ccc27015292b550f6f291ade97b83fdf728292c8 Mon Sep 17 00:00:00 2001 From: Eric - CAP-REL <1468823+rycks@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:09:01 +0200 Subject: [PATCH 06/16] fix donator identity in case of fk_soc is set and empty fields (option in setup) (#39390) --- .../modules/dons/html_cerfafr.modules.php | 63 ++++++++++++++++--- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/htdocs/core/modules/dons/html_cerfafr.modules.php b/htdocs/core/modules/dons/html_cerfafr.modules.php index e48e2e2c092..4c065b29a70 100644 --- a/htdocs/core/modules/dons/html_cerfafr.modules.php +++ b/htdocs/core/modules/dons/html_cerfafr.modules.php @@ -27,6 +27,7 @@ */ require_once DOL_DOCUMENT_ROOT.'/core/modules/dons/modules_don.php'; require_once DOL_DOCUMENT_ROOT.'/don/class/don.class.php'; +require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php'; require_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php'; @@ -121,12 +122,16 @@ class html_cerfafr extends ModeleDon // This is not the proper way to do it but $formclass->form_modes_reglement // prints the translation instead of returning it $formclass->load_cache_types_paiements(); - if ($don->modepaymentid) { - $paymentmode = $formclass->cache_types_paiements[$don->modepaymentid]['label']; + // Don::fetch() stores the payment mode into mode_reglement_id. The modepaymentid property is only + // set by the create/update forms, so it is always empty when the document is built from a fetched + // donation and no payment mode checkbox was ever ticked on the receipt. + $modepaymentid = !empty($don->mode_reglement_id) ? $don->mode_reglement_id : $don->modepaymentid; + if ($modepaymentid) { + $paymentmode = !empty($formclass->cache_types_paiements[$modepaymentid]['label']) ? $formclass->cache_types_paiements[$modepaymentid]['label'] : ''; } else { $paymentmode = ''; } - $modepaymentcode = !empty($formclass->cache_types_paiements[$don->modepaymentid]['code']) ? $formclass->cache_types_paiements[$don->modepaymentid]['code'] : ""; + $modepaymentcode = !empty($formclass->cache_types_paiements[$modepaymentid]['code']) ? $formclass->cache_types_paiements[$modepaymentid]['code'] : ""; if ($modepaymentcode == 'CHQ') { $ModePaiement = ' Remise d\'espèces Chèque Virement, prélèvement, carte bancaire'; } elseif ($modepaymentcode == 'LIQ') { @@ -147,6 +152,43 @@ class html_cerfafr extends ModeleDon $CodeDon = ' 200 du CGI 238 bis du CGI 978 du CGI'; } */ + // Donator identity. + // When option DONATION_USE_THIRDPARTIES is on, the donation form does not show the name/address + // inputs at all, so llx_don only holds fk_soc and every donator field stays empty. The receipt + // would then be issued without the mandatory identity of the donator, so fall back on the linked + // third party for each field left empty. + $donatorsociete = (string) $don->societe; + $donatorlastname = (string) $don->lastname; + $donatorfirstname = (string) $don->firstname; + $donatoraddress = (string) $don->address; + $donatorzip = (string) $don->zip; + $donatortown = (string) $don->town; + + if (!empty($don->socid) && $don->socid > 0) { + $donatorthirdparty = new Societe($this->db); + if ($donatorthirdparty->fetch($don->socid) > 0) { + if (dol_strlen(trim($donatorsociete.$donatorlastname.$donatorfirstname)) == 0) { + // A third party holds a single name field, even for a private individual, so it goes + // to the "Nom" cell of the form and the "Prénoms" cell is left empty. + $donatorsociete = (string) $donatorthirdparty->name; + } + if (dol_strlen(trim($donatoraddress)) == 0) { + $donatoraddress = (string) $donatorthirdparty->address; + } + if (dol_strlen(trim($donatorzip)) == 0) { + $donatorzip = (string) $donatorthirdparty->zip; + } + if (dol_strlen(trim($donatortown)) == 0) { + $donatortown = (string) $donatorthirdparty->town; + } + } else { + dol_syslog("html_cerfafr::write_file Failed to load thirdparty ".$don->socid." linked to donation ".$don->id.", donator block will remain empty - ".$donatorthirdparty->error, LOG_ERR); + } + } + + if (dol_strlen(trim($donatorsociete.$donatorlastname.$donatorfirstname)) == 0) { + dol_syslog("html_cerfafr::write_file No name found for the donator of donation ".$don->id.", the receipt will not be compliant", LOG_WARNING); + } // Define contents $donmodel = DOL_DOCUMENT_ROOT."/core/modules/dons/html_cerfafr.html"; @@ -163,13 +205,16 @@ class html_cerfafr extends ModeleDon $form = str_replace('__MAIN_INFO_SOCIETE_ZIP__', $mysoc->zip, $form); $form = str_replace('__MAIN_INFO_SOCIETE_TOWN__', $mysoc->town, $form); $form = str_replace('__MAIN_INFO_SOCIETE_OBJECT__', $mysoc->socialobject, $form); - $form = str_replace('__DONATOR_FIRSTNAME__', $don->firstname, $form); - $form = str_replace('__DONATOR_LASTNAME__', $don->lastname, $form); - $form = str_replace('__DONATOR_SOCIETE__', $don->societe, $form); + $form = str_replace('__DONATOR_FIRSTNAME__', dol_escape_htmltag($donatorfirstname), $form); + // The template concatenates __DONATOR_SOCIETE__ and __DONATOR_LASTNAME__ with no separator, + // so add a line break when both are filled in. + $donatorlastnameprefix = (dol_strlen(trim($donatorsociete)) > 0 && dol_strlen(trim($donatorlastname)) > 0) ? '
' : ''; + $form = str_replace('__DONATOR_LASTNAME__', $donatorlastnameprefix.dol_escape_htmltag($donatorlastname), $form); + $form = str_replace('__DONATOR_SOCIETE__', dol_escape_htmltag($donatorsociete), $form); $form = str_replace('__DONATOR_STATUT__', $don->statut, $form); - $form = str_replace('__DONATOR_ADDRESS__', $don->address, $form); - $form = str_replace('__DONATOR_ZIP__', $don->zip, $form); - $form = str_replace('__DONATOR_TOWN__', $don->town, $form); + $form = str_replace('__DONATOR_ADDRESS__', dol_nl2br(dol_escape_htmltag($donatoraddress, 0, 1)), $form); + $form = str_replace('__DONATOR_ZIP__', dol_escape_htmltag($donatorzip), $form); + $form = str_replace('__DONATOR_TOWN__', dol_escape_htmltag($donatortown), $form); $form = str_replace('__PAYMENTMODE_LIB__ ', $paymentmode, $form); $form = str_replace('__NOW__', dol_print_date($now, 'day', false, $outputlangs), $form); $form = str_replace('__DonationRef__', $outputlangs->trans("DonationRef"), $form); From 11a736d69efcffc412e72cad82c75dee48973b29 Mon Sep 17 00:00:00 2001 From: TowyTowy <85077986+TowyTowy@users.noreply.github.com> Date: Thu, 6 Aug 2026 17:54:08 +0200 Subject: [PATCH 07/16] FIX phone formatting dropping and duplicating digits for JO and PE (#39398) The substr offsets in dol_print_phone did not line up with the segment lengths, so formatted numbers repeated one digit and lost another. Co-authored-by: Claude Opus 5 --- htdocs/core/lib/functions.lib.php | 8 ++++---- test/phpunit/FunctionsLibTest.php | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/htdocs/core/lib/functions.lib.php b/htdocs/core/lib/functions.lib.php index c6f4b126928..db24d8d2b05 100644 --- a/htdocs/core/lib/functions.lib.php +++ b/htdocs/core/lib/functions.lib.php @@ -4166,7 +4166,7 @@ function dol_print_phone($phone, $countrycode = '', $cid = 0, $socid = 0, $addli } } elseif (strtoupper($countrycode) == "JO") {//Jordanie if (dol_strlen($phone) == 12) {//ex: +962_A_BCD_EF_GH - $newphone = substr($newphone, 0, 4).$separ.substr($newphone, 4, 1).$separ.substr($newphone, 5, 3).$separ.substr($newphone, 7, 2).$separ.substr($newphone, 9, 2); + $newphone = substr($newphone, 0, 4).$separ.substr($newphone, 4, 1).$separ.substr($newphone, 5, 3).$separ.substr($newphone, 8, 2).$separ.substr($newphone, 10, 2); } } elseif (strtoupper($countrycode) == "JM") {//Jamaïque if (dol_strlen($newphone) == 12) {//ex: +1867_ABC_DEFG @@ -4231,12 +4231,12 @@ function dol_print_phone($phone, $countrycode = '', $cid = 0, $socid = 0, $addli $newphone = substr($newphone, 0, 3).$separ.substr($newphone, 3, 4); } elseif (dol_strlen($phone) == 9) {// mobile add code and fix 9 chiffres +51_AAA_BBB_CCC $newphonewa = '+51'.$newphone; - $newphone = substr($newphone, 0, 3).$separ.substr($newphone, 3, 3).$separ.substr($newphone, 6, 3).$separ.substr($newphone, 10, 3); + $newphone = substr($newphone, 0, 3).$separ.substr($newphone, 3, 3).$separ.substr($newphone, 6, 3); } elseif (dol_strlen($phone) == 11) {// fix 11 chiffres +511_AAA_BBBB - $newphone = substr($newphone, 0, 4).$separ.substr($newphone, 4, 3).$separ.substr($newphone, 8, 4); + $newphone = substr($newphone, 0, 4).$separ.substr($newphone, 4, 3).$separ.substr($newphone, 7, 4); } elseif (dol_strlen($phone) == 12) {// mobile +51_AAA_BBB_CCC $newphonewa = $newphone; - $newphone = substr($newphone, 0, 3).$separ.substr($newphone, 3, 3).$separ.substr($newphone, 6, 3).$separ.substr($newphone, 10, 3).$separ.substr($newphone, 14, 3); + $newphone = substr($newphone, 0, 3).$separ.substr($newphone, 3, 3).$separ.substr($newphone, 6, 3).$separ.substr($newphone, 9, 3); } } diff --git a/test/phpunit/FunctionsLibTest.php b/test/phpunit/FunctionsLibTest.php index 60069cd725d..9cd87776e9f 100644 --- a/test/phpunit/FunctionsLibTest.php +++ b/test/phpunit/FunctionsLibTest.php @@ -1220,6 +1220,23 @@ class FunctionsLibTest extends CommonClassTest $object->country_code = 'CA'; $phone = dol_print_phone('1234567890', $object->country_code, 0, 0, 0, ' '); $this->assertEquals('(123) 456-7890', $phone, 'Phone for CA 1'); + + // Every digit must appear exactly once, in order, whatever the country format + $object->country_code = 'JO'; + $phone = dol_print_phone('+96212345678', $object->country_code, 0, 0, 0, ' '); + $this->assertEquals('+962 1 234 56 78', $phone, 'Phone for JO 1'); + + $object->country_code = 'PE'; + $phone = dol_print_phone('987654321', $object->country_code, 0, 0, 0, ' '); + $this->assertEquals('987 654 321', $phone, 'Phone for PE 1'); + + $object->country_code = 'PE'; + $phone = dol_print_phone('+5111234567', $object->country_code, 0, 0, 0, ' '); + $this->assertEquals('+511 123 4567', $phone, 'Phone for PE 2'); + + $object->country_code = 'PE'; + $phone = dol_print_phone('+51987654321', $object->country_code, 0, 0, 0, ' '); + $this->assertEquals('+51 987 654 321', $phone, 'Phone for PE 3'); } From c86264053e5e20da0ac97c23edd40147ec3cfacf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20FRANCE?= Date: Tue, 11 Aug 2026 01:25:46 +0200 Subject: [PATCH 08/16] avoid to delete directory if ref is empty (#38877) * avoid to delete directory * Update contrat.class.php * Update expedition.class.php * Update fournisseur.commande.class.php * Update fournisseur.facture.class.php * Update project.class.php --- htdocs/contrat/class/contrat.class.php | 2 +- htdocs/expedition/class/expedition.class.php | 2 +- htdocs/fichinter/class/fichinter.class.php | 2 +- htdocs/fourn/class/fournisseur.commande.class.php | 2 +- htdocs/fourn/class/fournisseur.facture.class.php | 4 ++-- htdocs/projet/class/project.class.php | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/htdocs/contrat/class/contrat.class.php b/htdocs/contrat/class/contrat.class.php index 9bafe098a09..f268e42e3e2 100644 --- a/htdocs/contrat/class/contrat.class.php +++ b/htdocs/contrat/class/contrat.class.php @@ -1276,7 +1276,7 @@ class Contrat extends CommonObject if (!$error) { // We remove directory $ref = dol_sanitizeFileName($this->ref); - if ($conf->contrat->dir_output) { + if ($conf->contrat->dir_output && !empty($ref)) { $dir = $conf->contrat->multidir_output[$this->entity]."/".$ref; if (file_exists($dir)) { $res = @dol_delete_dir_recursive($dir); diff --git a/htdocs/expedition/class/expedition.class.php b/htdocs/expedition/class/expedition.class.php index bcc6b745980..1580aea2533 100644 --- a/htdocs/expedition/class/expedition.class.php +++ b/htdocs/expedition/class/expedition.class.php @@ -1508,7 +1508,7 @@ class Expedition extends CommonObject // We delete PDFs $ref = dol_sanitizeFileName($this->ref); - if (!empty($conf->expedition->dir_output)) { + if (!empty($conf->expedition->dir_output) && !empty($ref)) { $dir = $conf->expedition->dir_output.'/sending/'.$ref; $file = $dir.'/'.$ref.'.pdf'; if (file_exists($file)) { diff --git a/htdocs/fichinter/class/fichinter.class.php b/htdocs/fichinter/class/fichinter.class.php index 8400f315026..cb7f59255fb 100644 --- a/htdocs/fichinter/class/fichinter.class.php +++ b/htdocs/fichinter/class/fichinter.class.php @@ -1085,7 +1085,7 @@ class Fichinter extends CommonObject // Remove directory with files $fichinterref = dol_sanitizeFileName($this->ref); - if ($conf->ficheinter->dir_output) { + if ($conf->ficheinter->dir_output && !empty($fichinterref)) { $dir = $conf->ficheinter->dir_output."/".$fichinterref; $file = $conf->ficheinter->dir_output."/".$fichinterref."/".$fichinterref.".pdf"; if (file_exists($file)) { diff --git a/htdocs/fourn/class/fournisseur.commande.class.php b/htdocs/fourn/class/fournisseur.commande.class.php index 4cfb71d908f..e18c3bbca93 100644 --- a/htdocs/fourn/class/fournisseur.commande.class.php +++ b/htdocs/fourn/class/fournisseur.commande.class.php @@ -2377,7 +2377,7 @@ class CommandeFournisseur extends CommonOrder // We remove directory $ref = dol_sanitizeFileName($this->ref); - if ($conf->fournisseur->commande->dir_output) { + if ($conf->fournisseur->commande->dir_output && !empty($ref)) { $dir = $conf->fournisseur->commande->dir_output."/".$ref; $file = $dir."/".$ref.".pdf"; if (file_exists($file)) { diff --git a/htdocs/fourn/class/fournisseur.facture.class.php b/htdocs/fourn/class/fournisseur.facture.class.php index 5e4181e2559..847bf538daf 100644 --- a/htdocs/fourn/class/fournisseur.facture.class.php +++ b/htdocs/fourn/class/fournisseur.facture.class.php @@ -1527,10 +1527,10 @@ class FactureFournisseur extends CommonInvoice $this->deleteEcmFiles(); // We remove directory - if ($conf->fournisseur->facture->dir_output) { + $ref = dol_sanitizeFileName($this->ref); + if ($conf->fournisseur->facture->dir_output && !empty($ref)) { include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; - $ref = dol_sanitizeFileName($this->ref); $dir = $conf->fournisseur->facture->dir_output.'/'.get_exdir($this->id, 2, 0, 0, $this, 'invoice_supplier').$ref; $file = $dir."/".$ref.".pdf"; if (file_exists($file)) { diff --git a/htdocs/projet/class/project.class.php b/htdocs/projet/class/project.class.php index 20eda4c3ca3..66e39413e22 100644 --- a/htdocs/projet/class/project.class.php +++ b/htdocs/projet/class/project.class.php @@ -997,7 +997,7 @@ class Project extends CommonObject if (empty($error)) { // We remove directory $projectref = dol_sanitizeFileName($this->ref); - if ($conf->project->dir_output) { + if ($conf->project->dir_output && !empty($projectref)) { $dir = $conf->project->dir_output."/".$projectref; if (file_exists($dir)) { $res = @dol_delete_dir_recursive($dir); From 61b3bc52b820165ec11001a07292679d63a2f760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20FRANCE?= Date: Tue, 11 Aug 2026 02:00:55 +0200 Subject: [PATCH 09/16] fix stock_alerte may be empty (null) (#38484) * fix stock_alerte may be empty (null) * Update product.php --- htdocs/product/stock/product.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/htdocs/product/stock/product.php b/htdocs/product/stock/product.php index c27af71c0bc..332f1f8a3b2 100644 --- a/htdocs/product/stock/product.php +++ b/htdocs/product/stock/product.php @@ -76,8 +76,8 @@ $cancel = GETPOST('cancel', 'alpha'); $id = GETPOSTINT('id'); $ref = GETPOST('ref', 'alpha'); -$stocklimit = GETPOSTFLOAT('seuil_stock_alerte'); -$desiredstock = GETPOSTFLOAT('desiredstock'); +$stocklimit = (GETPOST('seuil_stock_alerte') !== '') ? GETPOSTFLOAT('seuil_stock_alerte') : null; +$desiredstock = (GETPOST('desiredstock') !== '') ? GETPOSTFLOAT('desiredstock') : null; $cancel = GETPOST('cancel', 'alpha'); $fieldid = GETPOSTISSET("ref") ? 'ref' : 'rowid'; $d_eatby = dol_mktime(0, 0, 0, GETPOSTINT('eatbymonth'), GETPOSTINT('eatbyday'), GETPOSTINT('eatbyyear')); From 6a647097bd5b175b2ad8f76ec38ad29dfd297ffd Mon Sep 17 00:00:00 2001 From: w3houde Date: Wed, 12 Aug 2026 00:41:49 +0700 Subject: [PATCH 10/16] Fix #39355 load images.lib.php before image_format_supported in API (#39462) The POST /documents endpoint with generateThumbs=1 called image_format_supported() after requiring only files.lib.php, but that function is defined in images.lib.php, which was never included on this path. The call raised a fatal error and the request returned a bare HTTP 500 with no JSON body. Add the missing require_once. Signed-off-by: Dolicraft Co-authored-by: Dolicraft --- htdocs/api/class/api_documents.class.php | 1 + 1 file changed, 1 insertion(+) diff --git a/htdocs/api/class/api_documents.class.php b/htdocs/api/class/api_documents.class.php index 436b08351a7..2887e90b498 100644 --- a/htdocs/api/class/api_documents.class.php +++ b/htdocs/api/class/api_documents.class.php @@ -1061,6 +1061,7 @@ class Documents extends DolibarrApi if (is_object($object) && $generateThumbs) { require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; + require_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php'; // image_format_supported() is defined here if (image_format_supported($dest_file)) { $object->addThumbs($dest_file); } From d0810e11e397dbcbfc294d45b45d8f71ec0d95d8 Mon Sep 17 00:00:00 2001 From: Zakaria Boushaba <48571684+Boushabazakaria@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:46:22 +0100 Subject: [PATCH 11/16] FIX: Fix mandatory field validation for select (empty '-1') and textarea (CKEDITOR not defined) (#39458) Co-authored-by: Zakaria Boushaba --- htdocs/core/lib/functions.lib.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/htdocs/core/lib/functions.lib.php b/htdocs/core/lib/functions.lib.php index c363460a8e4..e247203d284 100644 --- a/htdocs/core/lib/functions.lib.php +++ b/htdocs/core/lib/functions.lib.php @@ -12055,7 +12055,7 @@ function printCommonFooter($zone = 'private') if (tmptypefield == 'textarea') { // We must instead check the content of ckeditor - var tmpeditor = CKEDITOR.instances['".dol_escape_js($paramkey)."']; + var tmpeditor = (typeof CKEDITOR !== 'undefined') ? CKEDITOR.instances['".dol_escape_js($paramkey)."'] : null; if (tmpeditor) { tmpvalue = tmpeditor.getData(); console.log('For textarea tmpvalue is '+tmpvalue); @@ -12063,7 +12063,7 @@ function printCommonFooter($zone = 'private') } let tmpvalueisempty = false; - if (tmpvalue === null || tmpvalue === undefined || tmpvalue === '' || tmpvalue === -1) { + if (tmpvalue === null || tmpvalue === undefined || tmpvalue === '' || tmpvalue === -1 || tmpvalue === '-1') { tmpvalueisempty = true; } if (tmpvalue === '0' && (tmptypefield == 'select' || tmptypefield == 'input')) { From 79128945ff15689c459092e80562584b240577fe Mon Sep 17 00:00:00 2001 From: w3houde Date: Thu, 13 Aug 2026 04:37:10 +0700 Subject: [PATCH 12/16] Fix #39400 unaccent Hungarian double acute letters (#39476) dol_string_unaccent() did not map the Hungarian letters o-double-acute and u-double-acute (U+0150/U+0151/U+0170/U+0171), so they were left untouched in sanitized filenames. Add the four missing entries so they transliterate to O/o/U/u like the other accented variants. Signed-off-by: Dolicraft Co-authored-by: Dolicraft --- htdocs/core/lib/functions.lib.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/htdocs/core/lib/functions.lib.php b/htdocs/core/lib/functions.lib.php index 2ee32dfae53..0f0b896bdc3 100644 --- a/htdocs/core/lib/functions.lib.php +++ b/htdocs/core/lib/functions.lib.php @@ -1452,18 +1452,18 @@ function dol_string_unaccent($str) '%C3%88' => 'E', '%C3%89' => 'E', '%C3%8A' => 'E', '%C3%8B' => 'E', '%C3%8C' => 'I', '%C3%8D' => 'I', '%C3%8E' => 'I', '%C3%8F' => 'I', '%C3%91' => 'N', - '%C3%92' => 'O', '%C3%93' => 'O', '%C3%94' => 'O', '%C3%95' => 'O', '%C3%96' => 'O', + '%C3%92' => 'O', '%C3%93' => 'O', '%C3%94' => 'O', '%C3%95' => 'O', '%C3%96' => 'O', '%C5%90' => 'O', '%C5%A0' => 'S', - '%C3%99' => 'U', '%C3%9A' => 'U', '%C3%9B' => 'U', '%C3%9C' => 'U', + '%C3%99' => 'U', '%C3%9A' => 'U', '%C3%9B' => 'U', '%C3%9C' => 'U', '%C5%B0' => 'U', '%C3%9D' => 'Y', '%C5%B8' => 'y', '%C3%A0' => 'a', '%C3%A1' => 'a', '%C3%A2' => 'a', '%C3%A3' => 'a', '%C3%A4' => 'a', '%C3%A5' => 'a', '%C3%A7' => 'c', '%C3%A8' => 'e', '%C3%A9' => 'e', '%C3%AA' => 'e', '%C3%AB' => 'e', '%C3%AC' => 'i', '%C3%AD' => 'i', '%C3%AE' => 'i', '%C3%AF' => 'i', '%C3%B1' => 'n', - '%C3%B2' => 'o', '%C3%B3' => 'o', '%C3%B4' => 'o', '%C3%B5' => 'o', '%C3%B6' => 'o', + '%C3%B2' => 'o', '%C3%B3' => 'o', '%C3%B4' => 'o', '%C3%B5' => 'o', '%C3%B6' => 'o', '%C5%91' => 'o', '%C5%A1' => 's', - '%C3%B9' => 'u', '%C3%BA' => 'u', '%C3%BB' => 'u', '%C3%BC' => 'u', + '%C3%B9' => 'u', '%C3%BA' => 'u', '%C3%BB' => 'u', '%C3%BC' => 'u', '%C5%B1' => 'u', '%C3%BD' => 'y', '%C3%BF' => 'y' ); $string = strtr($string, $replacements); From e7ab87886e64a05e55f867da463cc7c23a1b8f48 Mon Sep 17 00:00:00 2001 From: w3houde Date: Thu, 13 Aug 2026 04:37:36 +0700 Subject: [PATCH 13/16] Fix #39395 remove duplicate fk_user_modif in chargesociales update (#39474) ChargeSociales::update() set fk_user_modif twice in the same UPDATE statement. PostgreSQL rejects this with "multiple assignments to the same column", so editing a social contribution failed. Remove the duplicate assignment. Backport of the develop fix #39389 to the 22.0 and 23.0 maintenance branches, where the duplicate is still present. Signed-off-by: Dolicraft Co-authored-by: Dolicraft --- htdocs/compta/sociales/class/chargesociales.class.php | 1 - 1 file changed, 1 deletion(-) diff --git a/htdocs/compta/sociales/class/chargesociales.class.php b/htdocs/compta/sociales/class/chargesociales.class.php index 781c9cb8794..ccb715601c3 100644 --- a/htdocs/compta/sociales/class/chargesociales.class.php +++ b/htdocs/compta/sociales/class/chargesociales.class.php @@ -418,7 +418,6 @@ class ChargeSociales extends CommonObject if ($this->type > 0) { $sql .= ", fk_type = ".((int) $this->type); } - $sql .= ", fk_user_modif=".((int) $user->id); $sql .= " WHERE rowid=".((int) $this->id); dol_syslog(get_class($this)."::update", LOG_DEBUG); From bfbc4db36d5a04d130e551c81bfb0d07a5747f65 Mon Sep 17 00:00:00 2001 From: support-accellier <151022268+support-accellier@users.noreply.github.com> Date: Thu, 13 Aug 2026 06:53:59 +0530 Subject: [PATCH 14/16] Fix currency rate lost (comes as 1) when creating Supplier Invoice from Supplier Order (#39424) The origin multicurrency exchange rate was read with GETPOSTINT('originmulticurrency_tx'), which truncates any fractional rate (e.g. 0.83, 1.27) down to an integer. As a result, creating a Supplier Invoice from a Supplier Order (or other origin) always produced an exchange rate of 0 or 1 instead of the real rate, even when the multi-currency setting to keep the source object's original rate was enabled. This was already fixed for one of the four occurrences in this file (the credit-note/replacement-invoice path) but the other three were never backported to this branch. This commit switches all three remaining occurrences from GETPOSTINT() to GETPOSTFLOAT(), matching develop/23.0. Fixes #39423 --- htdocs/fourn/facture/card.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/htdocs/fourn/facture/card.php b/htdocs/fourn/facture/card.php index 45e397789df..c184eebebe6 100644 --- a/htdocs/fourn/facture/card.php +++ b/htdocs/fourn/facture/card.php @@ -951,7 +951,7 @@ if (empty($reshook)) { $object->fk_incoterms = GETPOSTINT('incoterm_id'); $object->location_incoterms = GETPOST('location_incoterms', 'alpha'); $object->multicurrency_code = GETPOST('multicurrency_code', 'alpha'); - $object->multicurrency_tx = GETPOSTINT('originmulticurrency_tx'); + $object->multicurrency_tx = GETPOSTFLOAT('originmulticurrency_tx'); $object->transport_mode_id = GETPOSTINT('transport_mode_id'); // Proprietes particulieres a facture avoir @@ -1054,7 +1054,7 @@ if (empty($reshook)) { $object->fk_incoterms = GETPOSTINT('incoterm_id'); $object->location_incoterms = GETPOST('location_incoterms', 'alpha'); $object->multicurrency_code = GETPOST('multicurrency_code', 'alpha'); - $object->multicurrency_tx = GETPOSTINT('originmulticurrency_tx'); + $object->multicurrency_tx = GETPOSTFLOAT('originmulticurrency_tx'); // Source facture $object->fac_rec = $fac_recid; @@ -1121,7 +1121,7 @@ if (empty($reshook)) { $object->fk_incoterms = GETPOSTINT('incoterm_id'); $object->location_incoterms = GETPOST('location_incoterms', 'alpha'); $object->multicurrency_code = GETPOST('multicurrency_code', 'alpha'); - $object->multicurrency_tx = GETPOSTINT('originmulticurrency_tx'); + $object->multicurrency_tx = GETPOSTFLOAT('originmulticurrency_tx'); $object->transport_mode_id = GETPOSTINT('transport_mode_id'); // Auto calculation of date due if not filled by user From 53085395c2c15fb37a1576f345a2893515160293 Mon Sep 17 00:00:00 2001 From: w3houde Date: Thu, 13 Aug 2026 19:22:04 +0700 Subject: [PATCH 15/16] Fix #39494 keep decimals on asset disposal amount (#39495) The asset disposal amount was read with GETPOSTINT, casting it to int and dropping the decimals, so a disposal at 1500.50 was stored as 1500. disposal_amount_ht is a 'price' field, so use GETPOSTFLOAT to keep the decimal amount, both when storing it and when re-displaying it in the disposal confirmation form. Signed-off-by: Dolicraft Co-authored-by: Dolicraft --- htdocs/asset/card.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/htdocs/asset/card.php b/htdocs/asset/card.php index 6006d5c4ecb..4b95ef140e5 100644 --- a/htdocs/asset/card.php +++ b/htdocs/asset/card.php @@ -124,7 +124,7 @@ if (empty($reshook)) { // Action dispose object if ($action == 'confirm_disposal' && $confirm == 'yes' && $permissiontoadd) { $object->disposal_date = dol_mktime(12, 0, 0, GETPOSTINT('disposal_datemonth'), GETPOSTINT('disposal_dateday'), GETPOSTINT('disposal_dateyear')); // for date without hour, we use gmt - $object->disposal_amount_ht = GETPOSTINT('disposal_amount'); + $object->disposal_amount_ht = GETPOSTFLOAT('disposal_amount'); $object->fk_disposal_type = GETPOSTINT('fk_disposal_type'); $disposal_invoice_id = GETPOSTINT('disposal_invoice_id'); $object->disposal_depreciated = ((GETPOST('disposal_depreciated') == '1' || GETPOST('disposal_depreciated') == 'on') ? 1 : 0); @@ -265,7 +265,7 @@ if ($object->id > 0 && (empty($action) || ($action != 'edit' && $action != 'crea $langs->load('bills'); $disposal_date = dol_mktime(12, 0, 0, GETPOSTINT('disposal_datemonth'), GETPOSTINT('disposal_dateday'), GETPOSTINT('disposal_dateyear')); // for date without hour, we use gmt - $disposal_amount = GETPOSTINT('disposal_amount'); + $disposal_amount = GETPOSTFLOAT('disposal_amount'); $fk_disposal_type = GETPOSTINT('fk_disposal_type'); $disposal_invoice_id = GETPOSTINT('disposal_invoice_id'); $disposal_depreciated = GETPOSTISSET('disposal_depreciated') ? GETPOST('disposal_depreciated') : 1; From 050a776b0e68d6efe9b46b19e0be69f2325422f2 Mon Sep 17 00:00:00 2001 From: w3houde Date: Thu, 13 Aug 2026 19:25:01 +0700 Subject: [PATCH 16/16] Fix #39020 use universal-search filter for MoLine lookup on V21+ (#39489) Follow-up to the 18.0 fix. On 21.0 and later, MoLine::fetchAll no longer handles fk_mo/origin_id/origin_type as exact matches in the array-filter path; every non-rowid key falls back to LIKE '%value%'. So the array filter added for 18.0 turned into origin_id LIKE '%..%' here, which can match an unrelated MO line and reintroduce the duplicate MO the fix was meant to prevent. Build the lookup with the universal-search string filter (fk_mo:=:.. AND origin_id:=:.. AND origin_type:=:'bomline') so the class turns it into exact equality, as requested during review of #39163. Signed-off-by: Dolicraft Co-authored-by: Dolicraft --- htdocs/mrp/mo_card.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/htdocs/mrp/mo_card.php b/htdocs/mrp/mo_card.php index 425ffe9a99c..df4c0131aa9 100644 --- a/htdocs/mrp/mo_card.php +++ b/htdocs/mrp/mo_card.php @@ -174,7 +174,8 @@ if (empty($reshook)) { // The lookup must be scoped to the parent MO and use an exact match on origin_id/origin_type, // otherwise the default 'origin_id LIKE %..%' filter can return an unrelated line (or none), // which would let the child MO be created from the leftover parent POST data (duplicate MO). - $TMoLines = $moline->fetchAll('DESC', 'rowid', '1', '', array('fk_mo' => $mo_parent->id, 'origin_id' => $id_bom_line, 'origin_type' => 'bomline')); + $filter = '(fk_mo:=:'.((int) $mo_parent->id).') AND (origin_id:=:'.((int) $id_bom_line).") AND (origin_type:=:'bomline')"; + $TMoLines = $moline->fetchAll('DESC', 'rowid', '1', '', $filter); if (empty($TMoLines)) { continue;