dolibarr 24.0.2
actions_massactions.inc.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2015-2025 Laurent Destailleur <eldy@users.sourceforge.net>
3 * Copyright (C) 2018-2021 Nicolas ZABOURI <info@inovea-conseil.com>
4 * Copyright (C) 2018 Juanjo Menent <jmenent@2byte.es>
5 * Copyright (C) 2019 Ferran Marcet <fmarcet@2byte.es>
6 * Copyright (C) 2019-2026 Frédéric France <frederic.france@free.fr>
7 * Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 3 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 * or see https://www.gnu.org/
22 */
23
30// $massaction must be defined
31// $objectclass and $objectlabel must be defined
32// $parameters, $object, $action must be defined for the hook.
33
34// $permissiontoread, $permissiontoadd, $permissiontodelete, $permissiontoclose may be defined
35// $uploaddir may be defined (example to $conf->project->dir_output."/";)
36// $hidedetails, $hidedesc, $hideref and $moreparams may have been set or not.
37// $toselect may be defined
38// $diroutputmassaction may be defined
39// $confirm
76'
77@phan-var-force int $error
78@phan-var-force ?string $permissiontoadd
79@phan-var-force ?string $permissiontoread
80@phan-var-force ?string $permissiontodelete
81@phan-var-force ?string $permissiontoclose
82@phan-var-force ?string $permissiontoapprove
83@phan-var-force ?int[] $toselect
84@phan-var-force ?string $diroutputmassaction
85@phan-var-force ?string $objectlabel
86@phan-var-force ?string $option
87@phan-var-force int $deliveryreceipt
88@phan-var-force string $month
89@phan-var-force string $year
90@phan-var-force ?array<string,mixed> $moreparams
91@phan-var-force ?string $search_status
92@phan-var-force ?int $hidedetails
93@phan-var-force ?int $hidedesc
94@phan-var-force ?int $hideref
95@phan-var-force string $confirm
96';
97
98
99// Protection
100if (empty($objectclass) || empty($uploaddir)) {
101 dol_print_error(null, 'include of actions_massactions.inc.php is done but var $objectclass or $uploaddir was not defined');
102 exit;
103}
104if (empty($massaction)) {
105 $massaction = '';
106}
107
108// Note: list of strings for objectclass could be extended to accepted/expected classes
109'
110@phan-var-force "CommonObject"|"CommandeFournisseur"|"ConferenceOrBoothAttendee"|"Contrat"|"Contact"|"Expedition"|"ExpenseReport"|"Facture"|"FactureFournisseur"|"Fichinter"|"Holiday"|"Partnership"|"Project"|"Propal"|"Societe"|"SupplierProposal" $objectclass
111@phan-var-force string $massaction
112@phan-var-force string $uploaddir
113';
114
116$TMsg = array();
117
118// For backward compatibility
119if (!empty($permtoread) && empty($permissiontoread)) {
120 $permissiontoread = $permtoread;
121}
122if (!empty($permtocreate) && empty($permissiontoadd)) {
123 $permissiontoadd = $permtocreate;
124}
125if (!empty($permtodelete) && empty($permissiontodelete)) {
126 $permissiontodelete = $permtodelete;
127}
128
129// Mass actions. Controls on number of lines checked.
130$maxformassaction = getDolGlobalInt('MAIN_LIMIT_FOR_MASS_ACTIONS', 1000);
131if ($massaction && is_array($toselect) && count($toselect) < 1) {
132 $error++;
133 setEventMessages($langs->trans("NoRecordSelected"), null, "warnings");
134}
135if (!$error && isset($toselect) && is_array($toselect) && count($toselect) > $maxformassaction) {
136 setEventMessages($langs->trans('TooManyRecordForMassAction', $maxformassaction), null, 'errors');
137 $error++;
138}
139
140if (!$error && $massaction == 'confirm_presend' && !GETPOST('sendmail')) { // If we do not choose button send (for example when we change template or limit), we must not send email, but keep on send email form
141 $massaction = 'presend';
142}
143
144if (!$error && $massaction == 'confirm_presend') {
145 $resaction = '';
146 $nbsent = 0;
147 $nbignored = 0;
148 $langs->load("mails");
149 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
150 include_once DOL_DOCUMENT_ROOT.'/core/lib/signature.lib.php';
151
152 $listofobjectid = array();
153 $listofobjectthirdparties = array();
154 $listofobjectcontacts = array();
155 $listofobjectref = array();
156 $contactidtosend = array();
157 $attachedfilesThirdpartyObj = array();
158 $oneemailperrecipient = (GETPOSTINT('oneemailperrecipient') ? 1 : 0);
159 $thirdparty = null;
160
161 if (!$error) {
162 $objecttmp = new $objectclass($db);
163
164 // Define object $thirdparty (Societe or User, Adherent, ConferenceOrBoothAttendee...)
165 $thirdparty = new Societe($db);
166 if ($objecttmp->element == 'expensereport') {
167 $thirdparty = new User($db);
168 } elseif ($objecttmp->element == 'contact') {
169 $thirdparty = new Contact($db);
170 } elseif ($objecttmp->element == 'partnership' && getDolGlobalString('PARTNERSHIP_IS_MANAGED_FOR') == 'member') {
171 $thirdparty = new Adherent($db);
172 } elseif ($objecttmp->element == 'holiday') {
173 $thirdparty = new User($db);
174 } elseif ($objecttmp->element == 'conferenceorboothattendee') {
175 $thirdparty = new ConferenceOrBoothAttendee($db);
176 }
177
178 foreach ($toselect as $toselectid) {
179 $objecttmp = new $objectclass($db); // we must create new instance because instance is saved into $listofobjectref array for future use
180 '@phan-var-force CommonObject $objecttmp';
182 $result = $objecttmp->fetch($toselectid);
183 if ($result > 0) {
184 $listofobjectid[$toselectid] = $toselectid;
185
186 // Set $tmpobjectid that is ID of parent object (thirdparty or user in most cases)
187 $tmpobjectid = ($objecttmp->fk_soc ? $objecttmp->fk_soc : $objecttmp->socid);
188 if ($objecttmp->element == 'societe') {
189 '@phan-var-force Societe $objecttmp';
191 $tmpobjectid = $objecttmp->id;
192 } elseif ($objecttmp->element == 'contact') {
193 '@phan-var-force Contact $objecttmp';
195 $tmpobjectid = $objecttmp->id;
196 } elseif ($objecttmp->element == 'expensereport') {
197 '@phan-var-force ExpenseReport $objecttmp';
199 $tmpobjectid = $objecttmp->fk_user_author;
200 } elseif ($objecttmp->element == 'partnership' && getDolGlobalString('PARTNERSHIP_IS_MANAGED_FOR') == 'member') {
201 '@phan-var-force Partnership $objecttmp';
203 $tmpobjectid = $objecttmp->fk_member;
204 } elseif ($objecttmp->element == 'holiday') {
205 '@phan-var-force Holiday $objecttmp';
207 $tmpobjectid = $objecttmp->fk_user;
208 } elseif ($objecttmp->element == 'conferenceorboothattendee') {
209 '@phan-var-force ConferenceOrBoothAttendee $objecttmp';
211 $tmpobjectid = $objecttmp->id;
212 }
213 if (empty($tmpobjectid)) {
214 $tmpobjectid = 0;
215 }
216
217 if ($objectclass == 'Facture') {
218 '@phan-var-force Facture $objecttmp';
220 $tmparraycontact = array();
221 $tmparraycontact = $objecttmp->liste_contact(-1, 'external', 0, 'BILLING');
222 if (is_array($tmparraycontact) && count($tmparraycontact) > 0) {
223 foreach ($tmparraycontact as $data_email) {
224 $listofobjectcontacts[$toselectid][$data_email['id']] = $data_email['email'];
225 }
226 }
227 } elseif ($objectclass == 'CommandeFournisseur') {
228 '@phan-var-force CommandeFournisseur $objecttmp';
230 $tmparraycontact = array();
231 $tmparraycontact = $objecttmp->liste_contact(-1, 'external', 0, 'CUSTOMER');
232 if (is_array($tmparraycontact) && count($tmparraycontact) > 0) {
233 foreach ($tmparraycontact as $data_email) {
234 $listofobjectcontacts[$toselectid][$data_email['id']] = $data_email['email'];
235 }
236 }
237 }
238
239 $listofobjectthirdparties[$tmpobjectid] = $tmpobjectid;
240 $listofobjectref[$tmpobjectid][$toselectid] = $objecttmp;
241 }
242 }
243 }
244
245 // Check mandatory parameters
246 if (GETPOST('fromtype', 'alpha') === 'user' && empty($user->email)) {
247 $error++;
248 setEventMessages($langs->trans("NoSenderEmailDefined"), null, 'warnings');
249 $massaction = 'presend';
250 }
251
252 $receiver = GETPOST('receiver', 'alphawithlgt');
253 if (!is_array($receiver)) {
254 if (empty($receiver) || $receiver == '-1') {
255 $receiver = array();
256 } else {
257 $receiver = array($receiver);
258 }
259 }
260 if (!trim(GETPOST('sendto', 'alphawithlgt')) && count($receiver) == 0 && count($listofobjectthirdparties) == 1) { // if only one recipient, receiver is mandatory
261 $error++;
262 setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("MailRecipient")), null, 'warnings');
263 $massaction = 'presend';
264 }
265
266 if (!GETPOST('subject', 'restricthtml')) {
267 $error++;
268 setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("MailTopic")), null, 'warnings');
269 $massaction = 'presend';
270 }
271
272 // Loop on each recipient (may be a thirdparty but also a user, a conferenceorboothattendee, ...)
273 if (!$error) {
274 foreach ($listofobjectthirdparties as $thirdpartyid) {
275 $result = $thirdparty->fetch($thirdpartyid);
276 if ($result < 0) {
278 exit;
279 }
280
281 $sendto = '';
282 $sendtocc = '';
283 $sendtobcc = '';
284 //$sendtoid = array();
285
286 // Define $sendto
287 $tmparray = array();
288 if (trim(GETPOST('sendto', 'alphawithlgt'))) {
289 // Recipients are provided into free text
290 $tmparray[] = trim(GETPOST('sendto', 'alphawithlgt'));
291 }
292 if (count($receiver) > 0 && is_object($thirdparty)) {
293 foreach ($receiver as $key => $val) {
294 // Recipient was provided from combo list
295 if ($val == 'thirdparty') { // Id of third party or user
296 $tmparray[] = $thirdparty->name.' <'.$thirdparty->email.'>';
297 } elseif ($val && method_exists($thirdparty, 'contact_get_property')) { // Id of contact
298 $tmparray[] = $thirdparty->contact_get_property((int) $val, 'email');
299 //$sendtoid[] = $val;
300 }
301 }
302 }
303 $sendto = implode(',', $tmparray);
304
305 // Define $sendtocc
306 $receivercc = GETPOST('receivercc', 'alphawithlgt');
307 if (!is_array($receivercc)) {
308 if ($receivercc == '-1') {
309 $receivercc = array();
310 } else {
311 $receivercc = array($receivercc);
312 }
313 }
314 $tmparray = array();
315 if (trim(GETPOST('sendtocc', 'alphawithlgt'))) {
316 $tmparray[] = trim(GETPOST('sendtocc', 'alphawithlgt'));
317 }
318 if (count($receivercc) > 0) {
319 foreach ($receivercc as $key => $val) {
320 // Recipient was provided from combo list
321 if ($val == 'thirdparty') { // Id of third party
322 $tmparray[] = $thirdparty->name.' <'.$thirdparty->email.'>';
323 } elseif ($val) { // Id du contact
324 $tmparray[] = $thirdparty->contact_get_property((int) $val, 'email');
325 //$sendtoid[] = $val; TODO Add also id of contact in CC ?
326 }
327 }
328 }
329 $sendtocc = implode(',', $tmparray);
330
331 //var_dump($listofobjectref);exit;
332 $listofqualifiedobj = array();
333 $listofqualifiedref = array();
334 $thirdpartywithoutemail = array();
335
336 foreach ($listofobjectref[$thirdpartyid] as $objectid => $objectobj) {
337 //var_dump($thirdpartyid.' - '.$objectid.' - '.$objectobj->statut);
338 // Honour the same per-object hidden constants used by the "Send by email"
339 // button on the card pages (comm/propal/card.php, commande/card.php,
340 // compta/facture/card.php) so the behaviour is consistent between the
341 // single-object action and the mass action.
342 if ($objectclass == 'Propal' && $objectobj->status == Propal::STATUS_DRAFT && !getDolGlobalString('PROPOSAL_SENDBYEMAIL_FOR_ALL_STATUS')) {
343 $langs->load("errors");
344 $nbignored++;
345 $resaction .= '<div class="error">'.$langs->trans('ErrorOnlyProposalNotDraftCanBeSentInMassAction', $objectobj->ref).'</div><br>';
346 continue; // Payment done or started or canceled
347 }
348 if ($objectclass == 'Commande' && $objectobj->status == Commande::STATUS_DRAFT && !getDolGlobalString('COMMANDE_SENDBYEMAIL_FOR_ALL_STATUS')) {
349 $langs->load("errors");
350 $nbignored++;
351 $resaction .= '<div class="error">'.$langs->trans('ErrorOnlyOrderNotDraftCanBeSentInMassAction', $objectobj->ref).'</div><br>';
352 continue;
353 }
354 if ($objectclass == 'Facture' && $objectobj->status == Facture::STATUS_DRAFT && !getDolGlobalString('FACTURE_SENDBYEMAIL_FOR_ALL_STATUS')) {
355 $langs->load("errors");
356 $nbignored++;
357 $resaction .= '<div class="error">'.$langs->trans('ErrorOnlyInvoiceValidatedCanBeSentInMassAction', $objectobj->ref).'</div><br>';
358 continue; // Payment done or started or canceled
359 }
360
361 // Test recipient
362 if (empty($sendto)) { // For the case, no recipient were set (multi thirdparties send)
363 if ($objectobj->element == 'societe') {
364 $sendto = $objectobj->email;
365 } elseif ($objectobj->element == 'expensereport') {
366 $fuser = new User($db);
367 $fuser->fetch($objectobj->fk_user_author);
368 $sendto = $fuser->email;
369 } elseif ($objectobj->element == 'contact') {
370 $fcontact = new Contact($db);
371 $fcontact->fetch($objectobj->id);
372 $sendto = $fcontact->email;
373 } elseif ($objectobj->element == 'partnership' && getDolGlobalString('PARTNERSHIP_IS_MANAGED_FOR') == 'member') {
374 $fadherent = new Adherent($db);
375 $fadherent->fetch($objectobj->fk_member);
376 $sendto = $fadherent->email;
377 } elseif ($objectobj->element == 'holiday') {
378 $fuser = new User($db);
379 $fuser->fetch($objectobj->fk_user);
380 $sendto = $fuser->email;
381 } elseif ($objectobj->element == 'facture' && !empty($listofobjectcontacts[$objectid])) {
382 $emails_to_sends = array();
383 $objectobj->fetch_thirdparty();
384 $contactidtosend = array();
385 foreach ($listofobjectcontacts[$objectid] as $contactemailid => $contactemailemail) {
386 $emails_to_sends[] = $objectobj->thirdparty->contact_get_property($contactemailid, 'email');
387 if (!in_array($contactemailid, $contactidtosend)) {
388 $contactidtosend[] = $contactemailid;
389 }
390 }
391 if (count($emails_to_sends) > 0) {
392 $sendto = implode(',', $emails_to_sends);
393 }
394 } elseif ($objectobj->element == 'order_supplier' && !empty($listofobjectcontacts[$objectid])) {
395 $emails_to_sends = array();
396 $objectobj->fetch_thirdparty();
397 $contactidtosend = array();
398 foreach ($listofobjectcontacts[$objectid] as $contactemailid => $contactemailemail) {
399 $emails_to_sends[] = $objectobj->thirdparty->contact_get_property($contactemailid, 'email');
400 if (!in_array($contactemailid, $contactidtosend)) {
401 $contactidtosend[] = $contactemailid;
402 }
403 }
404 if (count($emails_to_sends) > 0) {
405 $sendto = implode(',', $emails_to_sends);
406 }
407 } elseif ($objectobj->element == 'conferenceorboothattendee') {
408 $sendto = $objectobj->email;
409 } else {
410 $objectobj->fetch_thirdparty();
411 $sendto = $objectobj->thirdparty->email;
412 }
413 }
414
415 if (empty($sendto)) {
416 if ($objectobj->element == 'societe') {
418 $objectobj->thirdparty = $objectobj; // Hack so following code is compatible when objectobj is a thirdparty
419 }
420
421 // print "No recipient for thirdparty ".$objectobj->thirdparty->name;
422 $nbignored++;
423 if (empty($thirdpartywithoutemail[$objectobj->thirdparty->id])) {
424 $resaction .= '<div class="error">'.$langs->trans('NoRecipientEmail', $objectobj->thirdparty->name).'</div><br>';
425 }
426 dol_syslog('No recipient for thirdparty: '.$objectobj->thirdparty->name, LOG_WARNING);
427 $thirdpartywithoutemail[$objectobj->thirdparty->id] = 1;
428 continue;
429 }
430
431 if (GETPOST('addmaindocfile')) {
432 // TODO Use future field $objectobj->fullpathdoc to know where is stored default file
433 // TODO If not defined, use $objectobj->model_pdf (or default invoice config) to know what is template to use to regenerate doc.
434 $filename = dol_sanitizeFileName($objectobj->ref).'.pdf';
435 $subdir = '';
436 // TODO Set subdir to be compatible with multi levels dir trees
437 // $subdir = get_exdir($objectobj->id, 2, 0, 0, $objectobj, $objectobj->element)
438 $filedir = $uploaddir.'/'.$subdir.dol_sanitizeFileName($objectobj->ref);
439 $filepath = $filedir.'/'.$filename;
440
441 // For supplier invoices, we use the file provided by supplier, not the one we generate
442 if ($objectobj->element == 'invoice_supplier') {
443 $fileparams = dol_most_recent_file($uploaddir.'/'.get_exdir($objectobj->id, 2, 0, 0, $objectobj, $objectobj->element).$objectobj->ref, preg_quote($objectobj->ref, '/').'([^\-])+');
444 $filepath = $fileparams['fullname'];
445 }
446
447 if (getDolGlobalInt('MAIL_MASS_ACTION_SEARCH_MOST_RECENT_FILE_IF_NOT_FOUND') && isset($filepath) && !dol_is_file($filepath)) {
448 $fileparams = dol_most_recent_file($filedir, preg_quote($objectobj->ref, '/') . '([^\-])+' . (getDolGlobalInt('MAIN_ODT_AS_PDF') ? '\.pdf$' : ''));
449 if (isset($fileparams)) {
450 $filepath = $fileparams['fullname'];
451 $filename = $fileparams['name'];
452 }
453 }
454
455 // try to find other files generated for this object (last_main_doc)
456 $filename_found = '';
457 $filepath_found = '';
458 $file_check_list = array();
459 $file_check_list[] = array(
460 'name' => $filename,
461 'path' => $filepath,
462 );
463 if (getDolGlobalString('MAIL_MASS_ACTION_ADD_LAST_IF_MAIN_DOC_NOT_FOUND') && !empty($objectobj->last_main_doc)) {
464 $file_check_list[] = array(
465 'name' => basename($objectobj->last_main_doc),
466 'path' => DOL_DATA_ROOT . '/' . $objectobj->last_main_doc,
467 );
468 }
469 foreach ($file_check_list as $file_check_arr) {
470 if (dol_is_file($file_check_arr['path'])) {
471 $filename_found = $file_check_arr['name'];
472 $filepath_found = $file_check_arr['path'];
473 break;
474 }
475 }
476
477 if ($filepath_found) {
478 // Create form object
479 $attachedfilesThirdpartyObj[$thirdpartyid][$objectid] = array(
480 'paths' => array($filepath_found),
481 'names' => array($filename_found),
482 'mimes' => array(dol_mimetype($filepath_found))
483 );
484 } else {
485 $nbignored++;
486 $langs->load("errors");
487 foreach ($file_check_list as $file_check_arr) {
488 $resaction .= '<div class="error">'.$langs->trans('ErrorCantReadFile', $file_check_arr['path']).'</div><br>';
489 dol_syslog('Failed to read file: '.$file_check_arr['path'], LOG_WARNING);
490 }
491 continue;
492 }
493 }
494
495 // Object of thirdparty qualified, we add it
496 $listofqualifiedobj[$objectid] = $objectobj;
497 $listofqualifiedref[$objectid] = $objectobj->ref;
498
499
500 //var_dump($listofqualifiedref);
501 }
502
503 // Send email if there is at least one qualified object for current thirdparty
504 if (count($listofqualifiedobj) > 0) {
505 $langs->load("commercial");
506 $email_from = '';
507
508 $reg = array();
509 $fromtype = GETPOST('fromtype');
510 if ($fromtype === 'user') {
511 $email_from = dol_string_nospecial($user->getFullName($langs), ' ', array(",")).' <'.$user->email.'>';
512 } elseif ($fromtype === 'company') {
513 $email_from = getDolGlobalString('MAIN_INFO_SOCIETE_NOM') . ' <' . getDolGlobalString('MAIN_INFO_SOCIETE_MAIL').'>';
514 } elseif (preg_match('/global_aliases_(\d+)/', $fromtype, $reg)) {
515 $tmp = explode(',', getDolGlobalString('MAIN_INFO_SOCIETE_MAIL_ALIASES'));
516 $email_from = trim($tmp[((int) $reg[1] - 1)]);
517 } elseif (preg_match('/senderprofile_(\d+)_(\d+)/', $fromtype, $reg)) {
518 $sql = "SELECT rowid, label, email FROM ".MAIN_DB_PREFIX."c_email_senderprofile WHERE rowid = ".(int) $reg[1];
519 $resql = $db->query($sql);
520 $obj = $db->fetch_object($resql);
521 if ($obj) {
522 $email_from = dol_string_nospecial($obj->label, ' ', array(",")).' <'.$obj->email.'>';
523 }
524 } elseif (preg_match('/from_template_(\d+)/', $fromtype, $reg)) {
525 $sql = "SELECT rowid, email_from FROM ".MAIN_DB_PREFIX."c_email_templates WHERE rowid = ".(int) $reg[1];
526 $resql = $db->query($sql);
527 $obj = $db->fetch_object($resql);
528 if ($obj) {
529 $email_from = $obj->email_from;
530 }
531 } else {
532 $email_from = GETPOST('fromname').' <'.GETPOST('frommail').'>';
533 }
534
535 $replyto = $email_from;
536 $subject = GETPOST('subject', 'restricthtml');
537 $message = GETPOST('message', 'restricthtml');
538
539 $sendtobcc = GETPOST('sendtoccc');
540 if ($objectclass == 'Propal') {
541 $sendtobcc .= (!getDolGlobalString('MAIN_MAIL_AUTOCOPY_PROPOSAL_TO') ? '' : (($sendtobcc ? ", " : "") . getDolGlobalString('MAIN_MAIL_AUTOCOPY_PROPOSAL_TO')));
542 }
543 if ($objectclass == 'Commande') {
544 $sendtobcc .= (!getDolGlobalString('MAIN_MAIL_AUTOCOPY_ORDER_TO') ? '' : (($sendtobcc ? ", " : "") . getDolGlobalString('MAIN_MAIL_AUTOCOPY_ORDER_TO')));
545 }
546 if ($objectclass == 'Facture') {
547 $sendtobcc .= (!getDolGlobalString('MAIN_MAIL_AUTOCOPY_INVOICE_TO') ? '' : (($sendtobcc ? ", " : "") . getDolGlobalString('MAIN_MAIL_AUTOCOPY_INVOICE_TO')));
548 }
549 if ($objectclass == 'SupplierProposal') {
550 $sendtobcc .= (!getDolGlobalString('MAIN_MAIL_AUTOCOPY_SUPPLIER_PROPOSAL_TO') ? '' : (($sendtobcc ? ", " : "") . getDolGlobalString('MAIN_MAIL_AUTOCOPY_SUPPLIER_PROPOSAL_TO')));
551 }
552 if ($objectclass == 'CommandeFournisseur') {
553 $sendtobcc .= (!getDolGlobalString('MAIN_MAIL_AUTOCOPY_SUPPLIER_ORDER_TO') ? '' : (($sendtobcc ? ", " : "") . getDolGlobalString('MAIN_MAIL_AUTOCOPY_SUPPLIER_ORDER_TO')));
554 }
555 if ($objectclass == 'FactureFournisseur') {
556 $sendtobcc .= (!getDolGlobalString('MAIN_MAIL_AUTOCOPY_SUPPLIER_INVOICE_TO') ? '' : (($sendtobcc ? ", " : "") . getDolGlobalString('MAIN_MAIL_AUTOCOPY_SUPPLIER_INVOICE_TO')));
557 }
558 if ($objectclass == 'Project') {
559 $sendtobcc .= (!getDolGlobalString('MAIN_MAIL_AUTOCOPY_PROJECT_TO') ? '' : (($sendtobcc ? ", " : "") . getDolGlobalString('MAIN_MAIL_AUTOCOPY_PROJECT_TO')));
560 }
561
562 // $listofqualifiedobj is array with key = object id and value is instance of qualified objects, for the current thirdparty (but thirdparty property is not loaded yet)
563 // $looparray will be an array with number of email to send for the current thirdparty (so 1 or n if n object for same thirdparty)
564 $looparray = array();
565 if (!$oneemailperrecipient) {
566 $looparray = $listofqualifiedobj;
567 foreach ($looparray as $key => $objecttmp) {
568 $looparray[$key]->thirdparty = $thirdparty; // Force thirdparty on object
569 }
570 } else {
571 $objectforloop = new $objectclass($db);
572 $objectforloop->thirdparty = $thirdparty; // Force thirdparty on object (even if object was not loaded)
573 $looparray[0] = $objectforloop;
574 }
575 //var_dump($looparray);exit;
576 dol_syslog("We have set an array of ".count($looparray)." emails to send. oneemailperrecipient=".$oneemailperrecipient);
577 //var_dump($oneemailperrecipient); var_dump($listofqualifiedobj); var_dump($listofqualifiedref);
578 foreach ($looparray as $objectid => $objecttmp) { // $objecttmp is a real object or an empty object if we choose to send one email per thirdparty instead of one per object
579 // Make substitution in email content
580 if (isModEnabled('project') && method_exists($objecttmp, 'fetchProject') && is_null($objecttmp->project)) {
581 $objecttmp->fetchProject();
582 }
583 $substitutionarray = getCommonSubstitutionArray($langs, 0, null, $objecttmp);
584 $substitutionarray['__ID__'] = ($oneemailperrecipient ? implode(', ', array_keys($listofqualifiedobj)) : $objecttmp->id);
585 $substitutionarray['__REF__'] = ($oneemailperrecipient ? implode(', ', $listofqualifiedref) : $objecttmp->ref);
586 $substitutionarray['__EMAIL__'] = $thirdparty->email;
587 $substitutionarray['__CHECK_READ__'] = '<img src="'.DOL_MAIN_URL_ROOT.'/public/emailing/mailing-read.php?tag=undefined&securitykey='.dol_hash(getDolGlobalString('MAILING_EMAIL_UNSUBSCRIBE_KEY')."-undefined", 'md5').'" width="1" height="1" style="width:1px;height:1px" border="0"/>';
588
589 if ($oneemailperrecipient) {
590 $substitutionarray['__ONLINE_PAYMENT_URL__'] = '';
591 $substitutionarray['__ONLINE_PAYMENT_TEXT_AND_URL__'] = '';
592 }
593
594 $parameters = array('mode' => 'formemail');
595
596 if (!empty($listofobjectthirdparties)) {
597 $parameters['listofobjectthirdparties'] = $listofobjectthirdparties;
598 }
599 if (!empty($listofobjectref)) {
600 $parameters['listofobjectref'] = $listofobjectref;
601 }
602
603 complete_substitutions_array($substitutionarray, $langs, $objecttmp, $parameters);
604
605 $subjectreplaced = make_substitutions($subject, $substitutionarray);
606 $messagereplaced = make_substitutions($message, $substitutionarray);
607
608 $attachedfiles = array('paths' => array(), 'names' => array(), 'mimes' => array());
609 if ($oneemailperrecipient) {
610 // if "one email per recipient" is check we must collate $attachedfiles by thirdparty
611 if (is_array($attachedfilesThirdpartyObj[$thirdparty->id]) && count($attachedfilesThirdpartyObj[$thirdparty->id])) { // @phpstan-ignore-line
612 foreach ($attachedfilesThirdpartyObj[$thirdparty->id] as $keyObjId => $objAttachedFiles) {
613 // Create form object
614 $attachedfiles = array(
615 'paths' => array_merge($attachedfiles['paths'], $objAttachedFiles['paths']),
616 'names' => array_merge($attachedfiles['names'], $objAttachedFiles['names']),
617 'mimes' => array_merge($attachedfiles['mimes'], $objAttachedFiles['mimes'])
618 );
619 }
620 }
621 } elseif (!empty($attachedfilesThirdpartyObj[$thirdparty->id][$objectid])) {
622 // Create form object
623 // if "one email per recipient" isn't check we must separate $attachedfiles by object
624 $attachedfiles = $attachedfilesThirdpartyObj[$thirdparty->id][$objectid];
625 }
626
627 $filepath = $attachedfiles['paths'];
628 $filename = $attachedfiles['names'];
629 $mimetype = $attachedfiles['mimes'];
630
631 // Define the trackid when emails sent from the mass action
632 if ($oneemailperrecipient) {
633 $trackid = 'thi'.$thirdparty->id;
634 if ($objecttmp->element == 'expensereport') {
635 $trackid = 'use'.$thirdparty->id;
636 } elseif ($objecttmp->element == 'contact') {
637 $trackid = 'ctc'.$thirdparty->id;
638 } elseif ($objecttmp->element == 'holiday') {
639 $trackid = 'use'.$thirdparty->id;
640 }
641 } else {
642 $trackid = strtolower(get_class($objecttmp));
643 if (get_class($objecttmp) == 'Contact') {
644 $trackid = 'ctc';
645 } elseif (get_class($objecttmp) == 'Contrat') {
646 $trackid = 'con';
647 } elseif (get_class($objecttmp) == 'Propal') {
648 $trackid = 'pro';
649 } elseif (get_class($objecttmp) == 'Commande') {
650 $trackid = 'ord';
651 } elseif (get_class($objecttmp) == 'Facture') {
652 $trackid = 'inv';
653 } elseif (get_class($objecttmp) == 'SupplierProposal') {
654 $trackid = 'spr';
655 } elseif (get_class($objecttmp) == 'CommandeFournisseur') {
656 $trackid = 'sor';
657 } elseif (get_class($objecttmp) == 'FactureFournisseur') {
658 $trackid = 'sin';
659 }
660
661 $trackid .= $objecttmp->id;
662 }
663 //var_dump($filepath);
664 //var_dump($trackid);exit;
665 //var_dump($subjectreplaced);
666
667 if (empty($sendcontext)) {
668 $sendcontext = 'standard';
669 }
670
671 // Set tmp user directory (used to convert images embedded as img src=data:image)
672 $vardir = $conf->user->dir_output."/".$user->id;
673 $upload_dir_tmp = $vardir.'/temp'; // TODO Add $keytoavoidconflict in upload_dir path
674
675 // Send mail (substitutionarray must be done just before this)
676 require_once DOL_DOCUMENT_ROOT.'/core/class/CMailFile.class.php';
677 $mailfile = new CMailFile($subjectreplaced, (string) $sendto, $email_from, $messagereplaced, $filepath, $mimetype, $filename, $sendtocc, $sendtobcc, (int) $deliveryreceipt, -1, '', '', $trackid, '', $sendcontext, '', $upload_dir_tmp);
678 if ($mailfile->error) {
679 $resaction .= '<div class="error">'.$mailfile->error.'</div>';
680 } else {
681 $result = $mailfile->sendfile();
682 if ($result > 0) {
683 $resaction .= $langs->trans('MailSuccessfulySent', $mailfile->getValidAddress($mailfile->addr_from, 2), $mailfile->getValidAddress($mailfile->addr_to, 2)).'<br>'; // Must not contain "
684
685 $error = 0;
686
687 // Insert logs into agenda
688 foreach ($listofqualifiedobj as $objid2 => $objectobj2) {
689 if ((!$oneemailperrecipient) && $objid2 != $objectid) {
690 continue; // We discard this pass to avoid duplicate with other pass in looparray at higher level
691 }
692
693 dol_syslog("Try to insert email event into agenda for objid=".$objid2." => objectobj=".get_class($objectobj2));
694
695 /*if ($objectclass == 'Propale') $actiontypecode='AC_PROP';
696 if ($objectclass == 'Commande') $actiontypecode='AC_COM';
697 if ($objectclass == 'Facture') $actiontypecode='AC_FAC';
698 if ($objectclass == 'SupplierProposal') $actiontypecode='AC_SUP_PRO';
699 if ($objectclass == 'CommandeFournisseur') $actiontypecode='AC_SUP_ORD';
700 if ($objectclass == 'FactureFournisseur') $actiontypecode='AC_SUP_INV';*/
701
702 $actionmsg = $langs->transnoentities('MailSentByTo', $email_from, (string) $sendto);
703 if ($message) {
704 if ($sendtocc) {
705 $actionmsg = dol_concatdesc($actionmsg, $langs->transnoentities('Bcc').": ".$sendtocc);
706 }
707 $actionmsg = dol_concatdesc($actionmsg, $langs->transnoentities('MailTopic').": ".$subjectreplaced);
708 $actionmsg = dol_concatdesc($actionmsg, $langs->transnoentities('TextUsedInTheMessageBody').":");
709 $actionmsg = dol_concatdesc($actionmsg, $messagereplaced);
710 }
711 $actionmsg2 = '';
712
713 // Initialisation donnees
714 $objectobj2->sendtoid = (empty($contactidtosend) ? 0 : $contactidtosend);
715 $objectobj2->actionmsg = $actionmsg; // Long text
716 $objectobj2->actionmsg2 = $actionmsg2; // Short text
717 $objectobj2->fk_element = $objid2;
718 $objectobj2->elementtype = $objectobj2->element;
719 if (getDolGlobalString('MAIN_MAIL_REPLACE_EVENT_TITLE_BY_EMAIL_SUBJECT')) {
720 $objectobj2->actionmsg2 = $subjectreplaced; // Short text
721 }
722
723 $triggername = strtoupper(get_class($objectobj2)).'_SENTBYMAIL';
724 if ($triggername == 'SOCIETE_SENTBYMAIL') {
725 $triggername = 'COMPANY_SENTBYMAIL';
726 }
727 if ($triggername == 'CONTRAT_SENTBYMAIL') {
728 $triggername = 'CONTRACT_SENTBYMAIL';
729 }
730 if ($triggername == 'COMMANDE_SENTBYMAIL') {
731 $triggername = 'ORDER_SENTBYMAIL';
732 }
733 if ($triggername == 'FACTURE_SENTBYMAIL') {
734 $triggername = 'BILL_SENTBYMAIL';
735 }
736 if ($triggername == 'EXPEDITION_SENTBYMAIL') {
737 $triggername = 'SHIPPING_SENTBYMAIL';
738 }
739 if ($triggername == 'COMMANDEFOURNISSEUR_SENTBYMAIL') {
740 $triggername = 'ORDER_SUPPLIER_SENTBYMAIL';
741 }
742 if ($triggername == 'FACTUREFOURNISSEUR_SENTBYMAIL') {
743 $triggername = 'BILL_SUPPLIER_SENTBYMAIL';
744 }
745 if ($triggername == 'SUPPLIERPROPOSAL_SENTBYMAIL') {
746 $triggername = 'PROPOSAL_SUPPLIER_SENTBYMAIL';
747 }
748 if ($triggername == 'PROJET_SENTBYMAIL') {
749 $triggername = 'PROJECT_SENTBYMAIL';
750 }
751
752 if (!empty($triggername)) {
753 // Call trigger
754 $result = $objectobj2->call_trigger($triggername, $user);
755 if ($result < 0) {
756 $error++;
757 }
758 // End call triggers
759
760 if ($error) {
761 setEventMessages($db->lasterror(), null, 'errors');
762 dol_syslog("Error in trigger ".$triggername.' '.$db->lasterror(), LOG_ERR);
763 }
764 }
765
766 $nbsent++; // Nb of object sent
767 }
768 } else {
769 $langs->load("other");
770 if ($mailfile->error) {
771 $resaction .= $langs->trans('ErrorFailedToSendMail', $email_from, (string) $sendto);
772 $resaction .= '<br><div class="error">'.$mailfile->error.'</div>';
773 } elseif (getDolGlobalString('MAIN_DISABLE_ALL_MAILS')) {
774 $resaction .= '<div class="warning">No mail sent. Feature is disabled by option MAIN_DISABLE_ALL_MAILS</div>';
775 } else {
776 $resaction .= $langs->trans('ErrorFailedToSendMail', $email_from, (string) $sendto) . '<br><div class="error">(unhandled error)</div>';
777 }
778 }
779 }
780 }
781 }
782 }
783
784 $resaction .= ($resaction ? '<br>' : $resaction);
785 $resaction .= '<strong>'.$langs->trans("ResultOfMailSending").':</strong><br>'."\n";
786 $resaction .= $langs->trans("NbSelected").': '.count($toselect)."\n<br>";
787 $resaction .= $langs->trans("NbIgnored").': '.($nbignored ? $nbignored : 0)."\n<br>";
788 $resaction .= $langs->trans("NbSent").': '.($nbsent ? $nbsent : 0)."\n<br>";
789
790 if ($nbsent) {
791 $action = ''; // Do not show form post if there was at least one successful sent
792 //setEventMessages($langs->trans("EMailSentToNRecipients", $nbsent.'/'.count($toselect)), null, 'mesgs');
793 setEventMessages($langs->trans("EMailSentForNElements", $nbsent.'/'.count($toselect)), null, 'mesgs');
794 setEventMessages($resaction, null, 'mesgs');
795 } else {
796 //setEventMessages($langs->trans("EMailSentToNRecipients", 0), null, 'warnings'); // May be object has no generated PDF file
797 setEventMessages($resaction, null, 'warnings');
798 }
799
800 $action = 'list';
801 $massaction = '';
802 }
803}
804
805
806if (!$error && $massaction == 'cancelorders') {
807 $db->begin();
808
809 $nbok = 0;
810
811 $orders = GETPOST('toselect', 'array:int');
812 foreach ($orders as $id_order) {
813 $cmd = new Commande($db);
814 if ($cmd->fetch($id_order) <= 0) {
815 continue;
816 }
817
818 if ($cmd->status != Commande::STATUS_VALIDATED) {
819 $langs->load('errors');
820 setEventMessages($langs->trans("ErrorObjectMustHaveStatusValidToBeCanceled", $cmd->ref), null, 'errors');
821 $error++;
822 break;
823 } else {
824 // TODO We do not provide warehouse so no stock change here for the moment.
825 $result = $cmd->cancel($user);
826 }
827
828 if ($result < 0) {
829 setEventMessages($cmd->error, $cmd->errors, 'errors');
830 $error++;
831 break;
832 } else {
833 $nbok++;
834 }
835 }
836 if (!$error) {
837 setEventMessages($langs->trans("RecordsModified", $nbok), null, 'mesgs');
838 $db->commit();
839 } else {
840 $db->rollback();
841 }
842}
843
844
845if (!$error && $massaction == "builddoc" && $permissiontoread && !GETPOST('button_search')) {
846 if (empty($diroutputmassaction)) {
847 dol_print_error(null, 'include of actions_massactions.inc.php is done but var $diroutputmassaction was not defined');
848 exit;
849 }
850
851 require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
852 require_once DOL_DOCUMENT_ROOT.'/core/lib/pdf.lib.php';
853 require_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php';
854
855 $objecttmp = new $objectclass($db);
856 $listofobjectid = array();
857 $listofobjectthirdparties = array();
858 $listofobjectref = array();
859 foreach ($toselect as $toselectid) {
860 $objecttmp = new $objectclass($db); // must create new instance because instance is saved into $listofobjectref array for future use
861 $result = $objecttmp->fetch($toselectid);
862 if ($result > 0) {
863 $listofobjectid[$toselectid] = $toselectid;
864 $thirdpartyid = !empty($objecttmp->fk_soc) ? $objecttmp->fk_soc : $objecttmp->socid;
865 $listofobjectthirdparties[$thirdpartyid] = $thirdpartyid;
866 $listofobjectref[$toselectid] = $objecttmp->ref;
867 }
868 }
869
870 $arrayofinclusion = array();
871 $parameters = array(
872 'listofobjectref' => $listofobjectref,
873 'arrayofinclusion' => &$arrayofinclusion,
874 );
875 $reshook = $hookmanager->executeHooks('updateSearchRegexToMergeDoc', $parameters, $object, $action);
876
877 if (empty($reshook)) {
878 foreach ($listofobjectref as $tmppdf) {
879 $arrayofinclusion[] = '^'.preg_quote(dol_sanitizeFileName($tmppdf), '/').'\.pdf$';
880 }
881 foreach ($listofobjectref as $tmppdf) {
882 $arrayofinclusion[] = '^'.preg_quote(dol_sanitizeFileName($tmppdf), '/').'_[\w\-\'\&\.]+\.pdf$'; // To include PDF generated from ODX files
883 }
884 }
885
886 $listoffiles = dol_dir_list($uploaddir, 'all', 1, $arrayofinclusion, '\.meta$|\.png$', 'date', SORT_DESC, 0, 1);
887
888 dol_syslog("Found ".count($listoffiles)." files");
889
890 // build list of files with full path
891 $files = array();
892
893 foreach ($listofobjectref as $basename) {
894 $basename = dol_sanitizeFileName($basename);
895 foreach ($listoffiles as $filefound) {
896 if (strstr($filefound["name"], $basename)) {
897 $files[] = $filefound['fullname'];
898 break;
899 }
900 }
901 }
902
903 // Define output language (Here it is not used because we do only merging existing PDF)
904 $outputlangs = $langs;
905 $newlang = '';
906 if (getDolGlobalInt('MAIN_MULTILANGS') /* && empty($newlang) */ && GETPOST('lang_id', 'aZ09')) {
907 $newlang = GETPOST('lang_id', 'aZ09');
908 }
909 //elseif (getDolGlobalInt('MAIN_MULTILANGS') && empty($newlang) && is_object($objecttmp->thirdparty)) { // On massaction, we can have several values for $objecttmp->thirdparty
910 // $newlang = $objecttmp->thirdparty->default_lang;
911 //}
912 if (!empty($newlang)) {
913 $outputlangs = new Translate("", $conf);
914 $outputlangs->setDefaultLang($newlang);
915 }
916
917 if (getDolGlobalString('USE_PDFTK_FOR_PDF_CONCAT')) {
918 // Create output dir if not exists
919 dol_mkdir($diroutputmassaction);
920
921 // Defined name of merged file
922 $filename = strtolower(dol_sanitizeFileName($langs->transnoentities((string) $objectlabel)));
923 $filename = preg_replace('/\s/', '_', $filename);
924
925 // Save merged file
926 if (in_array($objecttmp->element, array('facture', 'invoice_supplier')) && $search_status == Facture::STATUS_VALIDATED) {
927 if ($option == 'late') {
928 $filename .= '_'.strtolower(dol_sanitizeFileName($langs->transnoentities("Unpaid"))).'_'.strtolower(dol_sanitizeFileName($langs->transnoentities("Late")));
929 } else {
930 $filename .= '_'.strtolower(dol_sanitizeFileName($langs->transnoentities("Unpaid")));
931 }
932 }
933 if ($year) {
934 $filename .= '_'.$year;
935 }
936 if ($month) {
937 $filename .= '_'.$month;
938 }
939
940 if (count($files) > 0) {
941 $now = dol_now();
942 $file = $diroutputmassaction.'/'.$filename.'_'.dol_print_date($now, 'dayhourlog').'.pdf';
943
944 $input_files = '';
945 foreach ($files as $f) {
946 $input_files .= ' '.escapeshellarg($f);
947 }
948
949 $cmd = 'pdftk '.$input_files.' cat output '.escapeshellarg($file);
950 exec($cmd);
951
952 // check if pdftk is installed
953 if (file_exists($file)) {
954 dolChmod($file);
955
956 $langs->load("exports");
957 setEventMessages($langs->trans('FileSuccessfullyBuilt', $filename.'_'.dol_print_date($now, 'dayhourlog')), null, 'mesgs');
958 } else {
959 setEventMessages($langs->trans('ErrorPDFTkOutputFileNotFound'), null, 'errors');
960 }
961 } else {
962 setEventMessages($langs->trans('NoPDFAvailableForDocGenAmongChecked'), null, 'errors');
963 }
964 } else {
965 // Create empty PDF
966 $formatarray = pdf_getFormat();
967 $page_largeur = $formatarray['width'];
968 $page_hauteur = $formatarray['height'];
969 $format = array($page_largeur, $page_hauteur);
970
971 $pdf = pdf_getInstance($format);
972
973 if (class_exists('TCPDF')) {
974 $pdf->setPrintHeader(false);
975 $pdf->setPrintFooter(false);
976 }
977 $pdf->SetFont(pdf_getPDFFont($outputlangs));
978
979 if (getDolGlobalString('MAIN_DISABLE_PDF_COMPRESSION')) {
980 $pdf->SetCompression(false);
981 }
982
983 $pagecount = 0;
984 // Add all others
985 foreach ($files as $file) {
986 // Load a PDF document from a file.
987 $pagecount = $pdf->setSourceFile($file);
988 for ($i = 1; $i <= $pagecount; $i++) {
989 $tplidx = $pdf->importPage($i);
990 $s = $pdf->getTemplatesize($tplidx);
991 $pdf->AddPage($s['h'] > $s['w'] ? 'P' : 'L');
992 $pdf->useTemplate($tplidx);
993 }
994 }
995
996 // Create output dir if not exists
997 dol_mkdir($diroutputmassaction);
998
999 // Defined name of merged file
1000 $filename = strtolower(dol_sanitizeFileName($langs->transnoentities((string) $objectlabel)));
1001 $filename = preg_replace('/\s/', '_', $filename);
1002
1003
1004 // Save merged file
1005 if (in_array($objecttmp->element, array('facture', 'invoice_supplier')) && $search_status == Facture::STATUS_VALIDATED) {
1006 if ($option == 'late') {
1007 $filename .= '_'.strtolower(dol_sanitizeFileName($langs->transnoentities("Unpaid"))).'_'.strtolower(dol_sanitizeFileName($langs->transnoentities("Late")));
1008 } else {
1009 $filename .= '_'.strtolower(dol_sanitizeFileName($langs->transnoentities("Unpaid")));
1010 }
1011 }
1012 if (!empty($year)) {
1013 $filename .= '_'.$year;
1014 }
1015 if (!empty($month)) {
1016 $filename .= '_'.$month;
1017 }
1018 if ($pagecount) {
1019 $now = dol_now();
1020 $file = $diroutputmassaction.'/'.$filename.'_'.dol_print_date($now, 'dayhourlog').'.pdf';
1021 $pdf->Output($file, 'F');
1022 dolChmod($file);
1023
1024 $langs->load("exports");
1025 setEventMessages($langs->trans('FileSuccessfullyBuilt', $filename.'_'.dol_print_date($now, 'dayhourlog')), null, 'mesgs');
1026 } else {
1027 setEventMessages($langs->trans('NoPDFAvailableForDocGenAmongChecked'), null, 'errors');
1028 }
1029 }
1030}
1031
1032// Remove a file from massaction area
1033if ($action == 'remove_file') {
1034 require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1035
1036 $langs->load("other");
1037 $upload_dir = $diroutputmassaction;
1038 $file = $upload_dir.'/'.GETPOST('file');
1039 $ret = dol_delete_file($file, 1);
1040 if ($ret) {
1041 setEventMessages($langs->trans("FileWasRemoved", GETPOST('file')), null, 'mesgs');
1042 } else {
1043 setEventMessages($langs->trans("ErrorFailToDeleteFile", GETPOST('file')), null, 'errors');
1044 }
1045 $action = '';
1046}
1047
1048
1049// Validate records
1050if (!$error && $massaction == 'validate' && $permissiontoadd) {
1051 $objecttmp = new $objectclass($db);
1052
1053 if (($objecttmp->element == 'facture' || $objecttmp->element == 'invoice') && isModEnabled('stock') && getDolGlobalString('STOCK_CALCULATE_ON_BILL')) {
1054 $langs->load("errors");
1055 setEventMessages($langs->trans('ErrorMassValidationNotAllowedWhenStockIncreaseOnAction'), null, 'errors');
1056 $error++;
1057 }
1058 if ($objecttmp->element == 'invoice_supplier' && isModEnabled('stock') && getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_BILL')) {
1059 $langs->load("errors");
1060 setEventMessages($langs->trans('ErrorMassValidationNotAllowedWhenStockIncreaseOnAction'), null, 'errors');
1061 $error++;
1062 }
1063 if ($objecttmp->element == 'facture') {
1064 if (!empty($toselect) && getDolGlobalString('INVOICE_CHECK_POSTERIOR_DATE')) {
1065 // order $toselect by date
1066 $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture";
1067 $sql .= " WHERE rowid IN (".$db->sanitize(implode(",", $toselect)).")";
1068 $sql .= " ORDER BY datef";
1069
1070 $resql = $db->query($sql);
1071 if ($resql) {
1072 $toselectnew = [];
1073 while (!empty($arr = $db->fetch_row($resql))) {
1074 $toselectnew[] = $arr[0];
1075 }
1076 $toselect = (empty($toselectnew)) ? $toselect : $toselectnew;
1077 } else {
1079 $error++;
1080 }
1081 }
1082 }
1083 if (!$error) {
1084 // MAIN_MASSVALIDATE_<OBJECTCLASS>_NO_GLOBAL_TRANSACTION = 1 wraps each
1085 // record in its own transaction instead of all-or-nothing. Modules
1086 // dealing with external systems (e.g. VeriFactu / AEAT for invoices)
1087 // can enable it on the list pages they need so a mid-loop failure does
1088 // not roll back the records already committed-and-pushed outside the
1089 // database. The default remains the secured all-or-nothing mode (#37365).
1090 $perrecordtransaction = (int) getDolGlobalInt('MAIN_MASSVALIDATE_'.strtoupper($objectclass).'_NO_GLOBAL_TRANSACTION');
1091 if (!$perrecordtransaction) {
1092 $db->begin();
1093 }
1094
1095 $nbok = 0;
1096 foreach ($toselect as $toselectid) {
1097 if ($perrecordtransaction) {
1098 $db->begin();
1099 }
1100 $result = $objecttmp->fetch($toselectid);
1101 if ($result > 0) {
1102 if (method_exists($objecttmp, 'validate')) {
1103 $result = $objecttmp->validate($user);
1104 } elseif (method_exists($objecttmp, 'setValid')) {
1105 $result = $objecttmp->setValid($user);
1106 } else {
1107 $objecttmp->error = 'No method validate or setValid on this object';
1108 $result = -1;
1109 }
1110 if ($result == 0) {
1111 $langs->load("errors");
1112 setEventMessages($langs->trans("ErrorObjectMustHaveStatusDraftToBeValidated", $objecttmp->ref), null, 'errors');
1113 $error++;
1114 if ($perrecordtransaction) {
1115 $db->rollback();
1116 }
1117 break;
1118 } elseif ($result < 0) {
1119 setEventMessages($objecttmp->error, $objecttmp->errors, 'errors');
1120 $error++;
1121 if ($perrecordtransaction) {
1122 $db->rollback();
1123 }
1124 break;
1125 } else {
1126 // validate() rename pdf but do not regenerate
1127 // Define output language
1128 if (!getDolGlobalString('MAIN_DISABLE_PDF_AUTOUPDATE')) {
1129 $outputlangs = $langs;
1130 $newlang = '';
1131 if (getDolGlobalInt('MAIN_MULTILANGS') /* && empty($newlang) */ && GETPOST('lang_id', 'aZ09')) {
1132 $newlang = GETPOST('lang_id', 'aZ09');
1133 }
1134 if (getDolGlobalInt('MAIN_MULTILANGS') && empty($newlang) && property_exists($objecttmp, 'thirdparty')) {
1135 if ((property_exists($objecttmp, 'socid') || property_exists($objecttmp, 'fk_soc')) && empty($objecttmp->thirdparty)) {
1136 $objecttmp->fetch_thirdparty();
1137 }
1138 if (!empty($objecttmp->thirdparty)) {
1139 $newlang = $objecttmp->thirdparty->default_lang;
1140 }
1141 }
1142 if (!empty($newlang)) {
1143 $outputlangs = new Translate("", $conf);
1144 $outputlangs->setDefaultLang($newlang);
1145 $outputlangs->load('products');
1146 }
1147 $model = $objecttmp->model_pdf;
1148 $ret = $objecttmp->fetch($objecttmp->id); // Reload to get new records
1149
1150 // To be sure vars is defined
1151 $hidedetails = isset($hidedetails) ? $hidedetails : (getDolGlobalString('MAIN_GENERATE_DOCUMENTS_HIDE_DETAILS') ? 1 : 0);
1152 $hidedesc = isset($hidedesc) ? $hidedesc : (getDolGlobalString('MAIN_GENERATE_DOCUMENTS_HIDE_DESC') ? 1 : 0);
1153 $hideref = isset($hideref) ? $hideref : (getDolGlobalString('MAIN_GENERATE_DOCUMENTS_HIDE_REF') ? 1 : 0);
1154 $moreparams = isset($moreparams) ? $moreparams : null;
1155
1156 $result = $objecttmp->generateDocument($model, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams);
1157 if ($result < 0) {
1158 setEventMessages($objecttmp->error, $objecttmp->errors, 'errors');
1159 }
1160 }
1161 $nbok++;
1162 if ($perrecordtransaction) {
1163 $db->commit();
1164 }
1165 }
1166 } else {
1167 setEventMessages($objecttmp->error, $objecttmp->errors, 'errors');
1168 $error++;
1169 if ($perrecordtransaction) {
1170 $db->rollback();
1171 }
1172 break;
1173 }
1174 }
1175
1176 if (!$error) {
1177 if ($nbok > 1) {
1178 setEventMessages($langs->trans("RecordsModified", $nbok), null, 'mesgs');
1179 } else {
1180 setEventMessages($langs->trans("RecordModifiedSuccessfully"), null, 'mesgs');
1181 }
1182 if (!$perrecordtransaction) {
1183 $db->commit();
1184 }
1185 } elseif (!$perrecordtransaction) {
1186 $db->rollback();
1187 }
1188 }
1189}
1190
1191// Delete record from mass action (massaction = 'delete' for direct delete, action/confirm='delete'/'yes' with a confirmation step before)
1192if (!$error && ($massaction == 'delete' || ($action == 'delete' && $confirm == 'yes')) && $permissiontodelete) {
1193 $db->begin();
1194
1195 $objecttmp = new $objectclass($db);
1196 $nbok = 0;
1197 $nbignored = 0;
1199 $TMsg = array();
1200
1201 //$toselect could contain duplicate entries, cf https://github.com/Dolibarr/dolibarr/issues/26244
1202 $unique_arr = array_unique($toselect);
1203 foreach ($unique_arr as $toselectid) {
1204 $result = $objecttmp->fetch($toselectid);
1205 if ($result > 0) {
1206 // Refuse deletion for some objects/status
1207 if ($objectclass == 'Facture' && $objecttmp->status != Facture::STATUS_DRAFT) {
1208 $langs->load("errors");
1209 $nbignored++;
1210 $TMsg[] = '<div class="error">'.$langs->trans('ErrorOnlyDraftStatusCanBeDeletedInMassAction', $objecttmp->ref).'</div><br>';
1211 continue;
1212 }
1213
1214 if (method_exists($objecttmp, 'is_erasable') && $objecttmp->is_erasable() <= 0) {
1215 $langs->load("errors");
1216 $nbignored++;
1217 $TMsg[] = '<div class="error">'.$langs->trans('ErrorRecordHasChildren').' '.$objecttmp->ref.'</div><br>';
1218 continue;
1219 }
1220
1221 if ($objectclass == 'Holiday' && ! in_array($objecttmp->statut, array(Holiday::STATUS_DRAFT, Holiday::STATUS_CANCELED, Holiday::STATUS_REFUSED))) {
1222 $langs->load("errors");
1223 $nbignored++;
1224 $TMsg[] = '<div class="error">'.$langs->trans('ErrorLeaveRequestMustBeDraftCanceledOrRefusedToBeDeleted', $objecttmp->ref).'</div><br>';
1225 continue;
1226 }
1227
1228 if ($objectclass == "Task" && $objecttmp->hasChildren() > 0) {
1229 $sql = "UPDATE ".MAIN_DB_PREFIX."projet_task SET fk_task_parent = 0 WHERE fk_task_parent = ".((int) $objecttmp->id);
1230 $res = $db->query($sql);
1231
1232 if (!$res) {
1233 setEventMessage('ErrorRecordParentingNotModified', 'errors');
1234 $error++;
1235 }
1236 }
1237
1238 if ($objecttmp->element == 'societe') {
1240 '@phan-var-force Societe $objecttmp';
1241 // TODO Change signature of delete for Societe
1242 $result = $objecttmp->delete($objecttmp->id, $user, 1);
1243 } else {
1244 $objecttmp->oldcopy = dol_clone($objecttmp);
1245 $result = $objecttmp->delete($user);
1246 }
1247
1248 if (empty($result)) { // if delete returns 0, there is at least one object linked
1249 $TMsg = array_merge($objecttmp->errors, $TMsg);
1250 } elseif ($result < 0) { // if delete returns is < 0, there is an error, we break and rollback later
1251 setEventMessages($objecttmp->error, $objecttmp->errors, 'errors');
1252 $error++;
1253 break;
1254 } else {
1255 $nbok++;
1256 }
1257 } else {
1258 setEventMessages($objecttmp->error, $objecttmp->errors, 'errors');
1259 $error++;
1260 break;
1261 }
1262 }
1263
1264 if (empty($error)) {
1265 // Message for elements well deleted
1266 if ($nbok > 1) {
1267 setEventMessages($langs->trans("RecordsDeleted", $nbok), null, 'mesgs');
1268 } elseif ($nbok > 0) {
1269 setEventMessages($langs->trans("RecordDeleted", $nbok), null, 'mesgs');
1270 } else {
1271 setEventMessages($langs->trans("NoRecordDeleted"), null, 'mesgs');
1272 }
1273
1274 // Message for elements which can't be deleted
1275 if (!empty($TMsg)) {
1276 sort($TMsg);
1277 setEventMessages('', array_unique($TMsg), 'warnings');
1278 }
1279
1280 $db->commit();
1281 } else {
1282 $db->rollback();
1283 }
1284}
1285
1286// Generate document foreach object according to model linked to object
1287// @todo : propose model selection
1288if (!$error && $massaction == 'generate_doc' && $permissiontoread) {
1289 // Complete with classes that use this massaction
1290 <<<'EOPHAN'
1291@phan-var-force 'Commande'|'CommandeFournisseur'|'Contrat'|'Expedition'|'ExpenseReport'|'Facture'|'FactureFournisseur'|'Fichinter'|'Project'|'Propal'|'SupplierProposal' $objectclass
1292EOPHAN;
1293
1294 $db->begin();
1295 $nbok = 0;
1296
1297 $objecttmp = new $objectclass($db);
1298
1299 foreach ($toselect as $toselectid) {
1300 $objecttmp->thirdparty = null; // Clear if another value was already set by fetch_thirdparty later
1301 $result = $objecttmp->fetch($toselectid);
1302
1303 if ($result > 0) {
1304 $outputlangs = $langs;
1305 $newlang = '';
1306
1307 if (getDolGlobalInt('MAIN_MULTILANGS') /* && empty($newlang) */ && GETPOST('lang_id', 'aZ09')) {
1308 $newlang = GETPOST('lang_id', 'aZ09');
1309 }
1310 if (getDolGlobalInt('MAIN_MULTILANGS') && empty($newlang) && isset($objecttmp->thirdparty->default_lang)) {
1311 $newlang = $objecttmp->thirdparty->default_lang; // for proposal, order, invoice, ...
1312 }
1313 if (getDolGlobalInt('MAIN_MULTILANGS') && empty($newlang) && isset($objecttmp->default_lang)) {
1314 $newlang = $objecttmp->default_lang; // for thirdparty @phan-suppress-current-line PhanUndeclaredProperty
1315 }
1316 if (getDolGlobalInt('MAIN_MULTILANGS') && empty($newlang) && empty($objecttmp->thirdparty)) { //load lang from thirdparty
1317 $objecttmp->fetch_thirdparty();
1318 $newlang = $objecttmp->thirdparty->default_lang; // for proposal, order, invoice, ...
1319 }
1320 if (!empty($newlang)) {
1321 $outputlangs = new Translate("", $conf);
1322 $outputlangs->setDefaultLang($newlang);
1323 }
1324
1325 // To be sure vars is defined
1326 $hidedetails = isset($hidedetails) ? $hidedetails : (getDolGlobalString('MAIN_GENERATE_DOCUMENTS_HIDE_DETAILS') ? 1 : 0);
1327 $hidedesc = isset($hidedesc) ? $hidedesc : (getDolGlobalString('MAIN_GENERATE_DOCUMENTS_HIDE_DESC') ? 1 : 0);
1328 $hideref = isset($hideref) ? $hideref : (getDolGlobalString('MAIN_GENERATE_DOCUMENTS_HIDE_REF') ? 1 : 0);
1329 $moreparams = isset($moreparams) ? $moreparams : null;
1330
1331 $result = $objecttmp->generateDocument($objecttmp->model_pdf, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams);
1332
1333 if ($result <= 0) {
1334 setEventMessages($objecttmp->error, $objecttmp->errors, 'errors');
1335 $error++;
1336 break;
1337 } else {
1338 $nbok++;
1339 }
1340 } else {
1341 setEventMessages($objecttmp->error, $objecttmp->errors, 'errors');
1342 $error++;
1343 break;
1344 }
1345 }
1346
1347 if (!$error) {
1348 if ($nbok > 1) {
1349 setEventMessages($langs->trans("RecordsGenerated", $nbok), null, 'mesgs');
1350 } else {
1351 setEventMessages($langs->trans("RecordGenerated", $nbok), null, 'mesgs');
1352 }
1353 $db->commit();
1354 } else {
1355 $db->rollback();
1356 }
1357}
1358
1359if (!$error && ($action == 'affecttag' && $confirm == 'yes') && $permissiontoadd) {
1360 $nbok = 0;
1361 $db->begin();
1362
1363 $affecttag_type = GETPOST('affecttag_type', 'alpha');
1364 if (!empty($affecttag_type)) {
1365 $affecttag_type_array = explode(',', $affecttag_type);
1366 } else {
1367 setEventMessage('CategTypeNotFound', 'errors');
1368 }
1369 if (!empty($affecttag_type_array)) {
1370 // check if tag type submitted exists into Tag Map categorie class
1371 require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php';
1372 $categ = new Categorie($db);
1373 $to_affecttag_type_array = array();
1374 $categ_type_array = $categ->getMapList();
1375 foreach ($categ_type_array as $categdef) {
1376 if (in_array($categdef['code'], $affecttag_type_array)) {
1377 $to_affecttag_type_array[] = $categdef['code'];
1378 }
1379 }
1380
1381 // For each valid categ type set common categ
1382 if (!empty($to_affecttag_type_array)) {
1383 foreach ($to_affecttag_type_array as $categ_type) {
1384 $contcats = GETPOST('contcats_' . $categ_type, 'array');
1385 foreach ($toselect as $toselectid) {
1386 $result = $object->fetch($toselectid);
1387 if ($result > 0) {
1388 $result = $object->setCategoriesCommon($contcats, $categ_type, false);
1389 if ($result > 0) {
1390 $nbok++;
1391 } else {
1392 setEventMessages($object->error, $object->errors, 'errors');
1393 }
1394 } else {
1395 setEventMessages($object->error, $object->errors, 'errors');
1396 $error++;
1397 break;
1398 }
1399 }
1400 }
1401 }
1402 }
1403
1404 if (!$error) {
1405 setEventMessages($langs->trans("RecordsModified", $nbok), null);
1406 $db->commit();
1407 $toselect = array();
1408 } else {
1409 $db->rollback();
1410 }
1411}
1412
1413if (!$error && ($action == 'updateprice' && $confirm == 'yes') && $permissiontoadd) {
1414 '@phan-var-force Product|ProductCustomerPrice $obj';
1415 '@phan-var-force Product|ProductCustomerPrice $object';
1416 $nbok = 0;
1417 $db->begin();
1418 if (GETPOSTISSET('pricerate')) {
1419 $pricepercentage = GETPOSTINT('pricerate');
1420 if ($pricepercentage == 0) {
1421 setEventMessages($langs->trans("RecordsModified", 0), null);
1422 } else {
1423 foreach ($toselect as $toselectid) {
1424 $result = $object->fetch($toselectid);
1425 if ($result > 0) {
1426 if (getDolGlobalString('PRODUCT_PRICE_UNIQ')
1427 || getDolGlobalString('PRODUIT_CUSTOMER_PRICES')) {
1428 if ($object->price_base_type == 'TTC') {
1429 $newprice = $object->price_ttc * (100 + $pricepercentage) / 100;
1430 $minprice = $object->price_min_ttc;
1431 } else {
1432 $newprice = $object->price * (100 + $pricepercentage) / 100;
1433 $minprice = $object->price_min;
1434 }
1435 $res = $object->updatePrice($newprice, $object->price_base_type, $user, $object->tva_tx, $minprice, 0, $object->tva_npr, 0, 0, array(), $object->default_vat_code);
1436 if ($res > 0) {
1437 $nbok++;
1438 } else {
1439 setEventMessages($object->error, $object->errors, 'errors');
1440 }
1441 } elseif (getDolGlobalString('PRODUIT_MULTIPRICES')) {
1442 $maxlevel = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
1443 for ($level = 1; $level <= $maxlevel; $level++) {
1444 if ($object->price_base_type == 'TTC') {
1445 $newprice = $object->multiprices_ttc[$level] * (100 + $pricepercentage) / 100;
1446 $minprice = $object->multiprices_min_ttc[$level];
1447 } else {
1448 $newprice = $object->multiprices[$level] * (100 + $pricepercentage) / 100;
1449 $minprice = $object->multiprices_min[$level];
1450 }
1451 $res = $object->updatePrice($newprice, $object->price_base_type, $user, $object->tva_tx, $minprice, $level, $object->tva_npr, 0, 0, array(), $object->default_vat_code);
1452 if ($res > 0) {
1453 $nbok++;
1454 }
1455 }
1456 }
1457 } else {
1458 setEventMessages($object->error, $object->errors, 'errors');
1459 $error++;
1460 break;
1461 }
1462 }
1463 }
1464 }
1465
1466 if (!$error) {
1467 if ($nbok > 0) {
1468 setEventMessages($langs->trans("RecordsModified", $nbok), null);
1469 }
1470 $db->commit();
1471 $toselect = array();
1472 } else {
1473 $db->rollback();
1474 }
1475}
1476
1477if (!$error && ($action == 'setsupervisor' && $confirm == 'yes') && $permissiontoadd) {
1478 '@phan-var-force User $object';
1479 $nbok = 0;
1480 $db->begin();
1481 $supervisortoset = GETPOSTINT('supervisortoset');
1482 if (!empty($supervisortoset)) {
1483 foreach ($toselect as $toselectid) {
1484 $result = $object->fetch($toselectid);
1485 if ($result > 0) {
1486 $object->fk_user = $supervisortoset;
1487 $res = $object->update($user);
1488 if ($res > 0) {
1489 $nbok++;
1490 } else {
1491 setEventMessages($object->error, $object->errors, 'errors');
1492 }
1493 } else {
1494 setEventMessages($object->error, $object->errors, 'errors');
1495 $error++;
1496 break;
1497 }
1498 }
1499 } else {
1500 setEventMessage('UserNotFound', 'errors');
1501 $error++;
1502 }
1503
1504 if (!$error) {
1505 setEventMessages($langs->trans("RecordsModified", $nbok), null);
1506 $db->commit();
1507 $toselect = array();
1508 } else {
1509 $db->rollback();
1510 }
1511}
1512
1513if (!$error && ($action == 'affectuser' && $confirm == 'yes') && $permissiontoadd) {
1514 $nbok = 0;
1515 $db->begin();
1516
1517 $usertoaffect = GETPOSTINT('usertoaffect');
1518 $projectrole = GETPOST('projectrole');
1519 $tasksrole = GETPOST('tasksrole');
1520 if (!empty($usertoaffect)) {
1521 foreach ($toselect as $toselectid) {
1522 $result = $object->fetch($toselectid);
1523 if ($result > 0) {
1524 $res = $object->add_contact($usertoaffect, $projectrole, 'internal');
1525 if ($res >= 0) {
1526 $taskstatic = new Task($db);
1527 $task_array = $taskstatic->getTasksArray(null, null, $object->id, 0, 0);
1528
1529 foreach ($task_array as $task) {
1530 $tasksToAffect = new Task($db);
1531 $result = $tasksToAffect->fetch($task->id);
1532 if ($result > 0) {
1533 $res = $tasksToAffect->add_contact($usertoaffect, $tasksrole, 'internal');
1534 if ($res < 0) {
1535 setEventMessages($tasksToAffect->error, $tasksToAffect->errors, 'errors');
1536 }
1537 }
1538 }
1539 $nbok++;
1540 } else {
1541 setEventMessages($object->error, $object->errors, 'errors');
1542 }
1543 } else {
1544 setEventMessages($object->error, $object->errors, 'errors');
1545 $error++;
1546 break;
1547 }
1548 }
1549 } else {
1550 setEventMessage('UserNotFound', 'errors');
1551 $error++;
1552 }
1553
1554 if (!$error) {
1555 setEventMessages($langs->trans("RecordsModified", $nbok), null);
1556 $db->commit();
1557 $toselect = array();
1558 } else {
1559 $db->rollback();
1560 }
1561}
1562
1563if (!$error && ($massaction == 'enable' || ($action == 'enable' && $confirm == 'yes')) && $permissiontoadd) {
1564 $db->begin();
1565
1566 $objecttmp = new $objectclass($db);
1567 $nbok = 0;
1568 foreach ($toselect as $toselectid) {
1569 $result = $objecttmp->fetch($toselectid);
1570 if ($result > 0) {
1571 if (in_array($objecttmp->element, array('societe'))) {
1572 $result = $objecttmp->setStatut(1);
1573 }
1574 if ($result <= 0) {
1575 setEventMessages($objecttmp->error, $objecttmp->errors, 'errors');
1576 $error++;
1577 break;
1578 } else {
1579 $nbok++;
1580 }
1581 } else {
1582 setEventMessages($objecttmp->error, $objecttmp->errors, 'errors');
1583 $error++;
1584 break;
1585 }
1586 }
1587
1588 if (!$error) {
1589 if ($nbok > 1) {
1590 setEventMessages($langs->trans("RecordsEnabled", $nbok), null, 'mesgs');
1591 } else {
1592 setEventMessages($langs->trans("RecordEnabled"), null, 'mesgs');
1593 }
1594 $db->commit();
1595 } else {
1596 $db->rollback();
1597 }
1598}
1599
1600if (!$error && ($massaction == 'disable' || ($action == 'disable' && $confirm == 'yes')) && $permissiontoadd) {
1601 $db->begin();
1602
1603 $objecttmp = new $objectclass($db);
1604 $nbok = 0;
1605 foreach ($toselect as $toselectid) {
1606 $result = $objecttmp->fetch($toselectid);
1607 if ($result > 0) {
1608 if (in_array($objecttmp->element, array('societe'))) {
1609 $result = $objecttmp->setStatut(0);
1610 }
1611 if ($result <= 0) {
1612 setEventMessages($objecttmp->error, $objecttmp->errors, 'errors');
1613 $error++;
1614 break;
1615 } else {
1616 $nbok++;
1617 }
1618 } else {
1619 setEventMessages($objecttmp->error, $objecttmp->errors, 'errors');
1620 $error++;
1621 break;
1622 }
1623 }
1624
1625 if (!$error) {
1626 if ($nbok > 1) {
1627 setEventMessages($langs->trans("RecordsDisabled", $nbok), null, 'mesgs');
1628 } else {
1629 setEventMessages($langs->trans("RecordDisabled"), null, 'mesgs');
1630 }
1631 $db->commit();
1632 } else {
1633 $db->rollback();
1634 }
1635}
1636
1637if (!$error && $action == 'confirm_edit_value_extrafields' && $confirm == 'yes' && $permissiontoadd) {
1638 $db->begin();
1639
1640 $objecttmp = new $objectclass($db);
1641 $e = new ExtraFields($db);// fetch optionals attributes and labels
1642 $e->fetch_name_optionals_label($objecttmp->table_element);
1643
1644 $nbok = 0;
1645 $extrafieldKeyToUpdate = GETPOST('extrafield-key-to-update');
1646
1647
1648 foreach ($toselect as $toselectid) {
1650 $objecttmp = new $objectclass($db); // to avoid ghost data
1651 $result = $objecttmp->fetch($toselectid);
1652 if ($result > 0) {
1653 // Fill array 'array_options' with data from add form
1654 $ret = $e->setOptionalsFromPost(null, $objecttmp, $extrafieldKeyToUpdate);
1655 if ($ret > 0) {
1656 $objecttmp->insertExtraFields();
1657 $nbok++;
1658 } else {
1659 $error++;
1660 setEventMessages($objecttmp->error, $objecttmp->errors, 'errors');
1661 }
1662 } else {
1663 setEventMessages($objecttmp->error, $objecttmp->errors, 'errors');
1664 $error++;
1665 break;
1666 }
1667 }
1668
1669 if (!$error) {
1670 if ($nbok > 1) {
1671 setEventMessages($langs->trans("RecordsModified", $nbok), null, 'mesgs');
1672 } elseif ($nbok == 1) {
1673 setEventMessages($langs->trans("RecordModifiedSuccessfully"), null, 'mesgs');
1674 }
1675 $db->commit();
1676 } else {
1677 $db->rollback();
1678 }
1679}
1680
1681if (!$error && ($massaction == 'assignsalerepresentative' || ($action == 'assignsalerepresentative' && $confirm == 'yes')) && $permissiontoadd) {
1682 $db->begin();
1683
1684 $objecttmp = new $objectclass($db);
1685 $nbok = 0;
1686
1687 foreach ($toselect as $toselectid) {
1688 $result = $objecttmp->fetch($toselectid);
1689 if ($result > 0) {
1690 if (in_array($objecttmp->element, array('societe'))) {
1691 $result = $objecttmp->setSalesRep(GETPOST("commercial", "alpha"));
1692 }
1693 if ($result <= 0) {
1694 setEventMessages($objecttmp->error, $objecttmp->errors, 'errors');
1695 $error++;
1696 break;
1697 } else {
1698 $nbok++;
1699 }
1700 } else {
1701 setEventMessages($objecttmp->error, $objecttmp->errors, 'errors');
1702 $error++;
1703 break;
1704 }
1705 }
1706
1707 if (!$error) {
1708 if ($nbok > 1) {
1709 setEventMessages($langs->trans("CommercialsAffected", $nbok), null, 'mesgs');
1710 } else {
1711 setEventMessages($langs->trans("CommercialAffected"), null, 'mesgs');
1712 }
1713 $db->commit();
1714 } else {
1715 $db->rollback();
1716 }
1717}
1718
1719if (!$error && ($massaction == 'unassigncommercial' || ($action == 'unassigncommercial' && $confirm == 'yes')) && $permissiontoadd) {
1720 $db->begin();
1721
1722 $objecttmp = new $objectclass($db);
1723 $nbok = 0;
1724
1725 foreach ($toselect as $toselectid) {
1726 $result = $objecttmp->fetch($toselectid);
1727 if ($result > 0) {
1728 if (in_array($objecttmp->element, array('societe'))) {
1729 $TCommercial = GETPOST("commercial", "alpha");
1730 if (is_array($TCommercial)) {
1731 foreach ($TCommercial as $commercial) {
1732 $result = $objecttmp->del_commercial($user, $commercial);
1733 }
1734 }
1735 }
1736 if ($result <= 0) {
1737 setEventMessages($objecttmp->error, $objecttmp->errors, 'errors');
1738 $error++;
1739 break;
1740 } else {
1741 $nbok++;
1742 }
1743 } else {
1744 setEventMessages($objecttmp->error, $objecttmp->errors, 'errors');
1745 $error++;
1746 break;
1747 }
1748 }
1749
1750 if (!$error) {
1751 if ($nbok > 1) {
1752 setEventMessages($langs->trans("CommercialsDisaffected", $nbok), null, 'mesgs');
1753 } else {
1754 setEventMessages($langs->trans("CommercialDisaffected"), null, 'mesgs');
1755 }
1756 $db->commit();
1757 } else {
1758 $db->rollback();
1759 }
1760}
1761// Approve for leave only
1762if (!$error && ($massaction == 'approveleave' || ($action == 'approveleave' && $confirm == 'yes')) && $permissiontoapprove) {
1763 $db->begin();
1764
1765 $objecttmp = new $objectclass($db);
1766 '@phan-var-force Holiday $objecttmp';
1767 $nbok = 0;
1768 foreach ($toselect as $toselectid) {
1769 $result = $objecttmp->fetch($toselectid);
1770 if ($result > 0) {
1771 if ($objecttmp->status != Holiday::STATUS_VALIDATED) {
1772 setEventMessages($langs->trans('StatusOfRefMustBe', $objecttmp->ref, $langs->transnoentitiesnoconv('Validated')), null, 'warnings');
1773 continue;
1774 }
1775 if ($user->id == $objecttmp->fk_validator) {
1776 $objecttmp->oldcopy = dol_clone($objecttmp, 2); // @phan-suppress-current-line PhanTypeMismatchProperty
1777
1778 $objecttmp->date_valid = dol_now();
1779 $objecttmp->fk_user_valid = $user->id;
1780 $objecttmp->date_approval = dol_now();
1781 $objecttmp->fk_user_approve = $user->id;
1782 $objecttmp->status = Holiday::STATUS_APPROVED;
1783 $objecttmp->statut = $objecttmp->status; // deprecated
1784
1785 $verif = $objecttmp->approve($user);
1786
1787 if ($verif <= 0) {
1788 setEventMessages($objecttmp->error, $objecttmp->errors, 'errors');
1789 $error++;
1790 } else {
1791 $nbok++;
1792 }
1793
1794 // If no SQL error, we redirect to the request form
1795 if (!$error) {
1796 $tmpUser = new User($db);
1797 $result = $tmpUser->fetch($objecttmp->fk_user);
1798
1799 // Calculate number of days consumed
1800 $nbopenedday = num_open_day($objecttmp->date_debut_gmt, $objecttmp->date_fin_gmt, 0, 1, $objecttmp->halfday, $tmpUser->country_id);
1801 $soldeActuel = $objecttmp->getCpforUser($objecttmp->fk_user, $objecttmp->fk_type);
1802 $newSolde = ($soldeActuel - $nbopenedday);
1803
1804 // With HOLIDAY_DECREASE_AT_END_OF_MONTH, the balance is decreased at the end of the month by updateSoldeCP(), as for an approval from the card
1805 if (!getDolGlobalInt('HOLIDAY_DECREASE_AT_END_OF_MONTH')) {
1806 // The modification is added to the LOG
1807 $result = $objecttmp->addLogCP($user->id, $objecttmp->fk_user, $langs->transnoentitiesnoconv("Holidays"), $newSolde, $objecttmp->fk_type);
1808 if ($result < 0) {
1809 $error++;
1810 setEventMessages(null, $objecttmp->errors, 'errors');
1811 }
1812
1813 // Update balance
1814 $result = $objecttmp->updateSoldeCP($objecttmp->fk_user, $newSolde, $objecttmp->fk_type);
1815 if ($result < 0) {
1816 $error++;
1817 setEventMessages(null, $objecttmp->errors, 'errors');
1818 }
1819 }
1820 }
1821
1822 if (!$error) {
1823 // To
1824 $destinataire = new User($db);
1825 $destinataire->fetch($objecttmp->fk_user);
1826 $emailTo = $destinataire->email;
1827
1828 if (!$emailTo) {
1829 dol_syslog("User that request leave has no email, so we redirect directly to finished page without sending email");
1830 } else {
1831 // From
1832 $expediteur = new User($db);
1833 $expediteur->fetch($objecttmp->fk_validator);
1834 //$emailFrom = $expediteur->email; Email of user can be an email into another company. Sending will fails, we must use the generic email.
1835 $emailFrom = getDolGlobalString('MAIN_MAIL_EMAIL_FROM');
1836
1837 // Subject
1838 $societeName = getDolGlobalString('MAIN_INFO_SOCIETE_NOM');
1839 if (getDolGlobalString('MAIN_APPLICATION_TITLE')) {
1840 $societeName = getDolGlobalString('MAIN_APPLICATION_TITLE');
1841 }
1842
1843 $subject = $societeName." - ".$langs->transnoentitiesnoconv("HolidaysValidated");
1844
1845 // Content
1846 $message = $langs->transnoentitiesnoconv("Hello")." ".$destinataire->firstname.",\n";
1847 $message .= "\n";
1848
1849 $message .= $langs->transnoentities("HolidaysValidatedBody", dol_print_date($objecttmp->date_debut, 'day'), dol_print_date($objecttmp->date_fin, 'day'))."\n";
1850
1851 $message .= "- ".$langs->transnoentitiesnoconv("ValidatedBy")." : ".dolGetFirstLastname($expediteur->firstname, $expediteur->lastname)."\n";
1852
1853 $message .= "- ".$langs->transnoentitiesnoconv("Link")." : ".$dolibarr_main_url_root."/holiday/card.php?id=".$objecttmp->id."\n\n";
1854 $message .= "\n";
1855
1856 $trackid = 'leav'.$objecttmp->id;
1857
1858 require_once DOL_DOCUMENT_ROOT.'/core/class/CMailFile.class.php';
1859 $mail = new CMailFile($subject, $emailTo, $emailFrom, $message, array(), array(), array(), '', '', 0, 0, '', '', $trackid);
1860
1861 // Sending email
1862 $result = $mail->sendfile();
1863
1864 if (!$result) {
1865 setEventMessages($mail->error, $mail->errors, 'warnings'); // Show error, but do no make rollback, so $error is not set to 1
1866 $action = '';
1867 }
1868 }
1869 }
1870 } else {
1871 $langs->load("errors");
1872 setEventMessages($langs->trans('ErrorNotApproverForHoliday', $objecttmp->ref), null, 'errors');
1873 }
1874 } else {
1875 setEventMessages($objecttmp->error, $objecttmp->errors, 'errors');
1876 $error++;
1877 break;
1878 }
1879 }
1880
1881 if (!$error) {
1882 if ($nbok > 1) {
1883 setEventMessages($langs->trans("RecordsApproved", $nbok), null, 'mesgs');
1884 } elseif ($nbok == 1) {
1885 setEventMessages($langs->trans("RecordAproved"), null, 'mesgs');
1886 }
1887 $db->commit();
1888 } else {
1889 $db->rollback();
1890 }
1891}
1892
1893if (!$error && ($massaction == 'increaseholiday' || ($action == 'increaseholiday' && $confirm == 'yes')) && $permissiontoapprove && is_array($toselect)) {
1894 '@phan-var-force Holiday $holiday'; // Supposing that $holiday is set, it is needed.
1896 $db->begin();
1897 $objecttmp = new $objectclass($db);
1898 $nbok = 0;
1899 $typeholiday = GETPOSTINT('typeholiday');
1900 $nbdaysholidays = GETPOSTFLOAT('nbdaysholidays'); // May be 1.5
1901
1902 if ($nbdaysholidays <= 0) {
1903 setEventMessages($langs->trans("WrongAmount"), null, 'errors');
1904 $error++;
1905 }
1906
1907 if (!$error) {
1908 foreach ($toselect as $toselectid) {
1909 $balancecpuser = $objecttmp->getCPforUser($toselectid, $typeholiday);
1910 if (!empty($balancecpuser)) {
1911 $newnbdaysholidays = $nbdaysholidays + $balancecpuser;
1912 } else {
1913 $newnbdaysholidays = $nbdaysholidays;
1914 }
1915 $result = $holiday->addLogCP($user->id, $toselectid, $langs->transnoentitiesnoconv('ManualUpdate'), $newnbdaysholidays, $typeholiday);
1916 if ($result <= 0) {
1917 setEventMessages($holiday->error, $holiday->errors, 'errors');
1918 $error++;
1919 break;
1920 }
1921
1922 $objecttmp->updateSoldeCP($toselectid, $newnbdaysholidays, $typeholiday);
1923 if ($result > 0) {
1924 $nbok++;
1925 } else {
1926 setEventMessages($langs->trans("ErrorUpdatingUsersCP"), null, 'errors');
1927 $error++;
1928 break;
1929 }
1930 }
1931 }
1932
1933 if (!$error) {
1934 if ($nbok > 1) {
1935 setEventMessages($langs->trans("HolidayRecordsIncreased", $nbok), null, 'mesgs');
1936 } elseif ($nbok == 1) {
1937 setEventMessages($langs->trans("HolidayRecordIncreased"), null, 'mesgs');
1938 }
1939 $db->commit();
1940 $toselect = array();
1941 } else {
1942 $db->rollback();
1943 }
1944}
1945
1946
1947if (!$error && ($massaction == 'clonetasks' || ($action == 'clonetasks' && $confirm == 'yes'))) { // Test on permission not required here, done later
1948 $num = 0;
1949
1950 include_once DOL_DOCUMENT_ROOT.'/projet/class/task.class.php';
1951
1952 $origin_task = new Task($db);
1953 $clone_task = new Task($db);
1954 $newproject = new Project($db);
1955 $newproject->fetch(GETPOSTINT('projectid'));
1956
1957 // Check if current user is contact of the new project (necessary only if project is not public)
1958 $iscontactofnewproject = 0;
1959 if (empty($newproject->public)) {
1960 $tmps = $newproject->getProjectsAuthorizedForUser($user, 0, 1, 0, '(fk_statut:=:1)'); // We check only open project (cloning on closed is not allowed)
1961 $tmparray = explode(',', $tmps);
1962 if (in_array($newproject->id, $tmparray)) {
1963 $iscontactofnewproject = 1;
1964 }
1965 }
1966
1967 // Check permission on new project
1968 $permisstiontoadd = false;
1969 if ($user->hasRight('project', 'all', 'creer') || ($user->hasRight('project', 'creer') && ($newproject->public || $iscontactofnewproject))) {
1970 $permisstiontoadd = true;
1971 }
1972
1973 if ($permisstiontoadd) {
1974 $taskidsmapping = array(); // old task id => new cloned task id
1975 $clonedtaskoldparent = array(); // new cloned task id => old parent task id
1976
1977 // Build the list of projects the current user is allowed to read, used to authorize
1978 // every source task against its actual project before cloning it (the previous fix for
1979 // CVE-2026-77923 only validated the destination project).
1980 $authorizedsourceprojects = null;
1981 if (!$user->hasRight('projet', 'all', 'lire')) {
1982 $sourceprojectstatic = new Project($db);
1983 $tmps = $sourceprojectstatic->getProjectsAuthorizedForUser($user, 0, 1, 0);
1984 $authorizedsourceprojects = explode(',', $tmps);
1985 }
1986
1987 foreach (GETPOST('selected') as $task) {
1988 if ($origin_task->fetch($task, '', 0) <= 0) {
1989 continue; // Source task not found, skip it
1990 }
1991
1992 // Authorize the source task against its actual project before cloning it
1993 if (is_array($authorizedsourceprojects) && !in_array($origin_task->fk_project, $authorizedsourceprojects)) {
1994 setEventMessages($langs->trans('NotEnoughPermissions'), null, 'errors');
1995 $error++;
1996 break;
1997 }
1998
1999 $defaultref = '';
2000 $classnamemodtask = getDolGlobalString('PROJECT_TASK_ADDON', 'mod_task_simple');
2001 if (getDolGlobalString('PROJECT_TASK_ADDON') && is_readable(DOL_DOCUMENT_ROOT . "/core/modules/project/task/" . getDolGlobalString('PROJECT_TASK_ADDON') . ".php")) {
2002 require_once DOL_DOCUMENT_ROOT . "/core/modules/project/task/" . getDolGlobalString('PROJECT_TASK_ADDON') . '.php';
2003 $modTask = new $classnamemodtask();
2004 '@phan-var-force ModeleNumRefTask $modTask';
2006 $defaultref = $modTask->getNextValue(null, $clone_task);
2007 }
2008
2009 if (!$error) {
2010 $clone_task->fk_project = GETPOSTINT('projectid');
2011 $clone_task->ref = $defaultref;
2012 $clone_task->label = $origin_task->label;
2013 $clone_task->description = $origin_task->description;
2014 $clone_task->planned_workload = $origin_task->planned_workload;
2015 $clone_task->fk_task_parent = $origin_task->fk_task_parent;
2016 $clone_task->date_c = dol_now();
2017 $clone_task->date_start = $origin_task->date_start;
2018 $clone_task->date_end = $origin_task->date_end;
2019 $clone_task->progress = $origin_task->progress;
2020
2021 // Fill array 'array_options' with data from add form
2022 $ret = $extrafields->setOptionalsFromPost(null, $clone_task);
2023
2024 $taskid = $clone_task->create($user);
2025
2026 if ($taskid > 0) {
2027 $result = $clone_task->add_contact(GETPOSTINT("userid"), 'TASKEXECUTIVE', 'internal');
2028 $taskidsmapping[$task] = $taskid; // remember old id => new id
2029 $clonedtaskoldparent[$taskid] = $origin_task->fk_task_parent; // remember new id => old parent id
2030 $num++;
2031 } else {
2032 if ($db->lasterrno() == 'DB_ERROR_RECORD_ALREADY_EXISTS') {
2033 $langs->load("projects");
2034 setEventMessages($langs->trans('NewTaskRefSuggested'), null, 'warnings');
2035 $duplicate_code_error = true;
2036 } else {
2037 setEventMessages($clone_task->error, $clone_task->errors, 'errors');
2038 }
2039 $action = 'list';
2040 $error++;
2041 }
2042 }
2043 }
2044
2045 // Remap the parent of cloned tasks: if a cloned task's original parent was also cloned, point it to the
2046 // new parent instead of the old id, otherwise the cloned sub-task would reference a task from the source
2047 // project and end up orphaned (#39596). If the old parent was not cloned, detach it (set to 0).
2048 if (!$error) {
2049 foreach ($clonedtaskoldparent as $newtaskid => $oldparentid) {
2050 if (empty($oldparentid)) {
2051 continue;
2052 }
2053 $newparentid = isset($taskidsmapping[$oldparentid]) ? $taskidsmapping[$oldparentid] : 0;
2054 $remaptask = new Task($db);
2055 if ($remaptask->fetch($newtaskid) > 0 && $remaptask->fk_task_parent != $newparentid) {
2056 $remaptask->fk_task_parent = $newparentid;
2057 if ($remaptask->update($user) < 0) {
2058 setEventMessages($remaptask->error, $remaptask->errors, 'errors');
2059 $error++;
2060 }
2061 }
2062 }
2063 }
2064
2065 if (!$error) {
2066 setEventMessages($langs->trans('NumberOfTasksCloned', $num), null, 'mesgs');
2067 header("Location: ".DOL_URL_ROOT.'/projet/tasks.php?id='.GETPOSTINT('projectid'));
2068 exit();
2069 }
2070 } else {
2071 setEventMessages($langs->trans('NotEnoughPermissions'), null, 'errors');
2072 }
2073}
2074
2075if (!$error && $action == 'createcreditnote' && $permissiontoadd) {
2076 // Security check to avoid creating credit note if stock calculation on bill is enabled
2077 if (getDolGlobalInt('STOCK_CALCULATE_ON_BILL')) {
2078 setEventMessages($langs->trans('CreditNoteNotCreatedStockCalculateOnBillEnabled'), null, 'errors');
2079 header("Location: ".DOL_URL_ROOT.'/compta/facture/list.php?mainmenu=compta&leftmenu=facture');
2080 exit();
2081 }
2082
2083 $objecttmp = new $objectclass($db);
2084 if ($objecttmp->element == 'facture' || $objecttmp->element == 'invoice') {
2085 $nbok = 0;
2087 $TMsg = array();
2088
2089 $unique_arr = array_unique($toselect);
2090 foreach ($unique_arr as $toselectid) {
2091 $sourceinvoice = $toselectid;
2092 $result = $objecttmp->fetch($sourceinvoice);
2093
2094 // We check if invoice is available.
2095 if ($result <= 0) {
2096 setEventMessages($objecttmp->error, $objecttmp->errors, 'errors');
2097 $TMsg[] = $langs->trans('CreditNoteNotCreatedSourceInvoiceNotFound', $sourceinvoice);
2098 continue;
2099 }
2100
2101 // We check if invoice type is supported. If not, we refuse to create credit note.
2102 $isSupportedType = false;
2103 if ($objecttmp->type == Facture::TYPE_STANDARD) {
2104 $isSupportedType = true;
2105 }
2106 if ($objecttmp->type == Facture::TYPE_PROFORMA) {
2107 $isSupportedType = true;
2108 }
2109 if ($objecttmp->type == Facture::TYPE_DEPOSIT && !getDolGlobalString('FACTURE_DEPOSITS_ARE_JUST_PAYMENTS')) {
2110 $isSupportedType = true;
2111 }
2112
2113 if (!$isSupportedType) {
2114 $listtype = array(
2115 Facture::TYPE_STANDARD => $langs->trans("InvoiceStandard"),
2116 Facture::TYPE_DEPOSIT => $langs->trans("InvoiceDeposit"),
2117 Facture::TYPE_CREDIT_NOTE => $langs->trans("InvoiceAvoir"),
2118 Facture::TYPE_REPLACEMENT => $langs->trans("InvoiceReplacement"),
2119 Facture::TYPE_SITUATION => $langs->trans("InvoiceSituation")
2120 );
2121
2122 $TMsg[] = $langs->trans('CreditNoteNotCreatedInvoiceTypeNotSupported', $objecttmp->ref, $listtype[$objecttmp->type]);
2123 continue;
2124 }
2125
2126 // We check if invoice is validated. If not, we refuse to create credit note.
2127 if ($objecttmp->status <= Facture::STATUS_DRAFT) {
2128 $TMsg[] = $langs->trans('CreditNoteNotCreatedInvoiceNotValidated', $objecttmp->ref);
2129 continue;
2130 }
2131
2132 // Test if there is at least one payment or credit note or deposit. If yes, we refuse.
2133 $totalpaid = $objecttmp->getSommePaiement();
2134 $totalcreditnotes = $objecttmp->getSumCreditNotesUsed();
2135 $totaldeposits = $objecttmp->getSumDepositsUsed();
2136 if ($totalpaid > 0) {
2137 $TMsg[] = $langs->trans('CreditNoteNotCreatedPaymentAlreadyDone', $objecttmp->ref);
2138 continue;
2139 }
2140 if ($totalcreditnotes > 0) {
2141 $TMsg[] = $langs->trans('CreditNoteNotCreatedCreditNoteAlreadyUsed', $objecttmp->ref);
2142 continue;
2143 }
2144 if ($totaldeposits > 0) {
2145 $TMsg[] = $langs->trans('CreditNoteNotCreatedDepositAlreadyUsed', $objecttmp->ref);
2146 continue;
2147 }
2148
2149 $db->begin();
2150
2151 // Create credit note
2152 $object = new Facture($db);
2153 $object->entity = $objecttmp->entity;
2154 $object->socid = $objecttmp->socid;
2155 $object->subtype = $objecttmp->subtype;
2156 $object->date = dol_now();
2157 // $object->note_public = '';
2158 // $object->note_private = '';
2159 $object->ref_client = $objecttmp->ref_client;
2160 $object->ref_customer = $objecttmp->ref_customer;
2161 $object->fk_project = $objecttmp->fk_project;
2162 $object->cond_reglement_id = 0; // No payment term for a credit note
2163 $object->fk_account = $objecttmp->fk_account;
2164 $object->fk_incoterms = $objecttmp->fk_incoterms;
2165 $object->location_incoterms = $objecttmp->location_incoterms;
2166 $object->multicurrency_code = $objecttmp->multicurrency_code;
2167 $object->multicurrency_tx = $objecttmp->multicurrency_tx;
2168
2169 $object->fk_facture_source = $sourceinvoice;
2171
2172
2173 if ($objecttmp->isSituationInvoice()) {
2174 $object->situation_counter = $objecttmp->situation_counter;
2175 $object->situation_cycle_ref = $objecttmp->situation_cycle_ref;
2176 $objecttmp->fetchPreviousNextSituationInvoice();
2177 }
2178
2179
2180 $id = $object->create($user);
2181 if ($id < 0) {
2182 setEventMessages($object->error, $object->errors, 'errors');
2183 $TMsg[] = $langs->trans('CreditNoteNotCreated', $objecttmp->ref);
2184 $db->rollback();
2185 continue;
2186 } else {
2187 $facture_source = $objecttmp;
2188
2189 // Copy linked contacts
2190 $object->copy_linked_contact($objecttmp, 'internal');
2191 $object->copy_linked_contact($objecttmp, 'external');
2192
2193 // Copy lines
2194 if (!empty($facture_source->lines)) {
2195 $fk_parent_line = 0;
2196
2197 foreach ($facture_source->lines as $line) {
2198 // Extrafields
2199 if (method_exists($line, 'fetch_optionals')) {
2200 // load extrafields
2201 $line->fetch_optionals();
2202 }
2203
2204 // Reset fk_parent_line for no child products and special product
2205 if (($line->product_type != 9 && empty($line->fk_parent_line)) || $line->product_type == 9) {
2206 $fk_parent_line = 0;
2207 }
2208
2209 if ($facture_source->isSituationInvoice()) {
2210 $source_fk_prev_id = $line->fk_prev_id; // temporary storing situation invoice fk_prev_id
2211 $line->fk_prev_id = $line->id; // The new line of the new credit note we are creating must be linked to the situation invoice line it is created from
2212
2213 if (!empty($facture_source->tab_previous_situation_invoice)) {
2214 // search the last standard invoice in cycle and the possible credit note between this last and facture_source
2215 // TODO Move this out of loop of $facture_source->lines
2216 $tab_jumped_credit_notes = array();
2217 $lineIndex = count($facture_source->tab_previous_situation_invoice) - 1;
2218 $searchPreviousInvoice = true;
2219 while ($searchPreviousInvoice) {
2220 if ($facture_source->tab_previous_situation_invoice[$lineIndex]->type == Facture::TYPE_SITUATION || $lineIndex < 1) {
2221 $searchPreviousInvoice = false; // find, exit;
2222 break;
2223 } else {
2224 if ($facture_source->tab_previous_situation_invoice[$lineIndex]->type == Facture::TYPE_CREDIT_NOTE) {
2225 $tab_jumped_credit_notes[$lineIndex] = $facture_source->tab_previous_situation_invoice[$lineIndex]->id;
2226 }
2227 $lineIndex--; // go to previous invoice in cycle
2228 }
2229 }
2230
2231 $maxPrevSituationPercent = 0;
2232 foreach ($facture_source->tab_previous_situation_invoice[$lineIndex]->lines as $prevLine) {
2233 if ($prevLine->id == $source_fk_prev_id) {
2234 $maxPrevSituationPercent = max($maxPrevSituationPercent, $prevLine->situation_percent);
2235
2236 //$line->subprice = $line->subprice - $prevLine->subprice;
2237 $line->total_ht -= $prevLine->total_ht;
2238 $line->total_tva -= $prevLine->total_tva;
2239 $line->total_ttc -= $prevLine->total_ttc;
2240 $line->total_localtax1 -= $prevLine->total_localtax1;
2241 $line->total_localtax2 -= $prevLine->total_localtax2;
2242
2243 $line->multicurrency_subprice -= $prevLine->multicurrency_subprice;
2244 $line->multicurrency_total_ht -= $prevLine->multicurrency_total_ht;
2245 $line->multicurrency_total_tva -= $prevLine->multicurrency_total_tva;
2246 $line->multicurrency_total_ttc -= $prevLine->multicurrency_total_ttc;
2247 }
2248 }
2249
2250 // prorata
2251 $line->situation_percent = $maxPrevSituationPercent - $line->situation_percent;
2252
2253 //print 'New line based on invoice id '.$facture_source->tab_previous_situation_invoice[$lineIndex]->id.' fk_prev_id='.$source_fk_prev_id.' will be fk_prev_id='.$line->fk_prev_id.' '.$line->total_ht.' '.$line->situation_percent.'<br>';
2254
2255 // If there is some credit note between last situation invoice and invoice used for credit note generation (note: credit notes are stored as delta)
2256 $maxPrevSituationPercent = 0;
2257 foreach ($tab_jumped_credit_notes as $index => $creditnoteid) {
2258 foreach ($facture_source->tab_previous_situation_invoice[$index]->lines as $prevLine) {
2259 if ($prevLine->fk_prev_id == $source_fk_prev_id) {
2260 $maxPrevSituationPercent = $prevLine->situation_percent;
2261
2262 $line->total_ht -= $prevLine->total_ht;
2263 $line->total_tva -= $prevLine->total_tva;
2264 $line->total_ttc -= $prevLine->total_ttc;
2265 $line->total_localtax1 -= $prevLine->total_localtax1;
2266 $line->total_localtax2 -= $prevLine->total_localtax2;
2267
2268 $line->multicurrency_subprice -= $prevLine->multicurrency_subprice;
2269 $line->multicurrency_total_ht -= $prevLine->multicurrency_total_ht;
2270 $line->multicurrency_total_tva -= $prevLine->multicurrency_total_tva;
2271 $line->multicurrency_total_ttc -= $prevLine->multicurrency_total_ttc;
2272 }
2273 }
2274 }
2275
2276 // prorata
2277 $line->situation_percent += $maxPrevSituationPercent;
2278
2279 //print 'New line based on invoice id '.$facture_source->tab_previous_situation_invoice[$lineIndex]->id.' fk_prev_id='.$source_fk_prev_id.' will be fk_prev_id='.$line->fk_prev_id.' '.$line->total_ht.' '.$line->situation_percent.'<br>';
2280 }
2281 }
2282
2283 $line->fk_facture = $object->id;
2284 $line->fk_parent_line = $fk_parent_line;
2285
2286 $line->subprice = -$line->subprice; // invert price for object
2287 // $line->pa_ht = $line->pa_ht; // we chose to have buy/cost price always positive, so no revert of sign here
2288 $line->total_ht = -$line->total_ht;
2289 $line->total_tva = -$line->total_tva;
2290 $line->total_ttc = -$line->total_ttc;
2291 $line->total_localtax1 = -$line->total_localtax1;
2292 $line->total_localtax2 = -$line->total_localtax2;
2293
2294 $line->multicurrency_subprice = -$line->multicurrency_subprice;
2295 $line->multicurrency_total_ht = -$line->multicurrency_total_ht;
2296 $line->multicurrency_total_tva = -$line->multicurrency_total_tva;
2297 $line->multicurrency_total_ttc = -$line->multicurrency_total_ttc;
2298
2299 $line->context['createcreditnotefrominvoice'] = 1;
2300 $result = $line->insert(0, 1); // When creating credit note with same lines than source, we must ignore error if discount already linked
2301
2302 $object->lines[] = $line; // insert new line in current object
2303
2304 // Defined the new fk_parent_line
2305 if ($result > 0 && $line->product_type == 9) {
2306 $fk_parent_line = $result;
2307 }
2308 }
2309
2310 $object->update_price(1);
2311 }
2312
2313 // Add link between credit note and origin
2314 if (!empty($object->fk_facture_source) && $id > 0) {
2315 $facture_source->fetch($object->fk_facture_source);
2316 $facture_source->fetchObjectLinked();
2317
2318 if (!empty($facture_source->linkedObjectsIds)) {
2319 foreach ($facture_source->linkedObjectsIds as $sourcetype => $TIds) {
2320 $object->add_object_linked($sourcetype, current($TIds));
2321 }
2322 }
2323 }
2324
2325 // We validate credit note
2326 $result = $object->validate($user);
2327 if ($result <= 0) {
2328 setEventMessages($object->error, $object->errors, 'errors');
2329 $TMsg[] = $langs->trans('CreditNoteNotCreatedErrorOnValidation', $objecttmp->ref);
2330 $db->rollback();
2331 continue;
2332 } else {
2333 // We update PDF if not disabled
2334 if (!getDolGlobalString('MAIN_DISABLE_PDF_AUTOUPDATE')) {
2335 $outputlangs = $langs;
2336 $newlang = '';
2337 if (getDolGlobalInt('MAIN_MULTILANGS') && GETPOST('lang_id', 'aZ09')) {
2338 $newlang = GETPOST('lang_id', 'aZ09');
2339 }
2340 if (getDolGlobalInt('MAIN_MULTILANGS') && empty($newlang)) {
2341 $newlang = $object->thirdparty->default_lang;
2342 }
2343 if (!empty($newlang)) {
2344 $outputlangs = new Translate("", $conf);
2345 $outputlangs->setDefaultLang($newlang);
2346 $outputlangs->load('products');
2347 }
2348 $model = $object->model_pdf;
2349
2350 $ret = $object->fetch($id); // Reload to get new records
2351
2352 $result = $object->generateDocument($model, $outputlangs, (int) $hidedetails, (int) $hidedesc, (int) $hideref);
2353 if ($result < 0) {
2354 setEventMessages($object->error, $object->errors, 'errors');
2355 }
2356 }
2357
2358 // Convert to reduction then apply reduction on source invoice
2359 $object->fetch($id);
2360 $object->fetch_thirdparty();
2361
2362 // Check if there is already a discount (protection)
2363 $discountcheck = new DiscountAbsolute($db);
2364 $result = $discountcheck->fetch(0, $object->id);
2365
2366 $canconvert = 0;
2367 if (empty($discountcheck->id)) {
2368 $canconvert = 1; // we can convert credit note into discount if there is no discount already linked to credit note
2369 }
2370
2371 if ($canconvert) {
2372 $amount_ht = $amount_tva = $amount_ttc = array();
2373 $multicurrency_amount_ht = $multicurrency_amount_tva = $multicurrency_amount_ttc = array();
2374
2375 // Loop on each vat rate
2376 $i = 0;
2377 foreach ($object->lines as $line) {
2378 if ($line->product_type < 9 && $line->total_ht != 0) { // Remove lines with product_type greater than or equal to 9 and no need to create discount if amount is null
2379 $keyforvatrate = $line->tva_tx.($line->vat_src_code ? ' ('.$line->vat_src_code.')' : '');
2380
2381 if (!isset($amount_ht[$keyforvatrate])) {
2382 $amount_ht[$keyforvatrate] = 0;
2383 }
2384 $amount_ht[$keyforvatrate] += $line->total_ht;
2385 if (!isset($amount_tva[$keyforvatrate])) {
2386 $amount_tva[$keyforvatrate] = 0;
2387 }
2388 $amount_tva[$keyforvatrate] += $line->total_tva;
2389 if (!isset($amount_ttc[$keyforvatrate])) {
2390 $amount_ttc[$keyforvatrate] = 0;
2391 }
2392 $amount_ttc[$keyforvatrate] += $line->total_ttc;
2393 if (!isset($multicurrency_amount_ht[$keyforvatrate])) {
2394 $multicurrency_amount_ht[$keyforvatrate] = 0;
2395 }
2396 $multicurrency_amount_ht[$keyforvatrate] += $line->multicurrency_total_ht;
2397 if (!isset($multicurrency_amount_tva[$keyforvatrate])) {
2398 $multicurrency_amount_tva[$keyforvatrate] = 0;
2399 }
2400 $multicurrency_amount_tva[$keyforvatrate] += $line->multicurrency_total_tva;
2401 if (!isset($multicurrency_amount_ttc[$keyforvatrate])) {
2402 $multicurrency_amount_ttc[$keyforvatrate] = 0;
2403 }
2404 $multicurrency_amount_ttc[$keyforvatrate] += $line->multicurrency_total_ttc;
2405 $i++;
2406 }
2407 }
2408
2409 // Insert one discount by VAT rate category
2410 $discount = new DiscountAbsolute($db);
2411 $discount->description = '(CREDIT_NOTE)';
2412 $discount->fk_soc = $object->socid;
2413 $discount->socid = $object->socid;
2414 $discount->socid = $object->socid;
2415 $discount->fk_facture_source = $object->id;
2416
2417 $error = 0;
2418 $id_discount = 0;
2419
2420 foreach ($amount_ht as $tva_tx => $xxx) {
2421 $discount->amount_ht = -((float) $amount_ht[$tva_tx]);
2422 $discount->amount_tva = -((float) $amount_tva[$tva_tx]);
2423 $discount->amount_ttc = -((float) $amount_ttc[$tva_tx]);
2424 $discount->total_ht = -((float) $amount_ht[$tva_tx]);
2425 $discount->total_tva = -((float) $amount_tva[$tva_tx]);
2426 $discount->total_ttc = -((float) $amount_ttc[$tva_tx]);
2427 $discount->multicurrency_amount_ht = -((float) $multicurrency_amount_ht[$tva_tx]);
2428 $discount->multicurrency_amount_tva = -((float) $multicurrency_amount_tva[$tva_tx]);
2429 $discount->multicurrency_amount_ttc = -((float) $multicurrency_amount_ttc[$tva_tx]);
2430 $discount->multicurrency_total_ht = -((float) $multicurrency_amount_ht[$tva_tx]);
2431 $discount->multicurrency_total_tva = -((float) $multicurrency_amount_tva[$tva_tx]);
2432 $discount->multicurrency_total_ttc = -((float) $multicurrency_amount_ttc[$tva_tx]);
2433
2434 // Clean vat code
2435 $reg = array();
2436 $vat_src_code = '';
2437 if (preg_match('/\‍((.*)\‍)/', $tva_tx, $reg)) {
2438 $vat_src_code = $reg[1];
2439 $tva_tx = preg_replace('/\s*\‍(.*\‍)/', '', $tva_tx); // Remove code into vatrate.
2440 }
2441
2442 $discount->tva_tx = abs((float) $tva_tx);
2443 $discount->vat_src_code = $vat_src_code;
2444
2445 $id_discount = $discount->create($user);
2446 if ($id_discount < 0) {
2447 $error++;
2448 break;
2449 }
2450 }
2451
2452 if (empty($error) && $id_discount > 0) {
2453 // Set invoice as paid
2454 $result = $object->setPaid($user); // We can close the invoice.
2455 if ($result >= 0) {
2456 $object->fetch($object->id); // Reload properties
2457
2458 // Apply discount on source invoice
2459 $discount = new DiscountAbsolute($db);
2460 $discount->fetch($id_discount);
2461 $result = $discount->link_to_invoice(0, $object->fk_facture_source);
2462 if ($result < 0) {
2463 setEventMessages($discount->error, $discount->errors, 'errors');
2464 $TMsg[] = $langs->trans('CreditNoteNotCreatedErrorOnDiscountLink', $objecttmp->ref);
2465 $db->rollback();
2466 continue;
2467 }
2468
2469 // Set source invoice as paid if amount to pay is 0 after discount application
2470 $objecttmp->fetch($objecttmp->id); // Reload properties
2471 if ($objecttmp->getRemainToPay(0) == 0) {
2472 $result = $objecttmp->setPaid($user);
2473 if ($result < 0) {
2474 setEventMessages($objecttmp->error, $objecttmp->errors, 'errors');
2475 $TMsg[] = $langs->trans('CreditNoteNotCreatedErrorOnSetPaidSourceInvoice', $objecttmp->ref);
2476 $db->rollback();
2477 continue;
2478 } else {
2479 if (!getDolGlobalString('MAIN_DISABLE_PDF_AUTOUPDATE')) {
2480 $outputlangs = $langs;
2481 $newlang = '';
2482 if (getDolGlobalInt('MAIN_MULTILANGS') && GETPOST('lang_id', 'aZ09')) {
2483 $newlang = GETPOST('lang_id', 'aZ09');
2484 }
2485 if (getDolGlobalInt('MAIN_MULTILANGS') && empty($newlang)) {
2486 $objecttmp->fetch_thirdparty();
2487 $newlang = $objecttmp->thirdparty->default_lang;
2488 }
2489 if (!empty($newlang)) {
2490 $outputlangs = new Translate("", $conf);
2491 $outputlangs->setDefaultLang($newlang);
2492 }
2493 $ret = $objecttmp->fetch($objecttmp->id); // Reload to get new records
2494
2495 $result = $objecttmp->generateDocument($objecttmp->model_pdf, $outputlangs, (int) $hidedetails, (int) $hidedesc, (int) $hideref);
2496 if ($result < 0) {
2497 setEventMessages($objecttmp->error, $objecttmp->errors, 'errors');
2498 }
2499 }
2500 }
2501 } else {
2502 // We rollback because we want all or nothing.
2503 $TMsg[] = $langs->trans('CreditNoteNotCreatedErrorOnSourceInvoiceNotFullyPaidAfterDiscount', $objecttmp->ref);
2504 $db->rollback();
2505 continue;
2506 }
2507 } else {
2508 setEventMessages($object->error, $object->errors, 'errors');
2509 $TMsg[] = $langs->trans('CreditNoteNotCreatedErrorOnSetPaid', $objecttmp->ref);
2510 $db->rollback();
2511 continue;
2512 }
2513 } else {
2514 setEventMessages($discount->error, $discount->errors, 'errors');
2515 $TMsg[] = $langs->trans('CreditNoteNotCreatedErrorOnDiscount', $objecttmp->ref);
2516 $db->rollback();
2517 continue;
2518 }
2519 } else {
2520 $TMsg[] = $langs->trans('CreditNoteNotCreatedAlreadyConverted', $objecttmp->ref);
2521 $db->rollback();
2522 continue;
2523 }
2524 }
2525
2526 $nbok++;
2527 $db->commit();
2528 $TMsg[] = $langs->trans('CreditNoteCreated', $object->ref, $objecttmp->ref);
2529 }
2530 }
2531
2532 // Show messages
2533 foreach ($TMsg as $msg) {
2534 setEventMessages($msg, null, 'warnings');
2535 }
2536
2537 // Show success message
2538 if ($nbok > 0) {
2539 setEventMessages($langs->trans('CreditNotesCreated', $nbok), null, 'mesgs');
2540 }
2541
2542 $toselect = array();
2543 header("Location: ".DOL_URL_ROOT.'/compta/facture/list.php?mainmenu=compta&leftmenu=facture');
2544 exit();
2545 } else {
2546 setEventMessages($langs->trans('ThisMassActionIsOnlyForInvoices'), null, 'errors');
2547 header("Location: ".DOL_URL_ROOT.'/compta/facture/list.php?mainmenu=compta&leftmenu=facture');
2548 exit();
2549 }
2550}
2551
2552if (empty($toselect)) {
2553 $toselect = [];
2554}
2555$parameters['toselect'] = &$toselect;
2556$parameters['uploaddir'] = $uploaddir;
2557$parameters['massaction'] = $massaction;
2558$parameters['diroutputmassaction'] = isset($diroutputmassaction) ? $diroutputmassaction : null;
2559
2560// @phan-suppress-next-line PhanTypeMismatchArgumentNullable
2561$reshook = $hookmanager->executeHooks('doMassActions', $parameters, $object, $action); // Note that $action and $object may have been modified by some hooks
2562if ($reshook < 0) {
2563 setEventMessages($hookmanager->error, $hookmanager->errors, 'errors');
2564}
$id
Support class for third parties, contacts, members, users or resources.
Definition account.php:47
if(! $sortfield) if(! $sortorder) $object
Definition account.php:100
global $dolibarr_main_url_root
$object ref
Definition info.php:90
Class to manage members of a foundation.
Class to send emails (with attachments or not) Usage: $mailfile = new CMailFile($subject,...
Class to manage categories.
Class to manage customers orders.
const STATUS_DRAFT
Draft status.
const STATUS_VALIDATED
Validated status.
Class for ConferenceOrBoothAttendee.
Class to manage contact/addresses.
Class to manage absolute discounts.
Class to manage standard extra fields.
Class to manage invoices.
const TYPE_REPLACEMENT
Replacement invoice.
const STATUS_DRAFT
Draft status.
const TYPE_STANDARD
Standard invoice.
const TYPE_SITUATION
Situation invoice.
const TYPE_PROFORMA
Proforma invoice (should not be used.
const STATUS_VALIDATED
Validated (need to be paid)
const TYPE_DEPOSIT
Deposit invoice.
const TYPE_CREDIT_NOTE
Credit note invoice.
const STATUS_VALIDATED
Validated status.
const STATUS_DRAFT
Draft status.
const STATUS_REFUSED
Refused.
const STATUS_CANCELED
Canceled.
const STATUS_APPROVED
Approved.
Class to manage projects.
const STATUS_DRAFT
Draft status.
Class to manage third parties objects (customers, suppliers, prospects...)
Class to manage tasks.
Class to manage translations.
Class to manage Dolibarr users.
num_open_day($timestampStart, $timestampEnd, $inhour=0, $lastday=0, $halfday=0, $countryCodeOrId='', $user_id=0)
Function to return number of working days (and text of units) between two dates (working days)
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $conf
The main.inc.php has been included so the following variable are now defined:
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $db
API class for accounts.
dol_delete_file($file, $disableglob=0, $nophperrors=0, $nohook=0, $object=null, $allowdotdot=false, $indexdatabase=1, $nolog=0)
Remove a file or several files with a mask.
dol_is_file($pathoffile)
Return if path is a file.
dol_dir_list($utf8_path, $types="all", $recursive=0, $filter="", $excludefilter=null, $sortcriteria="name", $sortorder=SORT_ASC, $mode=0, $nohook=0, $relativename="", $donotfollowsymlinks=0, $nbsecondsold=0)
Scan a directory and return a list of files/directories.
Definition files.lib.php:64
dol_most_recent_file($dir, $regexfilter='', $excludefilter=array('(\.meta|_preview.*\.png) $', '^\.'), $nohook=0, $mode=0)
Return file(s) into a directory (by default most recent)
dol_now($mode='gmt')
Return date for now.
setEventMessages($mesg, $mesgs, $style='mesgs', $messagekey='', $noduplicate=0, $attop=0)
Set event messages in dol_events session object.
dol_mimetype($file, $default='application/octet-stream', $mode=0)
Return MIME type of a file from its name with extension.
dol_string_nospecial($str, $newstr='_', $badcharstoreplace='', $badcharstoremove='', $keepspaces=0)
Clean a string from all punctuation characters to use it as a ref or login.
setEventMessage($mesgs, $style='mesgs', $noduplicate=0, $attop=0)
Set event message in dol_events session object.
dol_sanitizeFileName($str, $newstr='_', $unaccent=1, $includequotes=0, $allowdash=0)
Clean a string to use it as a file name.
dolChmod($filepath, $newmask='')
Change mod of a file.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
GETPOST($paramname, $check='alphanohtml', $method=0, $filter=null, $options=null, $noreplace=0, $nodefault=0)
Return value of a param into GET or POST supervariable.
GETPOSTFLOAT($paramname, $rounding='', $option=2)
Return the value of a $_GET or $_POST supervariable, converted into float.
dolGetFirstLastname($firstname, $lastname, $nameorder=-1)
Return firstname and lastname in correct order.
dol_concatdesc($text1, $text2, $forxml=false, $invert=false)
Concat 2 descriptions with a new line between them (second operand after first one with appropriate n...
complete_substitutions_array(&$substitutionarray, $outputlangs, $object=null, $parameters=null, $callfunc="completesubstitutionarray")
Complete the $substitutionarray with more entries coming from external module that had set the "subst...
make_substitutions($text, $substitutionarray, $outputlangs=null, $converttextinhtmlifnecessary=0)
Make substitution into a text string, replacing keys with vals from $substitutionarray (oldval=>newva...
GETPOSTINT($paramname, $method=0, $nodefault=0)
Return the value of a $_GET or $_POST supervariable, converted into integer.
dol_clone($srcobject, $native=2)
Create a clone of instance of object (new instance with same value for each properties) With native =...
dol_print_date($time, $format='', $tzoutput='auto', $outputlangs=null, $encodetooutput=false, $decorate=0)
Output date in a string format according to outputlangs (or langs if not defined).
dol_print_error($db=null, $error='', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
GETPOSTISSET($paramname)
Return true if we are in a context of submitting the parameter $paramname from a POST of a form.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
isModEnabled($module)
Is Dolibarr module enabled.
get_exdir($num, $level, $alpha, $withoutslash, $object, $modulepart='')
Return a path to have a the directory according to object where files are stored.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
dol_mkdir($dir, $dataroot='', $newmask='')
Creation of a directory (this can create recursive subdir)
pdf_getFormat($outputlangs=null, $mode='setup')
Return array with format properties of default PDF format.
Definition pdf.lib.php:87
pdf_getPDFFont($outputlangs)
Return font name to use for PDF generation.
Definition pdf.lib.php:273
pdf_getInstance($format='', $metric='mm', $pagetype='P')
Return a PDF instance object.
Definition pdf.lib.php:129
dol_hash($chain, $type='0', $nosalt=0, $mode=0)
Returns a hash (non reversible encryption) of a string.