diff --git a/dev/translation/sanity_check_trans_missing_unused.sh b/dev/translation/sanity_check_trans_missing_unused.sh index 21c09ec8ff3..a04a11208fe 100755 --- a/dev/translation/sanity_check_trans_missing_unused.sh +++ b/dev/translation/sanity_check_trans_missing_unused.sh @@ -62,9 +62,8 @@ exit_code=0 grep --no-filename -r -oP -- '^([^#=]+?)(?=\s*=.*)' "${LANG_DIR}" \ | grep -x -v -F -f "${EXCLUDE_KEYS_SRC_FILE}" \ | sort > "${AVAILABLE_FILE_NODEDUP}" -sort -u \ - < "${AVAILABLE_FILE_NODEDUP}" \ - > "${AVAILABLE_FILE}" +# sort -u is buggued in ubutnu 26.04. The line StatusProspect-1 and StatusProspect1 are treated as similar lines Replaced with "uniq". +uniq < "${AVAILABLE_FILE_NODEDUP}" > "${AVAILABLE_FILE}" # Combine strings found in sources with pre-determined dynamic string values. diff --git a/htdocs/adherents/list.php b/htdocs/adherents/list.php index ee4aff5a7d3..a28fc0bc934 100644 --- a/htdocs/adherents/list.php +++ b/htdocs/adherents/list.php @@ -378,7 +378,19 @@ if (empty($reshook)) { foreach ($toselect as $id) { $res = $tmpmember->fetch($id); if ($res > 0) { - $result = $tmpmember->subscription($now, (float) $amount, 0, '', '', '', '', '', $datesubend); + // Mirror the single subscription form (adherents/subscription.php): if no + // global EOM/EOY override was set, compute the end date from the member's + // AdherentType duration_value/duration_unit so a 6-month type does not + // silently fall back to the 1-year default in Adherent::subscription() + $datesubendthismember = $datesubend; + if (!$datesubendthismember && $tmpmember->typeid > 0) { + if ($adht->fetch($tmpmember->typeid) > 0) { + $delay = !empty($adht->duration_value) ? $adht->duration_value : 1; + $delayunit = !empty($adht->duration_unit) ? $adht->duration_unit : 'y'; + $datesubendthismember = dol_time_plus_duree(dol_time_plus_duree($now, $delay, $delayunit), -1, 'd'); + } + } + $result = $tmpmember->subscription($now, (float) $amount, 0, '', '', '', '', '', $datesubendthismember); if ($result < 0) { $error++; } else { diff --git a/htdocs/compta/facture/class/facture.class.php b/htdocs/compta/facture/class/facture.class.php index d1b6067515e..78c454fed8e 100644 --- a/htdocs/compta/facture/class/facture.class.php +++ b/htdocs/compta/facture/class/facture.class.php @@ -1270,6 +1270,12 @@ class Facture extends CommonInvoice $this->db->begin(); $object->fetch($fromid); + // fetch() clears $object->thirdparty, but the subsequent $object->create() path + // reaches addLine() -> getLocalTaxesFromRate() -> get_localtax() and uses the + // buyer thirdparty to decide whether IRPF/localtax2 applies. Without this fetch, + // get_localtax() sees $thirdparty_buyer = null, returns 0 and the cloned lines + // lose their localtax2_tx (see issue #29052). + $object->fetch_thirdparty(); // Load source object $objFrom = clone $object; diff --git a/htdocs/compta/prelevement/class/bonprelevement.class.php b/htdocs/compta/prelevement/class/bonprelevement.class.php index d2f0f904197..c368de656f1 100644 --- a/htdocs/compta/prelevement/class/bonprelevement.class.php +++ b/htdocs/compta/prelevement/class/bonprelevement.class.php @@ -2187,8 +2187,8 @@ class BonPrelevement extends CommonObject $pre = substr(dol_string_nospecial(dol_string_unaccent($langs->transnoentitiesnoconv('RUM'))), 0, 3); // Must always be on 3 char ('RUM' or 'UMR'. This is a protection against bad translation) - // 3 char + '-' + 12 + '-' + id + '-' + code Must be lower than 32. - return $pre . '-' . dol_print_date($row_datec, 'dayhourlogsmall') . '-' . dol_trunc($row_drum . ($row_code_client ? '-' . $row_code_client : ''), 13, 'right', 'UTF-8', 1); + // 3 char + '-' + 10 (yymmddHHMM) + '-' + id + '-' + code. Must be under 32 (SEPA char limit for MndtId is however 35). + return $pre . '-' . dol_print_date($row_datec, 'dayhourlogsmall') . '-' . dol_trunc($row_drum . ($row_code_client ? '-' . $row_code_client : ''), 17, 'right', 'UTF-8', 1); } diff --git a/htdocs/core/actions_massactions.inc.php b/htdocs/core/actions_massactions.inc.php index c4f391edc13..27d7f53ae09 100644 --- a/htdocs/core/actions_massactions.inc.php +++ b/htdocs/core/actions_massactions.inc.php @@ -324,19 +324,23 @@ if (!$error && $massaction == 'confirm_presend') { foreach ($listofobjectref[$thirdpartyid] as $objectid => $objectobj) { //var_dump($thirdpartyid.' - '.$objectid.' - '.$objectobj->statut); - if ($objectclass == 'Propal' && $objectobj->status == Propal::STATUS_DRAFT) { + // Honour the same per-object hidden constants used by the "Send by email" + // button on the card pages (comm/propal/card.php, commande/card.php, + // compta/facture/card.php) so the behaviour is consistent between the + // single-object action and the mass action. + if ($objectclass == 'Propal' && $objectobj->status == Propal::STATUS_DRAFT && !getDolGlobalString('PROPOSAL_SENDBYEMAIL_FOR_ALL_STATUS')) { $langs->load("errors"); $nbignored++; $resaction .= '
'.$langs->trans('ErrorOnlyProposalNotDraftCanBeSentInMassAction', $objectobj->ref).'

