From 0dbbd453e5801740ba2d2c59ba3ad4bbf6599ec4 Mon Sep 17 00:00:00 2001 From: Regis Houssin Date: Thu, 16 Oct 2025 19:10:32 +0200 Subject: [PATCH 01/18] FIX Multicompany compatibility with "project_task" --- htdocs/core/ajax/ajaxtooltip.php | 11 +++++++++-- htdocs/core/lib/security.lib.php | 10 +++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/htdocs/core/ajax/ajaxtooltip.php b/htdocs/core/ajax/ajaxtooltip.php index 616832d9555..bc652517efb 100644 --- a/htdocs/core/ajax/ajaxtooltip.php +++ b/htdocs/core/ajax/ajaxtooltip.php @@ -60,8 +60,15 @@ $module = $object->module; $element = $object->element; $usesublevelpermission = ($module != $element ? $element : ''); -if ($usesublevelpermission && !isset($user->rights->$module->$element)) { // There is no permission on object defined, we will check permission on module directly - $usesublevelpermission = ''; +$exclude = array('projet_task', 'project_task'); // for user rights compatibility +if ($usesublevelpermission) { + if (!in_array($usesublevelpermission, $exclude)) { + if (!isset($user->rights->$module->$element)) { // There is no permission on object defined, we will check permission on module directly + $usesublevelpermission = ''; + } + } elseif (!isset($user->rights->$module)) { + $usesublevelpermission = ''; + } } //print $object->id.' - '.$object->module.' - '.$object->element.' - '.$object->table_element.' - '.$usesublevelpermission."\n"; diff --git a/htdocs/core/lib/security.lib.php b/htdocs/core/lib/security.lib.php index 7609da636fe..c96d0034038 100644 --- a/htdocs/core/lib/security.lib.php +++ b/htdocs/core/lib/security.lib.php @@ -877,8 +877,11 @@ function checkUserAccessToObject($user, array $featuresarray, $object = 0, $tabl if ($feature == 'project') { $feature = 'projet'; } - if ($feature == 'task') { - $feature = 'projet_task'; + if ($feature == 'projet' && !empty($feature2) && is_array($feature2) && (in_array('project_task', $feature2) || in_array('projet_task', $feature2))) { + $feature = 'project_task'; + } + if ($feature == 'task' || $feature == 'projet_task') { + $feature = 'project_task'; } if ($feature == 'eventorganization') { $feature = 'agenda'; @@ -899,7 +902,7 @@ function checkUserAccessToObject($user, array $featuresarray, $object = 0, $tabl $checksoc = array('societe'); // Test for object Societe $checkparentsoc = array('agenda', 'contact', 'contrat'); // Test on entity + link to third party on field $dbt_keyfield. Allowed if link is empty (Ex: contacts...). $checkproject = array('projet', 'project'); // Test for project object - $checktask = array('projet_task'); // Test for task object + $checktask = array('projet_task', 'project_task'); // Test for task object $checkhierarchy = array('expensereport', 'holiday'); // check permission among the hierarchy of user $checkuser = array('bookmark'); // check permission among the fk_user (must be myself or null) $nocheck = array('barcode', 'stock'); // No test @@ -1034,6 +1037,7 @@ function checkUserAccessToObject($user, array $featuresarray, $object = 0, $tabl return false; } } else { + $sharedelement = 'project'; // for multicomany compatibility $sql = "SELECT COUNT(dbt.".$dbt_select.") as nb"; $sql .= " FROM ".MAIN_DB_PREFIX.$dbtablename." as dbt"; $sql .= " WHERE dbt.".$dbt_select." IN (".$db->sanitize($objectid, 1).")"; From 12d078c65187880ef8a605497921b9cad1d1b0ea Mon Sep 17 00:00:00 2001 From: Regis Houssin Date: Thu, 16 Oct 2025 19:33:38 +0200 Subject: [PATCH 02/18] FIXX clean code --- htdocs/core/lib/security.lib.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/htdocs/core/lib/security.lib.php b/htdocs/core/lib/security.lib.php index c96d0034038..6497f24ac0a 100644 --- a/htdocs/core/lib/security.lib.php +++ b/htdocs/core/lib/security.lib.php @@ -877,7 +877,7 @@ function checkUserAccessToObject($user, array $featuresarray, $object = 0, $tabl if ($feature == 'project') { $feature = 'projet'; } - if ($feature == 'projet' && !empty($feature2) && is_array($feature2) && (in_array('project_task', $feature2) || in_array('projet_task', $feature2))) { + if ($feature == 'projet' && !empty($feature2) && is_array($feature2) && in_array(array('project_task', 'projet_task'), $feature2)) { $feature = 'project_task'; } if ($feature == 'task' || $feature == 'projet_task') { @@ -1037,7 +1037,7 @@ function checkUserAccessToObject($user, array $featuresarray, $object = 0, $tabl return false; } } else { - $sharedelement = 'project'; // for multicomany compatibility + $sharedelement = 'project'; // for multicompany compatibility $sql = "SELECT COUNT(dbt.".$dbt_select.") as nb"; $sql .= " FROM ".MAIN_DB_PREFIX.$dbtablename." as dbt"; $sql .= " WHERE dbt.".$dbt_select." IN (".$db->sanitize($objectid, 1).")"; From 403dad1660f3fd17cb6fcb999cf4e6121d70b615 Mon Sep 17 00:00:00 2001 From: Marc de Lima Lucio <68746600+marc-dll@users.noreply.github.com> Date: Fri, 17 Oct 2025 13:56:07 +0200 Subject: [PATCH 03/18] FIX: expense report card: do not show bank account if user cannot see them --- htdocs/expensereport/card.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/htdocs/expensereport/card.php b/htdocs/expensereport/card.php index 2776b1e6aa6..4697586a885 100644 --- a/htdocs/expensereport/card.php +++ b/htdocs/expensereport/card.php @@ -1929,9 +1929,10 @@ if ($action == 'create') { print ''; // List of payments already done + $canSeeBankAccount = isModEnabled('banque') && $user->hasRight('banque', 'lire'); $nbcols = 3; $nbrows = 0; - if (isModEnabled("banque")) { + if ($canSeeBankAccount) { $nbrows++; $nbcols++; } @@ -1942,7 +1943,7 @@ if ($action == 'create') { print ''.$langs->trans('Payments').''; print ''.$langs->trans('Date').''; print ''.$langs->trans('Type').''; - if (isModEnabled("banque")) { + if ($canSeeBankAccount) { print ''.$langs->trans('BankAccount').''; } print ''.$langs->trans('Amount').''; @@ -1984,7 +1985,7 @@ if ($action == 'create') { $labeltype = $langs->trans("PaymentType".$objp->payment_code) != ("PaymentType".$objp->payment_code) ? $langs->trans("PaymentType".$objp->payment_code) : $objp->payment_type; print "".$labeltype.' '.$objp->num_payment."\n"; // Bank account - if (isModEnabled("banque")) { + if ($canSeeBankAccount) { $bankaccountstatic->id = $objp->baid; $bankaccountstatic->ref = $objp->baref; $bankaccountstatic->label = $objp->baref; From 66d72d0bbaf89dc774285f5f643b23165cb0a9c8 Mon Sep 17 00:00:00 2001 From: tnegre Date: Fri, 17 Oct 2025 13:59:59 +0200 Subject: [PATCH 04/18] FIX allow a situation with credit to be removed from cycle --- htdocs/compta/facture/card.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/htdocs/compta/facture/card.php b/htdocs/compta/facture/card.php index 39c271ea0ca..5aa884f61f5 100644 --- a/htdocs/compta/facture/card.php +++ b/htdocs/compta/facture/card.php @@ -5128,9 +5128,9 @@ if ($action == 'create') { $total_next_ht = $total_next_ttc = 0; foreach ($object->tab_next_situation_invoice as $next_invoice) { - $totalpaid = $next_invoice->getSommePaiement(0); - $totalcreditnotes = $next_invoice->getSumCreditNotesUsed(0); - $totaldeposits = $next_invoice->getSumDepositsUsed(0); + $next_invoice_total_paid = $next_invoice->getSommePaiement(0); + $next_invoice_totalcreditnotes = $next_invoice->getSumCreditNotesUsed(0); + $next_invoice_totaldeposits = $next_invoice->getSumDepositsUsed(0); $total_next_ht += $next_invoice->total_ht; $total_next_ttc += $next_invoice->total_ttc; @@ -5143,7 +5143,7 @@ if ($action == 'create') { } print ''.price($next_invoice->total_ht).''; print ''.price($next_invoice->total_ttc).''; - print ''.$next_invoice->getLibStatut(3, $totalpaid + $totalcreditnotes + $totaldeposits).''; + print ''.$next_invoice->getLibStatut(3, $next_invoice_total_paid + $next_invoice_totalcreditnotes + $next_invoice_totaldeposits).''; print ''; } From 38980a935cbf20c65eb3d62f209840dd57ad7325 Mon Sep 17 00:00:00 2001 From: Ryad ABANI Date: Wed, 22 Oct 2025 11:56:07 +0200 Subject: [PATCH 05/18] FIX: Email Collector Module: manage error when imap_fetchstructure return false. Previously it generated warning and fatal error because the returned value was not of type class --- .../class/emailcollector.class.php | 37 ++++++++++++++----- .../emailcollector/lib/emailcollector.lib.php | 13 ++++--- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/htdocs/emailcollector/class/emailcollector.class.php b/htdocs/emailcollector/class/emailcollector.class.php index 2f1cb793199..7cbffd1aa53 100644 --- a/htdocs/emailcollector/class/emailcollector.class.php +++ b/htdocs/emailcollector/class/emailcollector.class.php @@ -850,10 +850,9 @@ class EmailCollector extends CommonObject { global $user; - $nberror = 0; + $nbErrors = 0; $arrayofcollectors = $this->fetchAll($user, 1); - // Loop on each collector foreach ($arrayofcollectors as $emailcollector) { $result = $emailcollector->doCollectOneCollector(0); @@ -862,11 +861,12 @@ class EmailCollector extends CommonObject $this->error .= 'EmailCollector ID '.$emailcollector->id.':'.$emailcollector->error.'
'; if (!empty($emailcollector->errors)) { $this->error .= join('
', $emailcollector->errors); + $nbErrors++; } $this->output .= 'EmailCollector ID '.$emailcollector->id.': '.$emailcollector->lastresult.'
'; } - return $nberror; + return $nbErrors; } /** @@ -1755,7 +1755,11 @@ class EmailCollector extends CommonObject $attachments = []; } } else { - $this->getmsg($connection, $imapemail); // This set global var $charset, $htmlmsg, $plainmsg, $attachments + $getMsg = $this->getmsg($connection, $imapemail); // This set global var $charset, $htmlmsg, $plainmsg, $attachments + if ($getMsg < 0) { + $this->errors = array_merge($this->errors, [$this->error]); + return $getMsg; + } } //print $plainmsg; //var_dump($plainmsg); exit; @@ -2916,7 +2920,11 @@ class EmailCollector extends CommonObject $attachment->save($destdir.'/'); } } else { - $this->getmsg($connection, $imapemail, $destdir); + $getMsg = $this->getmsg($connection, $imapemail, $destdir); + if ($getMsg < 0) { + $this->errors = array_merge($this->errors, [$this->error]); + return $getMsg; + } } $operationslog .= '
Project created with attachments -> id='.dol_escape_htmltag($projecttocreate->id); @@ -3046,7 +3054,11 @@ class EmailCollector extends CommonObject $attachment->save($destdir.'/'); } } else { - $this->getmsg($connection, $imapemail, $destdir); + $getMsg = $this->getmsg($connection, $imapemail, $destdir); + if ($getMsg < 0) { + $this->errors = array_merge($this->errors, [$this->error]); + return $getMsg; + } } $operationslog .= '
Ticket created with attachments -> id='.dol_escape_htmltag($tickettocreate->id); @@ -3330,9 +3342,9 @@ class EmailCollector extends CommonObject * @param Object $mbox Structure * @param string $mid UID email * @param string $destdir Target dir for attachments - * @return void + * @return int */ - private function getmsg($mbox, $mid, $destdir = '') + private function getmsg($mbox, $mid, $destdir = ''): int { // input $mbox = IMAP stream, $mid = message id // output all the following: @@ -3346,9 +3358,12 @@ class EmailCollector extends CommonObject // BODY $s = imap_fetchstructure($mbox, $mid, FT_UID); + if ($s === false) { + $this->errors = array_merge($this->errors, [imap_last_error()]); + return -1; + } - - if (!$s->parts) { + if (empty($s->parts)) { // simple $this->getpart($mbox, $mid, $s, 0); // pass 0 as part-number } else { @@ -3357,6 +3372,8 @@ class EmailCollector extends CommonObject $this->getpart($mbox, $mid, $p, $partno0 + 1, $destdir); } } + + return 1; } /* partno string diff --git a/htdocs/emailcollector/lib/emailcollector.lib.php b/htdocs/emailcollector/lib/emailcollector.lib.php index 51190e768ca..eef737cfc86 100644 --- a/htdocs/emailcollector/lib/emailcollector.lib.php +++ b/htdocs/emailcollector/lib/emailcollector.lib.php @@ -115,14 +115,16 @@ function getDParameters($part) * @param object $mbox object connection imaap * @return array type, filename, pos */ -function getAttachments($jk, $mbox) -{ - $structure = imap_fetchstructure($mbox, $jk, FT_UID); + +function getAttachments($jk, $mbox) { + $structure = imap_fetchstructure($mbox, $jk, FT_UID); // @phan-suppress-current-line PhanTypeMismatchArgumentInternal $parts = getParts($structure); $fpos = 2; $attachments = array(); - $nb = count($parts); - if ($parts && $nb) { + + if (!empty($parts)) { + $nb = count($parts); + for ($i = 1; $i < $nb; $i++) { $part = $parts[$i]; @@ -139,6 +141,7 @@ function getAttachments($jk, $mbox) $fpos++; } } + return $attachments; } From 223326610e15ca95566366aea2eb804be0505f5f Mon Sep 17 00:00:00 2001 From: Marc de Lima Lucio <68746600+marc-dll@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:43:21 +0100 Subject: [PATCH 06/18] FIX: expense report card: use correct bank module designator for detection --- htdocs/expensereport/card.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/expensereport/card.php b/htdocs/expensereport/card.php index 4697586a885..f6e9e384134 100644 --- a/htdocs/expensereport/card.php +++ b/htdocs/expensereport/card.php @@ -1929,7 +1929,7 @@ if ($action == 'create') { print ''; // List of payments already done - $canSeeBankAccount = isModEnabled('banque') && $user->hasRight('banque', 'lire'); + $canSeeBankAccount = isModEnabled('bank') && $user->hasRight('banque', 'lire'); $nbcols = 3; $nbrows = 0; if ($canSeeBankAccount) { From 3ad30f8622b2fb0fe66bbcdb6802825761858478 Mon Sep 17 00:00:00 2001 From: Regis Houssin Date: Fri, 7 Nov 2025 12:07:50 +0100 Subject: [PATCH 07/18] FIX use array_intersect instead in_array --- htdocs/core/lib/security.lib.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/core/lib/security.lib.php b/htdocs/core/lib/security.lib.php index 6497f24ac0a..ff85bdf5472 100644 --- a/htdocs/core/lib/security.lib.php +++ b/htdocs/core/lib/security.lib.php @@ -877,7 +877,7 @@ function checkUserAccessToObject($user, array $featuresarray, $object = 0, $tabl if ($feature == 'project') { $feature = 'projet'; } - if ($feature == 'projet' && !empty($feature2) && is_array($feature2) && in_array(array('project_task', 'projet_task'), $feature2)) { + if ($feature == 'projet' && !empty($feature2) && is_array($feature2) && !empty(array_intersect(array('project_task', 'projet_task'), $feature2))) { $feature = 'project_task'; } if ($feature == 'task' || $feature == 'projet_task') { From e5480945bcf050436d38d88b671088badae2593d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9=20Cendrier?= <81741011+altairis-noe@users.noreply.github.com> Date: Wed, 12 Nov 2025 23:59:15 +0100 Subject: [PATCH 08/18] FIX: Mo::deleteLine() - stock movement correction (#34733) * FIX: Mo::deleteLine() - stock movement correction should not rely on linked lines * FIX: do not permit product line to consume removal if product consumption started --- htdocs/mrp/class/mo.class.php | 104 +++++++++++++++++----------------- htdocs/mrp/mo_production.php | 9 +-- 2 files changed, 57 insertions(+), 56 deletions(-) diff --git a/htdocs/mrp/class/mo.class.php b/htdocs/mrp/class/mo.class.php index 1249a181bad..95ccc304cec 100644 --- a/htdocs/mrp/class/mo.class.php +++ b/htdocs/mrp/class/mo.class.php @@ -1,8 +1,9 @@ - * Copyright (C) 2020 Lenin Rivas - * Copyright (C) 2023-2024 Frédéric France - * Copyright (C) 2024 MDW +/* Copyright (C) 2017 Laurent Destailleur + * Copyright (C) 2020 Lenin Rivas + * Copyright (C) 2023-2024 Frédéric France + * Copyright (C) 2024 MDW + * Copyright (C) 2025 Noé Cendrier * * 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 @@ -919,75 +920,74 @@ class Mo extends CommonObject $this->db->begin(); - if (!empty($arrayoflines)) { - // If there is child lines + if (!empty($fk_movement) || !empty($arrayoflines)) { $stockmove = new MouvementStock($this->db); $stockmove->setOrigin($this->element, $this->id); + } - if (!empty($fk_movement)) { - // The fk_movement was not recorded so we try to guess the product and quantity to restore. - $moline = new MoLine($this->db); - $TArrayMoLine = $moline->fetchAll('', '', 1, 0, '(fk_stock_movement:=:'.((int) $fk_movement).')'); - $moline = array_shift($TArrayMoLine); + if (!empty($fk_movement)) { + // The fk_movement was not recorded so we try to guess the product and quantity to restore. + $moline = new MoLine($this->db); + $TArrayMoLine = $moline->fetchAll('', '', 1, 0, '(fk_stock_movement:=:'.((int) $fk_movement).')'); + $moline = array_shift($TArrayMoLine); - $movement = new MouvementStock($this->db); - $movement->fetch($fk_movement); - $productstatic->fetch($movement->product_id); - $qtytoprocess = $movement->qty; + $movement = new MouvementStock($this->db); + $movement->fetch($fk_movement); + $productstatic->fetch($movement->product_id); + $qtytoprocess = $movement->qty; + + // Reverse stock movement + $labelmovementCancel = $langs->trans("CancelProductionForRef", $productstatic->ref); + $codemovementCancel = $langs->trans("StockIncrease"); + + if (($qtytoprocess >= 0)) { + $idstockmove = $stockmove->reception($user, $movement->product_id, $movement->warehouse_id, $qtytoprocess, 0, $labelmovementCancel, '', '', $movement->batch, dol_now(), 0, $codemovementCancel); + } else { + $idstockmove = $stockmove->livraison($user, $movement->product_id, $movement->warehouse_id, $qtytoprocess, 0, $labelmovementCancel, dol_now(), '', '', $movement->batch, 0, $codemovementCancel); + } + if ($idstockmove < 0) { + $this->error++; + setEventMessages($stockmove->error, $stockmove->errors, 'errors'); + } else { + $result = $moline->delete($user, $notrigger); + } + } elseif (!empty($arrayoflines)) { + // Loop on each child lines + foreach ($arrayoflines as $key => $arrayofline) { + $lineDetails = $arrayoflines[$key]; + $productstatic->fetch($lineDetails['fk_product']); + $qtytoprocess = $lineDetails['qty']; // Reverse stock movement $labelmovementCancel = $langs->trans("CancelProductionForRef", $productstatic->ref); $codemovementCancel = $langs->trans("StockIncrease"); - if (($qtytoprocess >= 0)) { - $idstockmove = $stockmove->reception($user, $movement->product_id, $movement->warehouse_id, $qtytoprocess, 0, $labelmovementCancel, '', '', $movement->batch, dol_now(), 0, $codemovementCancel); + + if ($qtytoprocess >= 0) { + $idstockmove = $stockmove->reception($user, $lineDetails['fk_product'], $lineDetails['fk_warehouse'], $qtytoprocess, 0, $labelmovementCancel, '', '', $lineDetails['batch'], dol_now(), 0, $codemovementCancel); } else { - $idstockmove = $stockmove->livraison($user, $movement->product_id, $movement->warehouse_id, $qtytoprocess, 0, $labelmovementCancel, dol_now(), '', '', $movement->batch, 0, $codemovementCancel); + $idstockmove = $stockmove->livraison($user, $lineDetails['fk_product'], $lineDetails['fk_warehouse'], $qtytoprocess, 0, $labelmovementCancel, dol_now(), '', '', $lineDetails['batch'], 0, $codemovementCancel); } if ($idstockmove < 0) { $this->error++; setEventMessages($stockmove->error, $stockmove->errors, 'errors'); } else { - $result = $moline->delete($user, $notrigger); - } - } else { - // Loop on each child lines - foreach ($arrayoflines as $key => $arrayofline) { - $lineDetails = $arrayoflines[$key]; - $productstatic->fetch($lineDetails['fk_product']); - $qtytoprocess = $lineDetails['qty']; + $moline = new MoLine($this->db); + $moline->fetch($lineDetails['rowid']); - // Reverse stock movement - $labelmovementCancel = $langs->trans("CancelProductionForRef", $productstatic->ref); - $codemovementCancel = $langs->trans("StockIncrease"); - - - if ($qtytoprocess >= 0) { - $idstockmove = $stockmove->reception($user, $lineDetails['fk_product'], $lineDetails['fk_warehouse'], $qtytoprocess, 0, $labelmovementCancel, '', '', $lineDetails['batch'], dol_now(), 0, $codemovementCancel); - } else { - $idstockmove = $stockmove->livraison($user, $lineDetails['fk_product'], $lineDetails['fk_warehouse'], $qtytoprocess, 0, $labelmovementCancel, dol_now(), '', '', $lineDetails['batch'], 0, $codemovementCancel); - } - if ($idstockmove < 0) { + $resdel = $moline->delete($user, $notrigger); + if ($resdel < 0) { $this->error++; - setEventMessages($stockmove->error, $stockmove->errors, 'errors'); - } else { - $moline = new MoLine($this->db); - $moline->fetch($lineDetails['rowid']); - - $resdel = $moline->delete($user, $notrigger); - if ($resdel < 0) { - $this->error++; - setEventMessages($moline->error, $moline->errors, 'errors'); - } + setEventMessages($moline->error, $moline->errors, 'errors'); } } - - if (empty($this->error)) { - $result = $this->deleteLineCommon($user, $idline, $notrigger); - } + } + + if (empty($this->error)) { + $result = $this->deleteLineCommon($user, $idline, $notrigger); } } else { - // No child lines + // No child lines and no associated movement $result = $this->deleteLineCommon($user, $idline, $notrigger); } diff --git a/htdocs/mrp/mo_production.php b/htdocs/mrp/mo_production.php index d75b48d44f1..537de7e227e 100644 --- a/htdocs/mrp/mo_production.php +++ b/htdocs/mrp/mo_production.php @@ -2,8 +2,9 @@ /* Copyright (C) 2019-2020 Laurent Destailleur * Copyright (C) 2023 Christian Humpel * Copyright (C) 2023 Vincent de Grandpré - * Copyright (C) 2024 Frédéric France - * Copyright (C) 2024 MDW + * Copyright (C) 2024 Frédéric France + * Copyright (C) 2024 MDW + * Copyright (C) 2025 Noé Cendrier * * 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 @@ -1239,8 +1240,8 @@ if ($object->id > 0 && (empty($action) || ($action != 'edit' && $action != 'crea print ''; } - // Action delete line - if ($permissiontodelete) { + // Action delete line, if no consumption has occured for this product + if ($permissiontodelete && empty($arrayoflines)) { $href = $_SERVER["PHP_SELF"] . '?id=' . ((int) $object->id) . '&action=deleteline&token=' . newToken() . '&lineid=' . ((int) $line->id); print ''; print ''; From 278983c4e73cf5daa8ffeb052ae3eb6f0816d51f Mon Sep 17 00:00:00 2001 From: Laurent Destailleur Date: Mon, 17 Nov 2025 21:16:46 +0100 Subject: [PATCH 09/18] Fix CI --- htdocs/mrp/mo_production.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/mrp/mo_production.php b/htdocs/mrp/mo_production.php index 537de7e227e..073dacf2a61 100644 --- a/htdocs/mrp/mo_production.php +++ b/htdocs/mrp/mo_production.php @@ -1240,7 +1240,7 @@ if ($object->id > 0 && (empty($action) || ($action != 'edit' && $action != 'crea print ''; } - // Action delete line, if no consumption has occured for this product + // Action delete line, if no consumption has occurred for this product if ($permissiontodelete && empty($arrayoflines)) { $href = $_SERVER["PHP_SELF"] . '?id=' . ((int) $object->id) . '&action=deleteline&token=' . newToken() . '&lineid=' . ((int) $line->id); print ''; From cd984fa6c5b2eb1e9812ef46baf2fbd0005e0e98 Mon Sep 17 00:00:00 2001 From: noec764 <58433943+noec764@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:18:28 +0100 Subject: [PATCH 10/18] FIX: Object should be cloned here (#36289) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Noé --- htdocs/categories/class/categorie.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/categories/class/categorie.class.php b/htdocs/categories/class/categorie.class.php index 141401b43b2..2d4f3bab5aa 100644 --- a/htdocs/categories/class/categorie.class.php +++ b/htdocs/categories/class/categorie.class.php @@ -958,7 +958,7 @@ class Categorie extends CommonObject $obj->id = 0; $obj->fetch($rec['fk_object']); if ($obj->id > 0) { // Failing fetch may happen for example when a category supplier was set and third party was moved as customer only. The object supplier can't be loaded. - $objs[] = $obj; + $objs[] = clone $obj; } } } From 21843e68b00a77c002bd0e16f27ba690c0d43d7b Mon Sep 17 00:00:00 2001 From: Regis Houssin Date: Tue, 18 Nov 2025 10:12:05 +0100 Subject: [PATCH 11/18] FIX #36149 (#36150) --- .../triggers/interface_20_modWorkflow_WorkflowManager.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/core/triggers/interface_20_modWorkflow_WorkflowManager.class.php b/htdocs/core/triggers/interface_20_modWorkflow_WorkflowManager.class.php index 520c6795d1e..e92c0e01b85 100644 --- a/htdocs/core/triggers/interface_20_modWorkflow_WorkflowManager.class.php +++ b/htdocs/core/triggers/interface_20_modWorkflow_WorkflowManager.class.php @@ -463,7 +463,7 @@ class InterfaceWorkflowManager extends DolibarrTriggers if (isModEnabled('contract') && isModEnabled('ticket') && isModEnabled('workflow') && getDolGlobalString('WORKFLOW_TICKET_LINK_CONTRACT') && getDolGlobalString('TICKET_PRODUCT_CATEGORY') && !empty($object->fk_soc)) { $societe = new Societe($this->db); $company_ids = (!getDolGlobalString('WORKFLOW_TICKET_USE_PARENT_COMPANY_CONTRACTS')) ? [$object->fk_soc] : $societe->getParentsForCompany($object->fk_soc, [$object->fk_soc]); - + require_once DOL_DOCUMENT_ROOT.'/contrat/class/contrat.class.php'; $contrat = new Contrat($this->db); $number_contracts_found = 0; foreach ($company_ids as $company_id) { From 2027f9c95110e5c1bb0d3707eabcb549cf44220b Mon Sep 17 00:00:00 2001 From: Regis Houssin Date: Tue, 18 Nov 2025 11:31:38 +0100 Subject: [PATCH 12/18] FIX avoid php warnings (#36301) --- htdocs/compta/cashcontrol/report.php | 2 +- htdocs/core/tpl/list_print_total.tpl.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/htdocs/compta/cashcontrol/report.php b/htdocs/compta/cashcontrol/report.php index 67ec8ab467b..b08a4b707b3 100644 --- a/htdocs/compta/cashcontrol/report.php +++ b/htdocs/compta/cashcontrol/report.php @@ -223,7 +223,7 @@ if ($resql) { $transactionspertype = array(); $amountpertype = array(); - $totalarray = array('nbfield' => 0, 'pos' => array()); + $totalarray = array('nbfield' => 0, 'pos' => array(), 'val' => array('totaldebfield' => 0, 'totalcredfield' => 0)); while ($i < $num) { $objp = $db->fetch_object($resql); diff --git a/htdocs/core/tpl/list_print_total.tpl.php b/htdocs/core/tpl/list_print_total.tpl.php index 8990ce12984..adc61ac141a 100644 --- a/htdocs/core/tpl/list_print_total.tpl.php +++ b/htdocs/core/tpl/list_print_total.tpl.php @@ -20,7 +20,7 @@ if (isset($totalarray['pos'])) { printTotalValCell($totalarray['type'][$i] ?? '', empty($totalarray['val'][$totalarray['pos'][$i]]) ? 0 : $totalarray['val'][$totalarray['pos'][$i]]); } else { if ($i == 1) { - if ((is_null($limit) || $num < $limit) && empty($offset)) { + if ((!isset($limit) || is_null($limit) || $num < $limit) && empty($offset)) { print ''.$langs->trans("Total").''; } else { print ''; From 8e30fc0bd1f8b1da3fbe6b230877e69da290ac25 Mon Sep 17 00:00:00 2001 From: Florian Mortgat <50440633+atm-florianm@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:39:45 +0100 Subject: [PATCH 13/18] FIX 19.0 - attachments upload dir for invoices not always determined correctly (doesn't always take multi-entity into account) (#36302) --- htdocs/compta/facture/document.php | 5 ++--- htdocs/core/lib/invoice.lib.php | 3 +++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/htdocs/compta/facture/document.php b/htdocs/compta/facture/document.php index e35e8165582..7e4a95619d5 100644 --- a/htdocs/compta/facture/document.php +++ b/htdocs/compta/facture/document.php @@ -68,9 +68,9 @@ if (!$sortfield) { } $object = new Facture($db); -if ($object->fetch($id, $ref)) { +if ($object->fetch($id, $ref) > 0) { $object->fetch_thirdparty(); - $upload_dir = $conf->facture->dir_output."/".dol_sanitizeFileName($object->ref); + $upload_dir = $conf->facture->multidir_output[$object->entity].'/'.dol_sanitizeFileName($object->ref); } $permissiontoadd = $user->hasRight('facture', 'creer'); @@ -119,7 +119,6 @@ if ($id > 0 || !empty($ref)) { if ($object->fetch($id, $ref) > 0) { $object->fetch_thirdparty(); - $upload_dir = $conf->facture->multidir_output[$object->entity].'/'.dol_sanitizeFileName($object->ref); $head = facture_prepare_head($object); print dol_get_fiche_head($head, 'documents', $langs->trans('InvoiceCustomer'), -1, 'bill'); diff --git a/htdocs/core/lib/invoice.lib.php b/htdocs/core/lib/invoice.lib.php index 852b06cdb4d..5f6af0f4019 100644 --- a/htdocs/core/lib/invoice.lib.php +++ b/htdocs/core/lib/invoice.lib.php @@ -108,6 +108,9 @@ function facture_prepare_head($object) require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; require_once DOL_DOCUMENT_ROOT.'/core/class/link.class.php'; $upload_dir = $conf->facture->dir_output."/".dol_sanitizeFileName($object->ref); + if (!empty($conf->facture->multidir_output[$object->entity])) { + $upload_dir = $conf->facture->multidir_output[$object->entity]."/".dol_sanitizeFileName($object->ref); + } $nbFiles = count(dol_dir_list($upload_dir, 'files', 0, '', '(\.meta|_preview.*\.png)$')); $nbLinks = Link::count($db, $object->element, $object->id); $head[$h][0] = DOL_URL_ROOT.'/compta/facture/document.php?id='.$object->id; From c0850d9a3f27185e93466c75ef49b194f7b23152 Mon Sep 17 00:00:00 2001 From: Florian Mortgat <50440633+atm-florianm@users.noreply.github.com> Date: Wed, 19 Nov 2025 19:19:55 +0100 Subject: [PATCH 14/18] FIX DA027383: permissions not checked on HRM evaluation card (#36328) Permissions involved: - hrm->evaluation->readall: the user can view anyone's evaluations - hrm->evaluation->read: the user can only view their or their subordinates' evaluations --- htdocs/core/lib/security.lib.php | 20 +++++++++++++++++++- htdocs/hrm/evaluation_card.php | 4 ++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/htdocs/core/lib/security.lib.php b/htdocs/core/lib/security.lib.php index 23562a4e44a..ecbeda58328 100644 --- a/htdocs/core/lib/security.lib.php +++ b/htdocs/core/lib/security.lib.php @@ -447,6 +447,10 @@ function restrictedArea(User $user, $features, $object = 0, $tableandshare = '', $tableandshare = 'paiementcharge'; $parentfortableentity = 'fk_charge@chargesociales'; } + if ($features == 'evaluation') { + $features = 'hrm'; + $feature2 = 'evaluation'; + } //print $features.' - '.$tableandshare.' - '.$feature2.' - '.$dbt_select."\n"; @@ -908,7 +912,7 @@ function checkUserAccessToObject($user, array $featuresarray, $object = 0, $tabl $checkparentsoc = array('agenda', 'contact', 'contrat'); // Test on entity + link to third party on field $dbt_keyfield. Allowed if link is empty (Ex: contacts...). $checkproject = array('projet', 'project'); // Test for project object $checktask = array('projet_task'); // Test for task object - $checkhierarchy = array('expensereport', 'holiday'); // check permission among the hierarchy of user + $checkhierarchy = array('expensereport', 'holiday', 'hrm'); // check permission among the hierarchy of user $checkuser = array('bookmark'); // check permission among the fk_user (must be myself or null) $nocheck = array('barcode', 'stock'); // No test @@ -1135,6 +1139,20 @@ function checkUserAccessToObject($user, array $featuresarray, $object = 0, $tabl } } } + if ($feature == 'hrm' && in_array('evaluation', $feature2)) { + $useridtocheck = $object->fk_user; + + if ($user->hasRight('hrm', 'evaluation', 'readall')) { + // the user can view evaluations for anyone + return true; + } + if (!$user->hasRight('hrm', 'evaluation', 'read')) { + // the user can't view any evaluations + return false; + } + // the user can only their own evaluations or their subordinates' + return in_array($useridtocheck, $childids); + } } // For some object, we also have to check it is public or owned by user diff --git a/htdocs/hrm/evaluation_card.php b/htdocs/hrm/evaluation_card.php index 5cddbd523be..b043125e2f2 100644 --- a/htdocs/hrm/evaluation_card.php +++ b/htdocs/hrm/evaluation_card.php @@ -92,8 +92,8 @@ $upload_dir = $conf->hrm->multidir_output[isset($object->entity) ? $object->enti // Security check (enable the most restrictive one) //if ($user->socid > 0) accessforbidden(); //if ($user->socid > 0) $socid = $user->socid; -//$isdraft = (($object->status == $object::STATUS_DRAFT) ? 1 : 0); -//restrictedArea($user, $object->element, $object->id, $object->table_element, '', 'fk_soc', 'rowid', $isdraft); +$isdraft = ($object->status == Evaluation::STATUS_DRAFT) ? 1 : 0; +restrictedArea($user, $object->element, $object, $object->table_element, '', 'fk_soc', 'rowid', $isdraft); if (!isModEnabled("hrm")) { accessforbidden(); } From 8d038f32bc18f9d5653548e22d056b66ab6ad240 Mon Sep 17 00:00:00 2001 From: iLLixM <162678117+iLLixM@users.noreply.github.com> Date: Thu, 20 Nov 2025 01:49:24 +0100 Subject: [PATCH 15/18] FIX #33148 - partial payments are taken into account in EPC QR codes (#36338) When generating EPC QR codes on an invoice, any partial payments already made are taken into account. The remaining balance (the value of "Remaining unpaid") is then entered as the amount in the EPC QR code. --- htdocs/core/class/commoninvoice.class.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/htdocs/core/class/commoninvoice.class.php b/htdocs/core/class/commoninvoice.class.php index abc8f7e2a49..d1adb0d9fce 100644 --- a/htdocs/core/class/commoninvoice.class.php +++ b/htdocs/core/class/commoninvoice.class.php @@ -1660,8 +1660,17 @@ abstract class CommonInvoice extends CommonObject { global $mysoc; - // Convert total_ttc to a string with 2 decimal places - $totalTTCString = number_format($this->total_ttc, 2, '.', ''); + // Get the amount to pay + if (method_exists($this, 'getRemainToPay')) { + // Get remaining amount to pay + $amount_to_pay = $this->getRemainToPay(); + } else { + // Use Total amount with taxes + $amount_to_pay = $this->total_ttc; + } + + // Ensure numeric formatting for EPC QR code + $amount_to_pay = price2num($amount_to_pay, 'MT'); // Initialize an array to hold the lines of the QR code $lines = array(); @@ -1689,7 +1698,7 @@ abstract class CommonInvoice extends CommonObject } // Add the amount and reference - $lines[] = 'EUR' . $totalTTCString; // Amount (optional) + $lines[] = 'EUR' . $amount_to_pay; // Amount (optional) $lines[] = ''; // Purpose (optional) $lines[] = ''; // Payment reference (optional) $lines[] = $this->ref; // Remittance Information (optional) From fd5c9b05626652325d71efa54a109d166b331db2 Mon Sep 17 00:00:00 2001 From: Eric Seigne Date: Thu, 20 Nov 2025 09:46:59 +0100 Subject: [PATCH 16/18] update github actions for race conditions of author/reviewer --- .github/workflows/pr-18.yaml | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/.github/workflows/pr-18.yaml b/.github/workflows/pr-18.yaml index 393e35013c0..6e5510dcb46 100644 --- a/.github/workflows/pr-18.yaml +++ b/.github/workflows/pr-18.yaml @@ -16,11 +16,10 @@ on: branches: - "18.0" - permissions: pull-requests: write issues: write - + jobs: assign-and-label-v18: runs-on: ubuntu-latest @@ -45,20 +44,28 @@ jobs: url: ${{ github.event.pull_request.html_url }} run: | echo "env.url=${{env.url}}" - gh pr edit "${{env.url}}" --add-label "Issue for v18 maintenance Team" + gh pr edit "${{env.url}}" --add-label "Issue for v18 maintenance Team" - - name: Assign reviewer 1 + - name: Set reviewers except PR author + id: set-reviewers + run: | + # Liste des reviewers à ajuster selon équipe + REVIEWERS=("lvessiller-opendsi" "rycks") + AUTHOR="${{ github.event.pull_request.user.login }}" + FINAL_REVIEWERS=() + for reviewer in "${REVIEWERS[@]}"; do + if [ "$reviewer" != "$AUTHOR" ]; then + FINAL_REVIEWERS+=("$reviewer") + fi + done + echo "reviewers=$(IFS=, ; echo "${FINAL_REVIEWERS[*]}")" >> $GITHUB_OUTPUT + + - name: Assign reviewers + if: steps.set-reviewers.outputs.reviewers != '' env: GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} url: ${{ github.event.pull_request.html_url }} + reviewers: ${{ steps.set-reviewers.outputs.reviewers }} run: | - echo "env.url=${{env.url}}" - gh pr edit "${{env.url}}" --add-reviewer lvessiller-opendsi - - - name: Assign reviewer 2 - env: - GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} - url: ${{ github.event.pull_request.html_url }} - run: | - echo "env.url=${{env.url}}" - gh pr edit "${{env.url}}" --add-reviewer rycks + echo "Assigning reviewers: ${{env.reviewers}}" + gh pr edit "${{env.url}}" --add-reviewer "${{env.reviewers}}" From f1f7ea93b04c87b7c0af5b5a8655d5f17abfefbd Mon Sep 17 00:00:00 2001 From: Eric Seigne Date: Thu, 20 Nov 2025 10:09:29 +0100 Subject: [PATCH 17/18] fix assign-and-label-v18 --- .github/workflows/pr-18.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-18.yaml b/.github/workflows/pr-18.yaml index 6e5510dcb46..d12d7002844 100644 --- a/.github/workflows/pr-18.yaml +++ b/.github/workflows/pr-18.yaml @@ -44,7 +44,7 @@ jobs: url: ${{ github.event.pull_request.html_url }} run: | echo "env.url=${{env.url}}" - gh pr edit "${{env.url}}" --add-label "Issue for v18 maintenance Team" + gh pr edit "${{env.url}}" --add-label "Issue for v18 maintenance Team" - name: Set reviewers except PR author id: set-reviewers From ac4820b4cb88b574dd88843197a31e18c50fb16f Mon Sep 17 00:00:00 2001 From: noec764 <58433943+noec764@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:34:06 +0100 Subject: [PATCH 18/18] FIX: TakePOS Missing Thirdparty Id when getting more products (#36341) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Noé --- htdocs/takepos/index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/takepos/index.php b/htdocs/takepos/index.php index 373948c64fa..fe9c34946a4 100644 --- a/htdocs/takepos/index.php +++ b/htdocs/takepos/index.php @@ -438,7 +438,7 @@ function MoreProducts(moreorless) { } var offset = * pageproducts; // Only show products for sale (tosell=1) - $.getJSON('/takepos/ajax/ajax.php?action=getProducts&token=&category='+currentcat+'&tosell=1&limit='+limit+'&offset='+offset, function(data) { + $.getJSON('/takepos/ajax/ajax.php?action=getProducts&token=&thirdpartyid=' + jQuery('#thirdpartyid').val() + '&category='+currentcat+'&tosell=1&limit='+limit+'&offset='+offset, function(data) { console.log("Call ajax.php (in MoreProducts) to get Products of category "+currentcat); if (typeof (data[0]) == "undefined" && moreorless=="more"){ // Return if no more pages