diff --git a/dev/translation/sanity_check_trans_missing_unused.sh b/dev/translation/sanity_check_trans_missing_unused.sh index 5cc1fe07c60..2ae2f477818 100755 --- a/dev/translation/sanity_check_trans_missing_unused.sh +++ b/dev/translation/sanity_check_trans_missing_unused.sh @@ -63,9 +63,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 8f4c5df7991..b5a5477c240 100644 --- a/htdocs/adherents/list.php +++ b/htdocs/adherents/list.php @@ -375,7 +375,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/categories/class/api_categories.class.php b/htdocs/categories/class/api_categories.class.php index 4a9f63b7694..713ec487b77 100644 --- a/htdocs/categories/class/api_categories.class.php +++ b/htdocs/categories/class/api_categories.class.php @@ -30,6 +30,7 @@ require_once DOL_DOCUMENT_ROOT.'/product/class/api_products.class.php'; require_once DOL_DOCUMENT_ROOT.'/societe/class/api_contacts.class.php'; require_once DOL_DOCUMENT_ROOT.'/societe/class/api_thirdparties.class.php'; require_once DOL_DOCUMENT_ROOT.'/projet/class/api_projects.class.php'; +require_once DOL_DOCUMENT_ROOT.'/ticket/class/api_tickets.class.php'; /** * API class for categories @@ -842,6 +843,8 @@ class Categories extends DolibarrApi $objects_api = new Contacts(); } elseif ($type == 'project') { $objects_api = new Projects(); + } elseif ($type == 'ticket') { + $objects_api = new Tickets(); } if (is_object($objects_api)) { diff --git a/htdocs/compta/facture/class/facture.class.php b/htdocs/compta/facture/class/facture.class.php index 985bf604ded..05278b7355c 100644 --- a/htdocs/compta/facture/class/facture.class.php +++ b/htdocs/compta/facture/class/facture.class.php @@ -1283,6 +1283,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 70d8214c8cf..1c53f2682b7 100644 --- a/htdocs/compta/prelevement/class/bonprelevement.class.php +++ b/htdocs/compta/prelevement/class/bonprelevement.class.php @@ -2254,8 +2254,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 1ebe5c9ca5e..fa316602057 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 b7f6c385421..6e4601bd164 100644 --- a/htdocs/core/class/extrafields.class.php +++ b/htdocs/core/class/extrafields.class.php @@ -1797,9 +1797,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 92f3e29bbeb..726db9c54e8 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 ?? $conf->entity].'/'.dol_sanitizeFileName($object->ref); + $upload_dir = $conf->holiday->multidir_output[$object->entity ?? $conf->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] = dolBuildUrl(DOL_URL_ROOT.'/holiday/document.php', ['id' => $object->id]); diff --git a/htdocs/core/lib/payments.lib.php b/htdocs/core/lib/payments.lib.php index 053b0d85e18..d7b0ef3587a 100644 --- a/htdocs/core/lib/payments.lib.php +++ b/htdocs/core/lib/payments.lib.php @@ -287,7 +287,7 @@ function getOnlinePaymentUrl($mode, $type, $ref = '', $amount = 0, $freetag = 'y } if ($type == 'free') { - $out = $urltouse.'/public/payment/newpayment.php?amount='.($mode ? '' : '').price2num($amount, 'MT').($mode ? '' : '').'&tag='.($mode ? '' : '').$freetag.($mode ? '' : ''); + $out = $urltouse.'/public/payment/newpayment.php?amount='.($mode ? '' : '').price2num($amount, 'MT').($mode ? '' : '').'&tag='.($mode ? '' : '').urlencode($freetag).($mode ? '' : ''); if (getDolGlobalString('PAYMENT_SECURITY_TOKEN')) { if (!getDolGlobalString('PAYMENT_SECURITY_TOKEN_UNIQUE')) { $out .= '&securekey='.urlencode(getDolGlobalString('PAYMENT_SECURITY_TOKEN')); 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 7266d33ab0f..e4a81d9713c 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/core/tpl/massactions_pre.tpl.php b/htdocs/core/tpl/massactions_pre.tpl.php index 2e96ab1d391..852a7aeb9e3 100644 --- a/htdocs/core/tpl/massactions_pre.tpl.php +++ b/htdocs/core/tpl/massactions_pre.tpl.php @@ -351,7 +351,7 @@ if ($massaction == 'presend') { // Tableau des parameters complementaires du post $formmail->param['action'] = $action; $formmail->param['models'] = $modelmail; // the filter to know which kind of template emails to show. 'none' means no template suggested. - $formmail->param['models_id'] = GETPOSTINT('modelmailselected') ? GETPOSTINT('modelmailselected') : '-1'; + $formmail->param['models_id'] = GETPOSTINT('modelmailselected') ? GETPOSTINT('modelmailselected') : ''; $formmail->param['id'] = implode(',', $arrayofselected); // $formmail->param['returnurl']=$_SERVER["PHP_SELF"].'?id='.$object->id; if (getDolGlobalString('MAILING_LIMIT_SENDBYWEB') && count($listofselectedrecipientobjid) > $conf->global->MAILING_LIMIT_SENDBYWEB) { diff --git a/htdocs/holiday/card.php b/htdocs/holiday/card.php index 607098476c7..5c109ea60e2 100644 --- a/htdocs/holiday/card.php +++ b/htdocs/holiday/card.php @@ -537,6 +537,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 70f8fc0937d..edf5c66ee0f 100644 --- a/htdocs/langs/en_US/errors.lang +++ b/htdocs/langs/en_US/errors.lang @@ -377,6 +377,7 @@ ErrorMaxDecimalsShownTooLowComparedToUnitOrTotal=Value for 'Max. decimals for pr ErrorOnlyDraftStatusCanBeDeletedInMassAction=Only elements in draft status can be deleted in mass action ErrorLDAPFunctionsAreDisabledOnThisPHP=LDAP functions are disabled on this PHP. ErrorFailedToGetTokenFromClientIdAndSecret=Failed to get a %s token from client ID and client secret +ErrorTooManyLinksIntoHTMLString=The HTML content contains too many links or images. Reduce the number or raise MAIN_SECURITY_MAX_IMG_IN_HTML_CONTENT. # Warnings WarningParamUploadMaxFileSizeHigherThanPostMaxSize=Your PHP parameter upload_max_filesize (%s) is higher than PHP parameter post_max_size (%s). This is not a consistent setup. diff --git a/htdocs/projet/tasks/time.php b/htdocs/projet/tasks/time.php index ab00ad72cd0..419229211e0 100644 --- a/htdocs/projet/tasks/time.php +++ b/htdocs/projet/tasks/time.php @@ -53,6 +53,7 @@ require_once DOL_DOCUMENT_ROOT . '/core/lib/date.lib.php'; require_once DOL_DOCUMENT_ROOT . '/core/class/html.formother.class.php'; require_once DOL_DOCUMENT_ROOT . '/core/class/html.formprojet.class.php'; require_once DOL_DOCUMENT_ROOT . '/core/class/html.formintervention.class.php'; +require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php'; // Load translation files required by the page $langsLoad = array('projects', 'bills', 'orders', 'companies'); @@ -575,8 +576,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'); @@ -675,7 +679,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 07c08496451..97f189b37f7 100644 --- a/htdocs/ticket/class/ticket.class.php +++ b/htdocs/ticket/class/ticket.class.php @@ -413,7 +413,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; } @@ -1048,7 +1050,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 4c778accc2c..313684e9ed1 100644 --- a/htdocs/user/card.php +++ b/htdocs/user/card.php @@ -318,6 +318,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'); @@ -472,7 +482,7 @@ if (empty($reshook)) { $object->pass = GETPOST("password", 'password'); } if ($permissiontoeditpasswordandsee || $user->hasRight("api", "apikey", "generate")) { - $object->api_key = (GETPOSTISSET("api_key", 'alphanohtml')) ? GETPOST("api_key", 'alphanohtml') : $object->api_key; + $object->api_key = (GETPOSTISSET("api_key") ? GETPOST("api_key", 'alphanohtml') : $object->api_key); } if (!empty($user->admin) && $user->id != $id) { // admin flag can only be set/unset by an admin user and not four ourself @@ -503,6 +513,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 368a09a724e..2cea987318c 100644 --- a/htdocs/user/class/api_users.class.php +++ b/htdocs/user/class/api_users.class.php @@ -884,6 +884,12 @@ class Users extends DolibarrApi throw new RestException(404, 'Usergroup 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->_cleanUserGroup($group_static); } diff --git a/htdocs/variants/class/ProductCombination.class.php b/htdocs/variants/class/ProductCombination.class.php index c00439241e1..e5bcc5aceab 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; }