'; continue; // Payment done or started or canceled } - if ($objectclass == 'Commande' && $objectobj->status == Commande::STATUS_DRAFT) { + if ($objectclass == 'Commande' && $objectobj->status == Commande::STATUS_DRAFT && !getDolGlobalString('COMMANDE_SENDBYEMAIL_FOR_ALL_STATUS')) { $langs->load("errors"); $nbignored++; $resaction .= '
'.$langs->trans('ErrorOnlyOrderNotDraftCanBeSentInMassAction', $objectobj->ref).'

'; continue; } - if ($objectclass == 'Facture' && $objectobj->status == Facture::STATUS_DRAFT) { + if ($objectclass == 'Facture' && $objectobj->status == Facture::STATUS_DRAFT && !getDolGlobalString('FACTURE_SENDBYEMAIL_FOR_ALL_STATUS')) { $langs->load("errors"); $nbignored++; $resaction .= '
'.$langs->trans('ErrorOnlyInvoiceValidatedCanBeSentInMassAction', $objectobj->ref).'

'; diff --git a/htdocs/core/class/extrafields.class.php b/htdocs/core/class/extrafields.class.php index cb0a231a041..d834053686c 100644 --- a/htdocs/core/class/extrafields.class.php +++ b/htdocs/core/class/extrafields.class.php @@ -1745,9 +1745,9 @@ class ExtraFields $InfoFieldList = array_merge($InfoFieldList, explode(':', $tmpafter)); } - // Fix better compatibility with some old extrafield syntax filter "(field=123)" + // Fix better compatibility with some old extrafield syntax filter "(field_name=123)" $reg = array(); - if (preg_match('/^\(?([a-z0-9]+)([=<>]+)(\d+)\)?$/i', $InfoFieldList[4], $reg)) { + if (preg_match('/^\(?([a-z0-9_]+)([=<>]+)(\d+)\)?$/i', $InfoFieldList[4], $reg)) { $InfoFieldList[4] = '('.$reg[1].':'.$reg[2].':'.$reg[3].')'; } diff --git a/htdocs/core/lib/holiday.lib.php b/htdocs/core/lib/holiday.lib.php index e25ee0b5902..da28712d029 100644 --- a/htdocs/core/lib/holiday.lib.php +++ b/htdocs/core/lib/holiday.lib.php @@ -44,7 +44,7 @@ function holiday_prepare_head($object) // Attachments require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; require_once DOL_DOCUMENT_ROOT.'/core/class/link.class.php'; - $upload_dir = $conf->holiday->multidir_output[$object->entity].'/'.dol_sanitizeFileName($object->ref); + $upload_dir = $conf->holiday->multidir_output[$object->entity].'/'.get_exdir(0, 0, 0, 1, $object, 'holiday'); $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.'/holiday/document.php?id='.$object->id; diff --git a/htdocs/core/modules/hrm/doc/pdf_standard_evaluation.modules.php b/htdocs/core/modules/hrm/doc/pdf_standard_evaluation.modules.php index 9ab6c30daab..4f069273c64 100644 --- a/htdocs/core/modules/hrm/doc/pdf_standard_evaluation.modules.php +++ b/htdocs/core/modules/hrm/doc/pdf_standard_evaluation.modules.php @@ -195,14 +195,14 @@ class pdf_standard_evaluation extends ModelePDFEvaluation if ($conf->hrm->dir_output) { // Definition of $dir and $file + $entity = isset($object->entity) ? $object->entity : 1; + $basedir = empty($conf->hrm->multidir_output[$entity]) ? $conf->hrm->dir_output : $conf->hrm->multidir_output[$entity]; if ($object->specimen) { - //$dir = $conf->hrm->dir_output; - $dir = $conf->hrm->multidir_output[isset($object->entity) ? $object->entity : 1].'/evaluation'; + $dir = $basedir.'/evaluation'; $file = $dir."/SPECIMEN.pdf"; } else { $objectref = dol_sanitizeFileName($object->ref); - //$dir = $conf->hrm->dir_output."/".$objectref; - $dir = $conf->hrm->multidir_output[isset($object->entity) ? $object->entity : 1].'/evaluation'."/".$objectref; + $dir = $basedir.'/evaluation'."/".$objectref; $file = $dir."/".$objectref.".pdf"; } diff --git a/htdocs/holiday/card.php b/htdocs/holiday/card.php index dab043c9ee2..f500be4fdba 100644 --- a/htdocs/holiday/card.php +++ b/htdocs/holiday/card.php @@ -529,6 +529,7 @@ if (empty($reshook)) { } } + // option to notify the validator if the balance is less than the request // (only for leave types that actually use the balance counter) if (!getDolGlobalString('HOLIDAY_HIDE_APPROVER_ABOUT_NEGATIVE_BALANCE')) { diff --git a/htdocs/langs/en_US/errors.lang b/htdocs/langs/en_US/errors.lang index e9286e843f1..012bd8be0ec 100644 --- a/htdocs/langs/en_US/errors.lang +++ b/htdocs/langs/en_US/errors.lang @@ -446,3 +446,4 @@ ThisIdNotDefined=Id not defined OperNotDefined=Payment method not defined EmptyMessageNotAllowedError=Empty message is not allowed SomeShipmentExists=Error, there is some shipment linked to the order. Deletion refused. +ErrorTooManyLinksIntoHTMLString=The HTML content contains too many links or images. Reduce the number or raise MAIN_SECURITY_MAX_IMG_IN_HTML_CONTENT. diff --git a/htdocs/projet/tasks/time.php b/htdocs/projet/tasks/time.php index d1c8e307076..fd7afbfb325 100644 --- a/htdocs/projet/tasks/time.php +++ b/htdocs/projet/tasks/time.php @@ -37,6 +37,7 @@ require '../../main.inc.php'; require_once DOL_DOCUMENT_ROOT . '/projet/class/project.class.php'; require_once DOL_DOCUMENT_ROOT . '/projet/class/task.class.php'; require_once DOL_DOCUMENT_ROOT . '/compta/facture/class/facture.class.php'; +require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php'; require_once DOL_DOCUMENT_ROOT . '/core/lib/project.lib.php'; require_once DOL_DOCUMENT_ROOT . '/core/lib/date.lib.php'; require_once DOL_DOCUMENT_ROOT . '/core/class/html.formother.class.php'; @@ -568,8 +569,11 @@ if ($action == 'confirm_generateinvoice' && $user->hasRight('facture', 'creer')) $idprodline = $fk_product; } - // Add lines - $lineid = $tmpinvoice->addline($langs->trans("TimeSpentForInvoice", $username) . ' : ' . $qtyhourtext, $pu_htline, round($qtyhour / $prodDurationHours, 2), $txtvaline, $localtax1line, $localtax2line, ($idprodline > 0 ? $idprodline : 0), (float) $remiseproject); + // Add lines. Pass type=1 (service) explicitly so the invoice line is tagged + // as a service even when no product is bound to the time entry. Otherwise + // the default $type=0 leaks through and the PDF labels the operation as + // "Delivery of goods" instead of "Provision of services" (issue #34571). + $lineid = $tmpinvoice->addline($langs->trans("TimeSpentForInvoice", $username) . ' : ' . $qtyhourtext, $pu_htline, round($qtyhour / $prodDurationHours, 2), $txtvaline, $localtax1line, $localtax2line, ($idprodline > 0 ? $idprodline : 0), (float) $remiseproject, '', '', 0, 0, '', 'HT', 0, Product::TYPE_SERVICE); if ($lineid < 0) { $error++; setEventMessages(null, $tmpinvoice->errors, 'errors'); @@ -668,7 +672,11 @@ if ($action == 'confirm_generateinvoice' && $user->hasRight('facture', 'creer')) } $idprodline = $value['fk_product']; } - $lineid = $tmpinvoice->addline($value['note'], $pu_htline, round($qtyhour / $prodDurationHours, 2), $txtvaline, $localtax1line, $localtax2line, ($idprodline > 0 ? $idprodline : 0), (float) $remiseproject); + // Pass type=1 (service) explicitly so the invoice line is tagged as a service + // even when no product is bound to the time entry. Otherwise the default + // $type=0 leaks through and the PDF labels the operation as + // "Delivery of goods" instead of "Provision of services" (issue #34571). + $lineid = $tmpinvoice->addline($value['note'], $pu_htline, round($qtyhour / $prodDurationHours, 2), $txtvaline, $localtax1line, $localtax2line, ($idprodline > 0 ? $idprodline : 0), (float) $remiseproject, '', '', 0, 0, '', 'HT', 0, Product::TYPE_SERVICE); if ($lineid < 0) { $error++; setEventMessages(null, $tmpinvoice->errors, 'errors'); diff --git a/htdocs/ticket/class/ticket.class.php b/htdocs/ticket/class/ticket.class.php index 7438f54649c..cdd544b6f7f 100644 --- a/htdocs/ticket/class/ticket.class.php +++ b/htdocs/ticket/class/ticket.class.php @@ -416,7 +416,9 @@ class Ticket extends CommonObject if (isset($this->message)) { $this->message = trim($this->message); if (dol_strlen($this->message) > 65000) { - $this->errors[] = 'ErrorFieldTooLong'; + global $langs; + $langs->loadLangs(array('errors', 'ticket')); + $this->errors[] = $langs->trans('ErrorFieldTooLong', $langs->transnoentitiesnoconv('InitialMessage')); dol_syslog(get_class($this).'::create error -1 message too long', LOG_ERR); $result = -1; } @@ -1039,7 +1041,9 @@ class Ticket extends CommonObject if (isset($this->message)) { $this->message = trim($this->message); if (dol_strlen($this->message) > 65000) { - $this->errors[] = 'ErrorFieldTooLong'; + global $langs; + $langs->loadLangs(array('errors', 'ticket')); + $this->errors[] = $langs->trans('ErrorFieldTooLong', $langs->transnoentitiesnoconv('InitialMessage')); dol_syslog(get_class($this).'::update error -1 message too long', LOG_ERR); return -1; } diff --git a/htdocs/user/card.php b/htdocs/user/card.php index 3cd66bb721c..dab96c97bd1 100644 --- a/htdocs/user/card.php +++ b/htdocs/user/card.php @@ -317,6 +317,16 @@ if (empty($reshook)) { $object->email = preg_replace('/\s+/', '', GETPOST("email", 'alphanohtml')); $object->job = GETPOST("job", 'alphanohtml'); $object->signature = GETPOST("signature", 'restricthtml'); + // restricthtml may swap the value with the literal 'ErrorTooManyLinksIntoHTMLString' + // when the html exceeds MAIN_SECURITY_MAX_IMG_IN_HTML_CONTENT (see issue #27987). + // Refuse the save so the literal does not end up persisted and later sent as an + // email body to customers. + if ($object->signature === 'ErrorTooManyLinksIntoHTMLString') { + $error++; + $langs->load("errors"); + setEventMessages($langs->trans('ErrorTooManyLinksIntoHTMLString'), null, 'errors'); + $action = 'create'; + } $object->accountancy_code = GETPOST("accountancy_code", 'alphanohtml'); $object->note_public = GETPOST("note_public", 'restricthtml'); $object->note_private = GETPOST("note_private", 'restricthtml'); @@ -500,6 +510,16 @@ if (empty($reshook)) { $object->email = preg_replace('/\s+/', '', GETPOST("email", 'alphanohtml')); $object->job = GETPOST("job", 'alphanohtml'); $object->signature = GETPOST("signature", 'restricthtml'); + // restricthtml may swap the value with the literal 'ErrorTooManyLinksIntoHTMLString' + // when the html exceeds MAIN_SECURITY_MAX_IMG_IN_HTML_CONTENT (see issue #27987). + // Refuse the save so the literal does not end up persisted and later sent as an + // email body to customers. + if ($object->signature === 'ErrorTooManyLinksIntoHTMLString') { + $error++; + $langs->load("errors"); + setEventMessages($langs->trans('ErrorTooManyLinksIntoHTMLString'), null, 'errors'); + $action = 'edit'; + } $object->accountancy_code = GETPOST("accountancy_code", 'alphanohtml'); $object->openid = GETPOST("openid", 'alphanohtml'); $object->fk_user = GETPOSTINT("fk_user") > 0 ? GETPOSTINT("fk_user") : 0; diff --git a/htdocs/user/class/api_users.class.php b/htdocs/user/class/api_users.class.php index 525616a6733..18f287062f5 100644 --- a/htdocs/user/class/api_users.class.php +++ b/htdocs/user/class/api_users.class.php @@ -723,6 +723,12 @@ class Users extends DolibarrApi throw new RestException(404, 'Group not found'); } + if ($load_members > 0 && is_array($group_static->members) && count($group_static->members) > 0) { + foreach ($group_static->members as &$member) { + $member = $this->_cleanObjectDatas($member); + } + } + return $this->_cleanObjectDatas($group_static); } diff --git a/htdocs/variants/class/ProductCombination.class.php b/htdocs/variants/class/ProductCombination.class.php index 25226a0746c..c2fc3dab1fe 100644 --- a/htdocs/variants/class/ProductCombination.class.php +++ b/htdocs/variants/class/ProductCombination.class.php @@ -430,7 +430,14 @@ class ProductCombination $parent = new Product($this->db); $parent->fetch($this->fk_product_parent); - $this->updateProperties($parent, $user); + // Propagate failure of updateProperties (otherwise an update where the variant + // percentage drives the new price below the parent product's price_min returns + // success but leaves the variant product at the price=0 it had right after + // createProductCombination, see issue #32372). + $result = $this->updateProperties($parent, $user); + if ($result < 0) { + return $result; + } return 1; }