* Copyright (C) 2004-2012 Laurent Destailleur * Copyright (C) 2004 Benoit Mortier * Copyright (C) 2004 Sebastien Di Cintio * Copyright (C) 2004 Eric Seigne * Copyright (C) 2005-2017 Regis Houssin * Copyright (C) 2006 Andre Cianfarani * Copyright (C) 2006 Marc Barilley/Ocebo * Copyright (C) 2007 Franky Van Liedekerke * Copyright (C) 2007 Patrick Raguin * Copyright (C) 2010 Juanjo Menent * Copyright (C) 2010-2021 Philippe Grand * Copyright (C) 2011 Herve Prot * Copyright (C) 2012-2016 Marcos García * Copyright (C) 2012 Cedric Salvador * Copyright (C) 2012-2015 Raphaël Doursenaud * Copyright (C) 2014-2026 Alexandre Spangaro * Copyright (C) 2018-2022 Ferran Marcet * Copyright (C) 2018-2026 Frédéric France * Copyright (C) 2018 Nicolas ZABOURI * Copyright (C) 2018 Christophe Battarel * Copyright (C) 2018 Josep Lluis Amador * Copyright (C) 2023 Joachim Kueter * Copyright (C) 2023 Nick Fragoulis * Copyright (C) 2024-2026 MDW * Copyright (C) 2024 William Mead * Copyright (C) 2026 Lenin Rivas * Copyright (C) 2026 Open-Dsi * Copyright (C) 2026 Jose MARTINEZ * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * \file htdocs/core/class/html.form.class.php * \ingroup core * \brief File of class with all html predefined components */ /** * Class to manage generation of HTML components * Only common components must be here. * * TODO Merge all function load_cache_* and loadCache* (except load_cache_vatrates) into one generic function loadCacheTable */ class Form { /** * @var DoliDB Database handler. */ public $db; /** * @var string Error code (or message) */ public $error = ''; /** * @var string[] Array of error strings */ public $errors = array(); // Some properties used to return data by some methods /** @var array */ public $result; /** @var int Number of lines returned by method to generate combo select */ public $num; // Cache arrays /** @var array */ public $cache_types_paiements = array(); /** @var array */ public $cache_conditions_paiements = array(); /** @var array */ public $cache_transport_mode = array(); /** @var array */ public $cache_availability = array(); /** @var array */ public $cache_demand_reason = array(); /** @var array */ public $cache_types_fees = array(); /** @var array */ public $cache_vatrates = array(); /** @var array */ public $cache_invoice_subtype = array(); /** @var array */ public $cache_rule_for_lines_dates = array(); /** * @var bool Whether the shared phone input JS (country-sync) has been emitted */ private $phoneInputSharedJsLoaded = false; /** * Constructor * * @param DoliDB $db Database handler */ public function __construct($db) { $this->db = $db; } /** * Return an array of Duration Types * * @param Translate $langs Translation to be used * @param bool $plurial return plurial or singular * @param bool $reverse change order of duration types * @return array{y:string,m:string,w:string,d:string,h:string,i:string,s:string} Types of durations */ public function getDurationTypes(Translate $langs, $plurial = true, $reverse = false) { if ($plurial) { $arrayoftypes = [ 'y' => $langs->trans('Years'), 'm' => $langs->trans('Month'), 'w' => $langs->trans('Weeks'), 'd' => $langs->trans('Days'), 'h' => $langs->trans('Hours'), 'i' => $langs->trans('Minutes'), 's' => $langs->trans('Seconds'), ]; } else { $arrayoftypes = [ "y" => $langs->trans("Year"), "m" => $langs->trans("Month"), "w" => $langs->trans("Week"), "d" => $langs->trans("Day"), "h" => $langs->trans("Hour"), "i" => $langs->trans("Minute"), 's' => $langs->trans('Second'), ]; } if ($reverse) { return array_reverse($arrayoftypes); } else { return $arrayoftypes; } } /** * Output key field for an editable field * * @param string $text Text of label or key to translate * @param string $htmlname Name of select field ('edit' prefix will be added) * @param string $preselected Value to show/edit (not used in this function) * @param ?object $object Object (on the page we show) * @param int<0,1>|boolean $perm Permission to allow button to edit parameter. Set it to 0 to have a not edited field. * @param string $typeofdata Type of data ('string' by default, 'email', 'amount:99', 'numeric:99', 'text' or 'textarea:rows:cols', 'datepicker' ('day' do not work, don't know why), 'dayhour' or 'datehourpicker' 'checkbox:ckeditor:dolibarr_zzz:width:height:savemethod:1:rows:cols', 'select;xxx[:class]'...) * @param string $moreparam More param to add on a href URL. * @param int<0,1> $fieldrequired 1 if we want to show field as mandatory using the "fieldrequired" CSS. * @param int<0,3> $notabletag 1=Do not output table tags but output a ':', 2=Do not output table tags and no ':', 3=Do not output table tags but output a ' ' * @param 'id'|'socid'|'projectid' $paramid Key of parameter for id ('id', 'socid') * @param string $help Tooltip help * @return string HTML edit field */ public function editfieldkey($text, $htmlname, $preselected, $object, $perm, $typeofdata = 'string', $moreparam = '', $fieldrequired = 0, $notabletag = 0, $paramid = 'id', $help = '') { global $langs; $ret = ''; // TODO change for compatibility if (getDolGlobalString('MAIN_USE_EDIT_IN_PLACE') && !preg_match('/^select;/', $typeofdata)) { if ($perm) { $tmp = explode(':', $typeofdata); $ret .= '
'; if ($fieldrequired) { $ret .= ''; } if ($help) { $ret .= $this->textwithpicto($langs->trans($text), $help); } else { $ret .= $langs->trans($text); } if ($fieldrequired) { $ret .= ''; } $ret .= '
' . "\n"; } else { if ($fieldrequired) { $ret .= ''; } if ($help) { $ret .= $this->textwithpicto($langs->trans($text), $help); } else { $ret .= $langs->trans($text); } if ($fieldrequired) { $ret .= ''; } } } else { if (empty($notabletag) && $perm) { $ret .= ''; } if (empty($notabletag) && $perm) { $ret .= ''; } if (empty($notabletag) && $perm) { $ret .= '
'; } if ($fieldrequired) { $ret .= ''; } if ($help) { $ret .= $this->textwithpicto($langs->trans($text), $help); } else { $ret .= $langs->trans($text); } if ($fieldrequired) { $ret .= ''; } if (!empty($notabletag)) { $ret .= ' '; } if (empty($notabletag) && $perm) { $ret .= ''; } if ($htmlname && GETPOST('action', 'aZ09') != 'edit' . $htmlname && $perm && is_object($object)) { $ret .= ' 'edit' . $htmlname, $paramid => $object->id], true) . $moreparam . '">'; $ret .= img_edit($langs->trans('Edit'), ($notabletag ? 0 : 1)); $ret .= ''; } if (!empty($notabletag) && $notabletag == 1) { if ($text) { $ret .= ' : '; } else { $ret .= ' '; } } if (!empty($notabletag) && $notabletag == 3) { $ret .= ' '; } if (empty($notabletag) && $perm) { $ret .= '
'; } } return $ret; } /** * Output value of a field for an editable field * * @param string $text Text of label (not used in this function) * @param string $htmlname Name of select field * @param string|int $value Value to show/edit * @param CommonObject|ActionsCardProduct|ActionsCardService $object Object (that we want to show) * @param bool|int<0,1> $perm Permission to allow button to edit parameter * @param string $typeofdata Type of data ('string' by default, 'checkbox', 'email', 'phone', 'amount:99', 'numeric:99', * 'text' or 'textarea:rows:cols%', 'safehtmlstring', 'restricthtml', * 'datepicker' ('day' do not work, don't know why), 'dayhour' or 'datehourpicker', 'ckeditor:dolibarr_zzz:width:height:savemethod:toolbarstartexpanded:rows:cols', 'select;xkey:xval,ykey:yval,...') * @param ?string $editvalue When in edit mode, use this value as $value instead of value (for example, you can provide here a formatted price instead of numeric value, or a select combo). Use '' to use same than $value * @param ?CommonObject $extObject External object ??? * @param string|array|null $custommsg String or Array of custom messages : eg array('success' => 'MyMessage', 'error' => 'MyMessage') * @param string $moreparam More param to add on the form on action href URL parameter * @param int<0,1> $notabletag Do no output table tags * @param string $formatfunc Call a specific method of $object->$formatfunc to output field in view mode (For example: 'dol_print_email') * @param string $paramid Key of parameter for id ('id', 'socid') * @param string $gm 'auto' or 'tzuser' or 'tzuserrel' or 'tzserver' (when $typeofdata is a date) * @param array $moreoptions Array with more options. For example array('addnowlink'=>1), array('valuealreadyhtmlescaped'=>1) * @param string $editaction [=''] use GETPOST default action or set action to edit mode * @return string HTML edit field */ public function editfieldval($text, $htmlname, $value, $object, $perm, $typeofdata = 'string', $editvalue = '', $extObject = null, $custommsg = null, $moreparam = '', $notabletag = 1, $formatfunc = '', $paramid = 'id', $gm = 'auto', $moreoptions = array(), $editaction = '') { global $conf, $langs; $ret = ''; // Check parameters if (empty($typeofdata)) { return 'ErrorBadParameter typeofdata is empty'; } // Clean parameter $typeofdata if ($typeofdata == 'datetime') { $typeofdata = 'dayhour'; } $reg = array(); if (preg_match('/^(\w+)\((\d+)\)$/', $typeofdata, $reg)) { if ($reg[1] == 'varchar') { $typeofdata = 'string'; } elseif ($reg[1] == 'int') { $typeofdata = 'numeric'; } else { return 'ErrorBadParameter ' . $typeofdata; } } // When option to edit inline is activated if (getDolGlobalString('MAIN_USE_EDIT_IN_PLACE') && !preg_match('/^select;|day|datepicker|dayhour|datehourpicker/', $typeofdata)) { // TODO add jquery timepicker and support select $ret .= $this->editInPlace($object, $value, $htmlname, ($perm ? 1 : 0), $typeofdata, $editvalue, $extObject, $custommsg); } else { if ($editaction == '') { $editaction = GETPOST('action', 'aZ09'); } $editmode = ($editaction == 'edit' . $htmlname); if ($editmode) { // edit mode $ret .= "\n"; $ret .= '
'; $ret .= ''; $ret .= ''; $ret .= ''; if (empty($notabletag)) { $ret .= ''; } if (empty($notabletag)) { $ret .= ''; } // Button save-cancel if (empty($notabletag)) { $ret .= ''; } if (empty($notabletag)) { $ret .= '
'; } if (preg_match('/^(string|safehtmlstring|email|phone|url)/', $typeofdata)) { $tmp = explode(':', $typeofdata); $ret .= ''; } elseif (preg_match('/^(integer)/', $typeofdata)) { $tmp = explode(':', $typeofdata); $valuetoshow = price2num($editvalue ? $editvalue : $value, 0); $ret .= ''; } elseif (preg_match('/^(numeric|amount)/', $typeofdata)) { $tmp = explode(':', $typeofdata); $valuetoshow = price2num($editvalue ? $editvalue : $value); $ret .= ''; } elseif (preg_match('/^(checkbox)/', $typeofdata)) { $tmp = explode(':', $typeofdata); $ret .= ''; } elseif (preg_match('/^text/', $typeofdata) || preg_match('/^note/', $typeofdata)) { // if wysiwyg is enabled $typeofdata = 'ckeditor' $tmp = explode(':', $typeofdata); $cols = (empty($tmp[2]) ? '' : $tmp[2]); $morealt = ''; if (preg_match('/%/', $cols)) { $morealt = ' style="width: ' . $cols . '"'; $cols = ''; } $valuetoshow = ($editvalue ? $editvalue : $value); $ret .= '
'; } elseif ($typeofdata == 'day' || $typeofdata == 'datepicker') { $addnowlink = empty($moreoptions['addnowlink']) ? 0 : $moreoptions['addnowlink']; $adddateof = empty($moreoptions['adddateof']) ? '' : $moreoptions['adddateof']; $labeladddateof = empty($moreoptions['labeladddateof']) ? '' : $moreoptions['labeladddateof']; $ret .= $this->selectDate($value, $htmlname, 0, 0, 1, 'form' . $htmlname, 1, $addnowlink, 0, '', '', $adddateof, '', 1, $labeladddateof, '', $gm); } elseif ($typeofdata == 'dayhour' || $typeofdata == 'datehourpicker') { $addnowlink = empty($moreoptions['addnowlink']) ? 0 : $moreoptions['addnowlink']; $adddateof = empty($moreoptions['adddateof']) ? '' : $moreoptions['adddateof']; $labeladddateof = empty($moreoptions['labeladddateof']) ? '' : $moreoptions['labeladddateof']; $ret .= $this->selectDate($value, $htmlname, 1, 1, 1, 'form' . $htmlname, 1, $addnowlink, 0, '', '', $adddateof, '', 1, $labeladddateof, '', $gm); } elseif (preg_match('/^select;/', $typeofdata)) { $arraydata = explode(',', preg_replace('/^select;/', '', $typeofdata)); $arraylist = array(); foreach ($arraydata as $val) { $tmp = explode(':', $val); $tmpkey = str_replace('|', ':', $tmp[0]); $arraylist[$tmpkey] = $tmp[1]; } $ret .= $this->selectarray($htmlname, $arraylist, $value); } elseif (preg_match('/^link/', $typeofdata)) { // TODO Not yet implemented. See code for extrafields } elseif (preg_match('/^ckeditor/', $typeofdata)) { $tmp = explode(':', $typeofdata); // Example: ckeditor:dolibarr_zzz:width:height:savemethod:toolbarstartexpanded:rows:cols:uselocalbrowser require_once DOL_DOCUMENT_ROOT . '/core/class/doleditor.class.php'; $doleditor = new DolEditor($htmlname, ($editvalue ? $editvalue : $value), (empty($tmp[2]) ? '' : $tmp[2]), (empty($tmp[3]) ? 100 : (int) $tmp[3]), (empty($tmp[1]) ? 'dolibarr_notes' : $tmp[1]), 'In', (empty($tmp[5]) ? false : (bool) $tmp[5]), (isset($tmp[8]) ? ($tmp[8] ? true : false) : true), true, (empty($tmp[6]) ? 20 : (int) $tmp[6]), (empty($tmp[7]) ? '100' : $tmp[7])); $ret .= $doleditor->Create(1); } elseif ($typeofdata == 'asis') { $ret .= ($editvalue ? $editvalue : $value); } if (empty($notabletag)) { $ret .= '
'; } //else $ret.='
'; $ret .= ''; if (preg_match('/ckeditor|textarea/', $typeofdata) && empty($notabletag)) { $ret .= '
' . "\n"; } $ret .= ''; if (empty($notabletag)) { $ret .= '
' . "\n"; } $ret .= '
' . "\n"; } else { // view mode if (preg_match('/^email/', $typeofdata)) { $ret .= dol_print_email($value, 0, 0, 0, 0, 1); } elseif (preg_match('/^phone/', $typeofdata)) { $ret .= dol_print_phone($value, '_blank', 32, 1); } elseif (preg_match('/^url/', $typeofdata)) { $ret .= dol_print_url($value, '_blank', 32, 1); } elseif (preg_match('/^(amount|numeric)/', $typeofdata)) { $ret .= ($value != '' ? price($value, 0, $langs, 0, -1, -1, $conf->currency) : ''); } elseif (preg_match('/^checkbox/', $typeofdata)) { $tmp = explode(':', $typeofdata); $ret .= ''; } elseif (preg_match('/^text/', $typeofdata) || preg_match('/^note/', $typeofdata)) { $ret .= dol_htmlwithnojs(dol_string_onlythesehtmltags(dol_htmlentitiesbr($value), 1, 1, 1)); } elseif (preg_match('/^(safehtmlstring|restricthtml)/', $typeofdata)) { // 'restricthtml' is not an allowed type for editfieldval. Value is 'safehtmlstring' $ret .= dol_htmlwithnojs(dol_string_onlythesehtmltags($value)); } elseif ($typeofdata == 'day' || $typeofdata == 'datepicker') { $ret .= '' . dol_print_date($value, 'day', $gm) . ''; } elseif ($typeofdata == 'dayhour' || $typeofdata == 'datehourpicker') { $ret .= '' . dol_print_date($value, 'dayhour', $gm) . ''; } elseif (preg_match('/^select;/', $typeofdata)) { $arraydata = explode(',', preg_replace('/^select;/', '', $typeofdata)); $arraylist = array(); foreach ($arraydata as $val) { $tmp = explode(':', $val); $arraylist[$tmp[0]] = $tmp[1]; } $ret .= $arraylist[$value]; if ($htmlname == 'fk_product_type') { if ($value == 0) { $ret = img_picto($langs->trans("Product"), 'product', 'class="paddingleftonly paddingrightonly colorgrey"') . $ret; } else { $ret = img_picto($langs->trans("Service"), 'service', 'class="paddingleftonly paddingrightonly colorgrey"') . $ret; } } } elseif (preg_match('/^ckeditor/', $typeofdata)) { $tmpcontent = dol_htmlentitiesbr($value); if (getDolGlobalString('MAIN_DISABLE_NOTES_TAB')) { $firstline = preg_replace('/
.*/', '', $tmpcontent); $firstline = preg_replace('/[\n\r].*/', '', $firstline); $tmpcontent = $firstline . ((strlen($firstline) != strlen($tmpcontent)) ? '...' : ''); } // We don't use dol_escape_htmltag to get the html formatting active, but this need we must also // clean data from some dangerous html $ret .= dol_string_onlythesehtmltags(dol_htmlentitiesbr($tmpcontent)); } else { if (empty($moreoptions['valuealreadyhtmlescaped'])) { $ret .= dol_escape_htmltag($value); } else { $ret .= $value; // $value must be already html escaped. } } // Custom format if parameter $formatfunc has been provided if ($formatfunc && method_exists($object, $formatfunc)) { $ret = $object->$formatfunc($ret); } } } return $ret; } /** * Output edit in place form * * @param string $fieldname Name of the field * @param CommonObject $object Object * @param bool|int<0,1> $perm Permission to allow button to edit parameter. Set it to 0 to have a not edited field. * @param string $typeofdata Type of data ('string' by default, 'email', 'amount:99', 'numeric:99', 'text' or 'textarea:rows:cols', 'datepicker' ('day' do not work, don't know why), 'ckeditor:dolibarr_zzz:width:height:savemethod:1:rows:cols', 'select;xxx[:class]'...) * @param string $check Same coe than $check parameter of GETPOST() * @param string $morecss More CSS * @return string HTML code for the edit of alternative language */ public function widgetForTranslation($fieldname, $object, $perm, $typeofdata = 'string', $check = '', $morecss = '') { global $conf, $langs, $extralanguages; $result = ''; // List of extra languages $arrayoflangcode = array(); if (getDolGlobalString('PDF_USE_ALSO_LANGUAGE_CODE')) { $arrayoflangcode[] = getDolGlobalString('PDF_USE_ALSO_LANGUAGE_CODE'); } if (is_array($arrayoflangcode) && count($arrayoflangcode)) { if (!is_object($extralanguages)) { include_once DOL_DOCUMENT_ROOT . '/core/class/extralanguages.class.php'; $extralanguages = new ExtraLanguages($this->db); } $extralanguages->fetch_name_extralanguages('societe'); // ExtraLanguages::fetch_name_extralanguages() leaves $this->attributes empty // when MAIN_USE_ALTERNATE_TRANSLATION_FOR is not configured, so PHP 8 raises // 'Undefined array key' on the read below if we do not guard it (issue #34596). if (empty($extralanguages->attributes[$object->element]) || !is_array($extralanguages->attributes[$object->element]) || empty($extralanguages->attributes[$object->element][$fieldname])) { return ''; // No extralang field to show } $result .= '' . "\n"; $result .= '
'; $s = img_picto($langs->trans("ShowOtherLanguages"), 'language', '', 0, 0, 0, '', 'fa-15 editfieldlang'); $result .= $s; $result .= '
'; $result .= ''; $result .= ''; } return $result; } /** * Output edit in place form * * @param CommonObject $object Object * @param string $value Value to show/edit * @param string $htmlname DIV ID (field name) * @param int $condition Condition to edit * @param string $inputType Type of input ('string', 'numeric', 'datepicker' ('day' do not work, don't know why), 'textarea:rows:cols', 'ckeditor:dolibarr_zzz:width:height:?:1:rows:cols', 'select:loadmethod:savemethod:buttononly') * @param ?string $editvalue When in edit mode, use this value as $value instead of value * @param ?CommonObject $extObject External object * @param string|array|null $custommsg String or Array of custom messages : eg array('success' => 'MyMessage', 'error' => 'MyMessage') * @return string HTML edit in place */ protected function editInPlace($object, $value, $htmlname, $condition, $inputType = 'textarea', $editvalue = null, $extObject = null, $custommsg = null) { $out = ''; // Check parameters if (preg_match('/^text/', $inputType)) { $value = dol_nl2br($value); } elseif (preg_match('/^numeric/', $inputType)) { $value = price($value); } elseif ($inputType == 'day' || $inputType == 'datepicker') { $value = dol_print_date($value, 'day'); } if ($condition) { $element = false; $table_element = false; $fk_element = false; $loadmethod = false; $savemethod = false; $ext_element = false; $button_only = false; $inputOption = ''; $rows = ''; $cols = ''; if (is_object($object)) { $element = $object->element; $table_element = $object->table_element; $fk_element = $object->id; } if (is_object($extObject)) { $ext_element = $extObject->element; } if (preg_match('/^(string|email|numeric)/', $inputType)) { $tmp = explode(':', $inputType); $inputType = $tmp[0]; if (!empty($tmp[1])) { $inputOption = $tmp[1]; } if (!empty($tmp[2])) { $savemethod = $tmp[2]; } $out .= '' . "\n"; } elseif ((preg_match('/^day$/', $inputType)) || (preg_match('/^datepicker/', $inputType)) || (preg_match('/^datehourpicker/', $inputType))) { $tmp = explode(':', $inputType); $inputType = $tmp[0]; if (!empty($tmp[1])) { $inputOption = $tmp[1]; } if (!empty($tmp[2])) { $savemethod = $tmp[2]; } $out .= '' . "\n"; // Use for timestamp format } elseif (preg_match('/^(select|autocomplete)/', $inputType)) { $tmp = explode(':', $inputType); $inputType = $tmp[0]; $loadmethod = $tmp[1]; if (!empty($tmp[2])) { $savemethod = $tmp[2]; } if (!empty($tmp[3])) { $button_only = true; } } elseif (preg_match('/^textarea/', $inputType)) { $tmp = explode(':', $inputType); $inputType = $tmp[0]; $rows = (empty($tmp[1]) ? '8' : $tmp[1]); $cols = (empty($tmp[2]) ? '80' : $tmp[2]); } elseif (preg_match('/^ckeditor/', $inputType)) { $tmp = explode(':', $inputType); $inputType = $tmp[0]; $toolbar = $tmp[1]; if (!empty($tmp[2])) { $width = $tmp[2]; } if (!empty($tmp[3])) { $height = $tmp[3]; } if (!empty($tmp[4])) { $savemethod = $tmp[4]; } if (isModEnabled('fckeditor')) { $out .= '' . "\n"; } else { $inputType = 'textarea'; } } $out .= '' . "\n"; $out .= '' . "\n"; $out .= '' . "\n"; $out .= '' . "\n"; if (!empty($savemethod)) { $out .= '' . "\n"; } if (!empty($ext_element)) { $out .= '' . "\n"; } if (!empty($custommsg)) { if (is_array($custommsg)) { if (!empty($custommsg['success'])) { $out .= '' . "\n"; } if (!empty($custommsg['error'])) { $out .= '' . "\n"; } } else { $out .= '' . "\n"; } } if ($inputType == 'textarea') { $out .= '' . "\n"; $out .= '' . "\n"; } $out .= '' . $value . '' . "\n"; $out .= '' . (!empty($editvalue) ? $editvalue : $value) . '' . "\n"; } else { $out = $value; } return $out; } /** * Show a text and picto with tooltip on text or picto. * Can be called by an instancied $form->textwithtooltip or by a static call Form::textwithtooltip * * @param string $text Text to show * @param string $htmltext HTML content of tooltip. Must be HTML/UTF8 encoded. * @param int<0,3> $tooltipon 1=tooltip on text, 2=tooltip on image, 3=tooltip on both * @param int<-1,1> $direction -1=image is before, 0=no image, 1=image is after * @param string $img Html code for image (use img_xxx() function to get it) * @param string $extracss Add a CSS style to td tags * @param int<0,3> $notabs 0=Include table and tr tags, 1=Do not include table and tr tags, 2=use div, 3=use span * @param string $incbefore Include code before the text * @param int<0,1> $noencodehtmltext Do not encode into html entity the htmltext * @param string $tooltiptrigger ''=Tooltip on hover, 'abc'=Tooltip on click (abc is a unique key) * @param int<0,1> $forcenowrap Force no wrap between text and picto (works with notabs=2 only) * @return string Code html du tooltip (texte+picto) * @see textwithpicto() Use textwithpicto() instead of textwithtooltip if you can. */ public function textwithtooltip($text, $htmltext, $tooltipon = 1, $direction = 0, $img = '', $extracss = '', $notabs = 3, $incbefore = '', $noencodehtmltext = 0, $tooltiptrigger = '', $forcenowrap = 0) { if ($incbefore) { $text = $incbefore . $text; } if (!$htmltext) { return $text; } $direction = (int) $direction; // For backward compatibility when $direction was set to '' instead of 0 $tag = 'td'; if ($notabs == 2) { $tag = 'div'; } if ($notabs == 3) { $tag = 'span'; } // Sanitize tooltip $htmltext = str_replace(array("\r", "\n"), '', $htmltext); $extrastyle = ''; if ($direction < 0) { $extracss = ($extracss ? $extracss : '') . ($notabs != 3 ? ' inline-block' : ''); $extrastyle = 'padding: 0px; padding-left: 2px;'; } if ($direction > 0) { $extracss = ($extracss ? $extracss : '') . ($notabs != 3 ? ' inline-block' : ''); $extrastyle = 'padding: 0px; padding-right: 2px;'; } $classfortooltip = 'classfortooltip'; $s = ''; $textfordialog = ''; if ($tooltiptrigger == '') { $htmltext = str_replace('"', '"', $htmltext); } else { $classfortooltip = 'classfortooltiponclick'; $textfordialog .= ''; } if ($tooltipon == 2 || $tooltipon == 3) { $paramfortooltipimg = ' class="' . $classfortooltip . ($notabs != 3 ? ' inline-block' : '') . ($extracss ? ' ' . $extracss : '') . '" style="padding: 0px;' . ($extrastyle ? ' ' . $extrastyle : '') . '"'; if ($tooltiptrigger == '') { $paramfortooltipimg .= ' title="' . ($noencodehtmltext ? $htmltext : dol_escape_htmltag($htmltext, 1, 0, 'span', 0, 1)) . '"'; // Attribute to put on img tag to store tooltip } else { $paramfortooltipimg .= ' dolid="' . $tooltiptrigger . '"'; } } else { $paramfortooltipimg = ($extracss ? ' class="' . $extracss . '"' : '') . ($extrastyle ? ' style="' . $extrastyle . '"' : ''); // Attribute to put on td text tag } if ($tooltipon == 1 || $tooltipon == 3) { $paramfortooltiptd = ' class="' . ($tooltipon == 3 ? 'cursorpointer ' : '') . $classfortooltip . ($tag != 'td' ? ' inline-block' : '') . ($extracss ? ' ' . $extracss : '') . '" style="padding: 0px;' . ($extrastyle ? ' ' . $extrastyle : '') . '" '; if ($tooltiptrigger == '') { $paramfortooltiptd .= ' title="' . ($noencodehtmltext ? $htmltext : dol_escape_htmltag($htmltext, 1, 0, 'span', 0, 1)) . '"'; // Attribute to put on td tag to store tooltip } else { $paramfortooltiptd .= ' dolid="' . $tooltiptrigger . '"'; } } else { $paramfortooltiptd = ($extracss ? ' class="' . $extracss . '"' : '') . ($extrastyle ? ' style="' . $extrastyle . '"' : ''); // Attribute to put on td text tag } if (empty($notabs)) { $s .= ''; } elseif ($notabs == 2) { $s .= '
'; } // Define value if value is before if ($direction < 0) { $s .= '<' . $tag . $paramfortooltipimg; if ($tag == 'td') { $s .= ' class="valigntop" width="14"'; } $s .= '>' . $textfordialog . $img . ''; } // Use another method to help avoid having a space in value in order to use this value with jquery // Define label if ((string) $text != '') { $s .= '<' . $tag . $paramfortooltiptd . '>' . $text . ''; } // Define value if value is after if ($direction > 0) { $s .= '<' . $tag . $paramfortooltipimg; if ($tag == 'td') { $s .= ' class="valignmiddle" width="14"'; } $s .= '>' . $textfordialog . $img . ''; } if (empty($notabs)) { $s .= '
'; } elseif ($notabs == 2) { $s .= ''; } return $s; } /** * Show a text with a picto and a tooltip on picto * * @param string $text Text to show * @param string $htmltooltip Content of tooltip. Warning: By default we keep only tags. * @param int<-1,1> $direction 1=Icon is after text, -1=Icon is before text, 0=no icon * @param string $type Type of picto ('info', 'infoclickable', 'help', 'helpclickable', 'warning', 'superadmin', 'mypicto@mymodule', ...) or image filepath or 'none' * @param string $extracss Add a CSS style to td, div or span tag * @param int<0,1> $noencodehtmltext Do not encode into html entity the htmltext * @param int<0,3> $notabs 0=Include table and tr tags, 1=Do not include table and tr tags, 2=use div, 3=use span * @param string $tooltiptrigger ''=Tooltip on hover and hidden on smartphone, 'abconsmartphone'=Tooltip on hover and on click on smartphone, 'abc'=Tooltip on click (abc is a unique key, clickable link is on image or on link if param $type='none' or on both if $type='xxxclickable') * @param int<0,1> $forcenowrap Force no wrap between text and picto (works with notabs=2 only) * @return string HTML code of text, picto, tooltip */ public function textwithpicto($text, $htmltooltip, $direction = 1, $type = 'help', $extracss = 'valignmiddle', $noencodehtmltext = 0, $notabs = 3, $tooltiptrigger = '', $forcenowrap = 0) { global $conf, $langs; //For backwards compatibility if ($type == '0') { $type = 'info'; } elseif ($type == '1') { $type = 'help'; } // Clean parameters $tooltiptrigger = preg_replace('/[^a-z0-9]/i', '', $tooltiptrigger); if (preg_match('/onsmartphone$/', $tooltiptrigger) && empty($conf->dol_no_mouse_hover)) { $tooltiptrigger = preg_replace('/^.*onsmartphone$/', '', $tooltiptrigger); } $alt = ''; if ($tooltiptrigger) { $alt = $langs->transnoentitiesnoconv("ClickToShowHelp"); } // If info or help with no javascript, show only text if (empty($conf->use_javascript_ajax)) { if ($type == 'info' || $type == 'infoclickable' || $type == 'help' || $type == 'helpclickable') { return $text; } else { $alt = $htmltooltip; $htmltooltip = ''; } } // If info or help with smartphone, show only text (tooltip hover can't works) if (!empty($conf->dol_no_mouse_hover) && empty($tooltiptrigger)) { if ($type == 'info' || $type == 'infoclickable' || $type == 'help' || $type == 'helpclickable') { return $text; } } // If info or help with smartphone, show only text (tooltip on click does not works with dialog on smaprtphone) //if (!empty($conf->dol_no_mouse_hover) && !empty($tooltiptrigger)) //{ //if ($type == 'info' || $type == 'help') return ''.$text.''; //} $img = ''; if ($type == 'info') { $img = img_help(($tooltiptrigger != '' ? 2 : 0), $alt); } elseif ($type == 'help') { $img = img_help(($tooltiptrigger != '' ? 2 : 1), $alt); } elseif ($type == 'helpclickable') { $img = img_help(($tooltiptrigger != '' ? 2 : 1), $alt); } elseif ($type == 'warning') { $img = img_warning($alt); } elseif ($type != 'none') { // @phan-suppress-next-line PhanPluginSuspiciousParamPosition $img = img_picto($alt, $type); // $type can be an image path } $tooltipon = ((($tooltiptrigger && !$img) || strpos($type, 'clickable')) ? 3 : 2); return $this->textwithtooltip($text, $htmltooltip, $tooltipon, $direction, $img, $extracss, $notabs, '', $noencodehtmltext, $tooltiptrigger, $forcenowrap); } /** * Generate select HTML to choose massaction * * @param string $selected Value auto selected when at least one record is selected. Not a preselected value. Use '0' by default. * @param array $arrayofaction array('code'=>'label', ...). The code is the key stored into the GETPOST('massaction') when submitting action. * @param int $alwaysvisible 1=select button always visible * @param string $name Name for massaction * @param string $cssclass CSS class used to check for select * @return string|void Select list */ public function selectMassAction($selected, $arrayofaction, $alwaysvisible = 0, $name = 'massaction', $cssclass = 'checkforselect') { global $conf, $langs, $hookmanager; $disabled = 0; $ret = '
'; $ret .= ''; if (empty($conf->dol_optimize_smallscreen)) { $ret .= ajax_combobox('.' . $name . 'select'); } // Warning: if you set submit button to disabled, post using 'Enter' will no more work if there is no another input submit. So we add a hidden button $ret .= ''; // Hidden button BEFORE so it is the one used when we submit with ENTER. $ret .= 'use_javascript_ajax) ? '' : ' style="display: none"') . ' class="reposition button smallpaddingimp' . (empty($conf->use_javascript_ajax) ? '' : ' hideobject') . ' ' . $name . ' ' . $name . 'confirmed" value="' . dol_escape_htmltag($langs->trans("Confirm")) . '">'; $ret .= '
'; if (!empty($conf->use_javascript_ajax)) { $ret .= ' '; } return $ret; } // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps /** * Return combo list of activated countries, into language of user * * @param int|string $selected Id or Code or Label of preselected country (depending on $usecodeaskey) * @param string $htmlname Name of html select object * @param string $htmloption More html options on select object * @param integer $maxlength Max length for labels (0=no limit) * @param string $morecss More css class * @param string $usecodeaskey ''=Use id as key (default), 'code3'=Use code on 3 alpha as key, 'code2"=Use code on 2 alpha as key * @param int<0,1>|string $showempty Show empty choice * @param int<0,1> $disablefavorites 1=Disable favorites, * @param int<0,1> $addspecialentries 1=Add dedicated entries for group of countries (like 'European Economic Community', ...) * @param string[] $exclude_country_code Array of country code (iso2) to exclude * @param int<0,1> $hideflags Hide flags * @param int<0,1> $forcecombo Force to load all values and output a standard combobox (with no beautification) * @return string HTML string with select */ public function select_country($selected = '', $htmlname = 'country_id', $htmloption = '', $maxlength = 0, $morecss = 'minwidth300', $usecodeaskey = '', $showempty = 1, $disablefavorites = 0, $addspecialentries = 0, $exclude_country_code = array(), $hideflags = 0, $forcecombo = 0) { // phpcs:enable global $langs, $mysoc; $langs->load("dict"); $selected = (string) $selected; $out = ''; /** @var array $countryArray */ $countryArray = array(); $favorite = array(); $label = array(); $atleastonefavorite = 0; $sql = "SELECT rowid, code as code_iso, code_iso as code_iso3, label, favorite, eec"; $sql .= " FROM " . $this->db->prefix() . "c_country"; $sql .= " WHERE active > 0"; //$sql.= " ORDER BY code ASC"; dol_syslog(get_class($this) . "::select_country", LOG_DEBUG); $resql = $this->db->query($sql); if ($resql) { $out .= ''; } else { dol_print_error($this->db); } // Make select dynamic if (empty($forcecombo)) { include_once DOL_DOCUMENT_ROOT . '/core/lib/ajax.lib.php'; $out .= ajax_combobox('select' . $htmlname, array(), 0, 0, 'resolve'); } return $out; } /** * Return a select list of country phone calling codes * * @param string $selected Preselected phone code value (e.g. "+33") * @param string $htmlname Name of HTML select element * @param string $morecss More CSS classes * @param int $showempty Show empty option (1) or not (0) * @param int $country_id_hint Country ID to prefer when multiple countries share a code * @return string HTML select string */ public function selectPhoneCode($selected = '', $htmlname = 'phone_code', $morecss = 'maxwidth150', $showempty = 0, $country_id_hint = 0) { global $langs; $langs->load("dict"); $out = ''; $codeArray = array(); $favorite = array(); $label = array(); $atleastonefavorite = 0; $sql = "SELECT rowid, code, label, phone_code, favorite, trunk_prefix"; $sql .= " FROM ".$this->db->prefix()."c_country"; $sql .= " WHERE active > 0 AND phone_code IS NOT NULL AND phone_code != ''"; dol_syslog(get_class($this)."::selectPhoneCode", LOG_DEBUG); $resql = $this->db->query($sql); if ($resql) { $num = $this->db->num_rows($resql); $i = 0; while ($i < $num) { $obj = $this->db->fetch_object($resql); $translabel = ($obj->code && $langs->transnoentitiesnoconv("Country".$obj->code) != "Country".$obj->code) ? $langs->transnoentitiesnoconv("Country".$obj->code) : $obj->label; $codeArray[$i]['rowid'] = $obj->rowid; $codeArray[$i]['code'] = $obj->code; $codeArray[$i]['label'] = $translabel; $codeArray[$i]['phone_code'] = '+'.$obj->phone_code; $codeArray[$i]['favorite'] = $obj->favorite; $codeArray[$i]['trunk_prefix'] = $obj->trunk_prefix; $favorite[$i] = $obj->favorite; $label[$i] = dol_string_unaccent($translabel); $i++; } $array1_sort_order = SORT_DESC; $array2_sort_order = SORT_ASC; array_multisort($favorite, $array1_sort_order, $label, $array2_sort_order, $codeArray); $out .= ''; // Picto $out .= img_picto('', $picto, 'class="pictofixedwidth"'); // Phone code select (display-only name, not submitted as separate POST param) $out .= $this->selectPhoneCode($selectedCode, $codename, 'maxwidth75 phone_code_select', 0, $country_id_hint); // Visible number input (no name — not POSTed) $out .= ' 0) { $out .= ' maxlength="'.$maxlength.'"'; } $out .= ' value="'.dol_escape_htmltag($numberValue).'">'; // Per-field JS to sync hidden field $out .= $this->getPhoneInputFieldJs($htmlname, $codename); // Shared JS for country-sync (output once per page) $out .= $this->getPhoneInputSharedJs($countrySelectorId); return $out; } /** * Return inline JS that syncs the hidden phone field from select + text input. * * Strips formatting chars and trunk prefix from the number, then builds * the hidden value as "{code} {number}" or just "{number}" if no code. * * @param string $htmlname Base name (e.g. "phone_pro") * @param string $codename Code select name (e.g. "phone_pro_code") * @return string Inline '."\n"; return $out; } /** * Return inline JS for country-selector → phone code sync (output once per page). * * When the country select changes, fetches the phone code via AJAX and updates * all .phone_code_select dropdowns, which in turn triggers per-field sync. * * @param string $countrySelectorId ID of the country select element * @return string Inline '."\n"; return $out; } /** * Generate HTML table rows for standard object linking (invoices, orders, proposals, etc.). * * This method creates the table body rows with checkboxes for selecting objects to link. * It displays Ref, RefCustomer, AmountHTShort, and Company columns. * * @param CommonObject $object The source object we are linking from * @param string $key The element type key (e.g., 'invoice', 'order', 'propal') * @param array{enabled:bool,perms:int,label:string,sql:string,linkname?:string} $possiblelink Array containing link configuration * @param int $num Number of records returned from the SQL query * @param mysqli_result|resource|true $resqllist Database result resource from the SQL query * @return string HTML table rows for the link selection table */ private function makeAddLinkToObject($object, $key, $possiblelink, $num, $resqllist) { dol_syslog(__METHOD__, LOG_DEBUG); global $langs, $form; if (empty($form)) { $form = new Form($this->db); } $htmltoenteralink = ''; $i = 0; // headers $htmltoenteralink .= ''; $htmltoenteralink .= ''; $htmltoenteralink .= '' . $langs->trans("Ref") . ''; $htmltoenteralink .= '' . $langs->trans("RefCustomer") . ''; $htmltoenteralink .= '' . $langs->trans("AmountHTShort") . ''; $htmltoenteralink .= '' . $langs->trans("Company") . ''; $htmltoenteralink .= ''; // rows with data while ($i < $num) { $objp = $this->db->fetch_object($resqllist); $alreadylinked = false; if (!empty($object->linkedObjectsIds[$possiblelink['linkname'] ?? $key])) { if (in_array($objp->rowid, array_values($object->linkedObjectsIds[$possiblelink['linkname'] ?? $key]))) { $alreadylinked = true; } } $htmltoenteralink .= ''; $htmltoenteralink .= ''; if ($alreadylinked) { $htmltoenteralink .= img_picto('', 'link'); } else { $htmltoenteralink .= ''; } $htmltoenteralink .= ''; $htmltoenteralink .= ''; if (!$alreadylinked) { $htmltoenteralink .= ''; } $htmltoenteralink .= ''; $htmltoenteralink .= '' . (!empty($objp->ref_client) ? $objp->ref_client : (!empty($objp->ref_supplier) ? $objp->ref_supplier : '')) . ''; $htmltoenteralink .= ''; if ($possiblelink['label'] == 'LinkToContract') { $htmltoenteralink .= $form->textwithpicto('', $langs->trans("InformationOnLinkToContract")) . ' '; } $htmltoenteralink .= '' . (isset($objp->total_ht) ? price($objp->total_ht) : '') . ''; $htmltoenteralink .= ''; $htmltoenteralink .= '' . $objp->name . ''; $htmltoenteralink .= ''; $i++; } return $htmltoenteralink; } /** * Generate HTML table rows for conference/booth attendee linking. * * This method creates custom table body rows specifically for ConferenceOrBoothAttendee objects. * It displays Ref, Name, Email, Company, DateOfRegistration, and Project columns. * Uses getNomUrl() for clickable links to attendee, company, and project records. * * @param CommonObject $object The source object we are linking from (e.g., propal, order) * @param string $key The element type key ('conferenceorboothattendee') * @param array{enabled:bool,perms:int,label:string,sql:string,linkname?:string} $possiblelink Array containing link configuration * @param int $num Number of records returned from the SQL query * @param mysqli_result|resource|true $resqllist Database result resource from the SQL query * @return string HTML table rows for the attendee link selection table */ private function makeAddLinkToAttendee($object, $key, $possiblelink, $num, $resqllist) { dol_syslog(__METHOD__, LOG_DEBUG); global $langs, $form; require_once DOL_DOCUMENT_ROOT . '/eventorganization/class/conferenceorboothattendee.class.php'; require_once DOL_DOCUMENT_ROOT . '/societe/class/societe.class.php'; require_once DOL_DOCUMENT_ROOT . '/projet/class/project.class.php'; $attendeestatic = new ConferenceOrBoothAttendee($this->db); $companystatic = new Societe($this->db); $projectstatic = new Project($this->db); if (empty($form)) { $form = new Form($this->db); } $htmltoenteralink = ''; $i = 0; // headers $htmltoenteralink .= ''; $htmltoenteralink .= ''; $htmltoenteralink .= '' . $langs->trans("Ref") . ''; $htmltoenteralink .= '' . $langs->trans("Name") . ''; $htmltoenteralink .= '' . $langs->trans("Email") . ''; $htmltoenteralink .= '' . $langs->trans("Company") . ''; $htmltoenteralink .= '' . $langs->trans("Project") . ''; $htmltoenteralink .= '' . $langs->trans("DateOfRegistration") . ''; $htmltoenteralink .= ''; // rows with data while ($i < $num) { $objp = $this->db->fetch_object($resqllist); $alreadylinked = false; if (!empty($object->linkedObjectsIds[$possiblelink['linkname'] ?? $key])) { if (in_array($objp->rowid, array_values($object->linkedObjectsIds[$possiblelink['linkname'] ?? $key]))) { $alreadylinked = true; } } $htmltoenteralink .= ''; $htmltoenteralink .= ''; if ($alreadylinked) { $htmltoenteralink .= img_picto('', 'link'); } else { $htmltoenteralink .= ''; } $htmltoenteralink .= ''; $fetchattendee = $attendeestatic->fetch($objp->rowid); if ($fetchattendee) { $htmltoenteralink .= '' . $attendeestatic->getNomUrl(0). ''; } else { $htmltoenteralink .= ''; } $htmltoenteralink .= '' . $objp->name . ''; $htmltoenteralink .= '' . $objp->email . ''; $fetchcompany = $companystatic->fetch($objp->socid); if ($fetchcompany) { $htmltoenteralink .= '' . $companystatic->getNomUrl(0). ''; } else { $htmltoenteralink .= '' . $objp->name . ''; } $fetchcproject = $projectstatic->fetch($objp->fk_project); if ($fetchcproject) { $htmltoenteralink .= '' . $projectstatic->getNomUrl(0). ''; } else { $htmltoenteralink .= '' . $objp->fk_project . ''; } $htmltoenteralink .= '' . $objp->date_subscription . ''; $htmltoenteralink .= ''; $i++; } return $htmltoenteralink; } // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps /** * Return select list of incoterms * * @param string $selected Id or Code of preselected incoterm * @param string $location_incoterms Value of input location * @param string $page Defined the form action * @param string $htmlname Name of html select object * @param string $htmloption Options html on select object * @param int<0,1> $forcecombo Force to load all values and output a standard combobox (with no beautification) * @param array}> $events Event options to run on change. Example: array(array('method'=>'getContacts', 'url'=>dol_buildpath('/core/ajax/contacts.php',1), 'htmlname'=>'contactid', 'params'=>array('add-customer-contact'=>'disabled'))) * @param int<0,1> $disableautocomplete Disable autocomplete * @return string HTML string with select and input */ public function select_incoterms($selected = '', $location_incoterms = '', $page = '', $htmlname = 'incoterm_id', $htmloption = '', $forcecombo = 1, $events = array(), $disableautocomplete = 0) { // phpcs:enable global $conf, $langs; $langs->load("dict"); $out = ''; //$moreattrib = ''; $incotermArray = array(); $sql = "SELECT rowid, code"; $sql .= " FROM " . $this->db->prefix() . "c_incoterms"; $sql .= " WHERE active > 0"; $sql .= " ORDER BY code ASC"; dol_syslog(get_class($this) . "::select_incoterm", LOG_DEBUG); $resql = $this->db->query($sql); if ($resql) { if ($conf->use_javascript_ajax && !$forcecombo) { include_once DOL_DOCUMENT_ROOT . '/core/lib/ajax.lib.php'; $out .= ajax_combobox($htmlname, $events); } if (!empty($page)) { $out .= '
'; $out .= ''; $out .= ''; } $out .= ''; $out .= ajax_combobox($htmlname); if ($conf->use_javascript_ajax && empty($disableautocomplete)) { $out .= ajax_multiautocompleter('location_incoterms', array(), DOL_URL_ROOT . '/core/ajax/locationincoterms.php') . "\n"; //$moreattrib .= ' autocomplete="off"'; } $out .= '' . "\n"; if (!empty($page)) { $out .= '
'; } } else { dol_print_error($this->db); } return $out; } // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps /** * Return list of types of lines (product or service) * Example: 0=product, 1=service, 9=other (for external module) * * @param string $selected Preselected type * @param string $htmlname Name of field in html form * @param int<0,1>|string $showempty Add an empty field * @param int $hidetext Do not show label 'Type' before combo box (used only if there is at least 2 choices to select) * @param integer $forceall 1=Force to show products and services in combo list, whatever are activated modules, 0=No force, 2=Force to show only Products, 3=Force to show only services, -1=Force none (and set hidden field to 'service') * @param string $morecss More css * @param int $useajaxcombo 1=Use ajaxcombo * @return void */ public function select_type_of_lines($selected = '', $htmlname = 'type', $showempty = 0, $hidetext = 0, $forceall = 0, $morecss = "", $useajaxcombo = 1) { // phpcs:enable global $langs; // If product & services are enabled or both disabled. if ($forceall == 1 || (empty($forceall) && isModEnabled("product") && isModEnabled("service")) || (empty($forceall) && !isModEnabled('product') && !isModEnabled('service'))) { if (empty($hidetext)) { print $langs->trans("Type").'...'; } print ''; if ($useajaxcombo) { print ajax_combobox('select_' . $htmlname); } //if ($user->admin) print info_admin($langs->trans("YouCanChangeValuesForThisListFromDictionarySetup"),1); } if ((empty($forceall) && !isModEnabled('product') && isModEnabled("service")) || $forceall == 3) { print $langs->trans("Service"); print ''; } if ((empty($forceall) && isModEnabled("product") && !isModEnabled('service')) || $forceall == 2) { print $langs->trans("Product"); print ''; } if ($forceall < 0) { // This should happened only for contracts when both predefined product and service are disabled. print ''; // By default we set on service for contract. If CONTRACT_SUPPORT_PRODUCTS is set, forceall should be 1 not -1 } } // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps /** * Load into cache cache_types_fees, array of types of fees * * @return int Nb of lines loaded, <0 if KO */ public function load_cache_types_fees() { // phpcs:enable global $langs; $num = count($this->cache_types_fees); if ($num > 0) { return 0; // Cache already loaded } dol_syslog(__METHOD__, LOG_DEBUG); $langs->load("trips"); $sql = "SELECT c.code, c.label"; $sql .= " FROM " . $this->db->prefix() . "c_type_fees as c"; $sql .= " WHERE active > 0"; $resql = $this->db->query($sql); if ($resql) { $num = $this->db->num_rows($resql); $i = 0; while ($i < $num) { $obj = $this->db->fetch_object($resql); // If a translation exists, we use is, otherwise, we take the label by default $label = ($obj->code != $langs->trans($obj->code) ? $langs->trans($obj->code) : $langs->trans($obj->label)); $this->cache_types_fees[$obj->code] = $label; $i++; } asort($this->cache_types_fees); return $num; } else { dol_print_error($this->db); return -1; } } // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps /** * Return list of types of notes * * @param string $selected Preselected type * @param string $htmlname Name of field in form * @param int $showempty Add an empty field * @return void */ public function select_type_fees($selected = '', $htmlname = 'type', $showempty = 0) { // phpcs:enable global $user, $langs; dol_syslog(__METHOD__ . " selected=" . $selected . ", htmlname=" . $htmlname, LOG_DEBUG); $this->load_cache_types_fees(); print ''; if ($user->admin) { print info_admin($langs->trans("YouCanChangeValuesForThisListFromDictionarySetup"), 1); } } // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps /** * Output html form to select a third party * This call select_thirdparty_list() or ajax depending on setup. This component is not able to support multiple select. * * @param int|string $selected Preselected ID * @param string $htmlname Name of field in form * @param string $filter Optional filter criteria. WARNING: To avoid SQL injection, only few chars [.a-z0-9 =<>()] are allowed here. Example: ((s.client:IN:1,3) AND (s.status:=:1)). Do not use a filter coming from input of users. * @param string|int<1,1> $showempty Add an empty field (Can be '1' or text key to use on empty line like 'SelectThirdParty') * @param int<0,1> $showtype Show third party type in combolist (customer, prospect or supplier) * @param int<0,1> $forcecombo Force to load all values and output a standard combobox (with no beautification) * @param array}> $events Ajax event options to run on change. Example: array(array('method'=>'getContacts', 'url'=>dol_buildpath('/core/ajax/contacts.php',1), 'htmlname'=>'contactid', 'params'=>array('add-customer-contact'=>'disabled'))) * @param int $limit Maximum number of elements * @param string $morecss Add more css styles to the SELECT component * @param string $moreparam Add more parameters onto the select tag. For example 'style="width: 95%"' to avoid select2 component to go over parent container * @param string $selected_input_value Value of preselected input text (for use with ajax) * @param int<0,3> $hidelabel Hide label (0=no, 1=yes, 2=show search icon (before) and placeholder 'Search', 3 search icon after) * @param array $ajaxoptions Options for ajax_autocompleter * @param bool $multiple add [] in the name of element and add 'multiple' attribute (not working with ajax_autocompleter) * @param string[] $excludeids Exclude IDs from the select combo * @param int<0,1> $showcode Show code * @return string HTML string with select box for thirdparty. */ public function select_company($selected = '', $htmlname = 'socid', $filter = '', $showempty = '', $showtype = 0, $forcecombo = 0, $events = array(), $limit = 0, $morecss = 'minwidth100', $moreparam = '', $selected_input_value = '', $hidelabel = 1, $ajaxoptions = array(), $multiple = false, $excludeids = array(), $showcode = 0) { // phpcs:enable global $conf, $langs; $out = ''; if (!empty($conf->use_javascript_ajax) && getDolGlobalString('COMPANY_USE_SEARCH_TO_SELECT') && !$forcecombo) { if (is_null($ajaxoptions)) { $ajaxoptions = array(); } require_once DOL_DOCUMENT_ROOT . '/core/lib/ajax.lib.php'; // No immediate load of all database $placeholder = ''; if ($selected && empty($selected_input_value)) { require_once DOL_DOCUMENT_ROOT . '/societe/class/societe.class.php'; $societetmp = new Societe($this->db); $societetmp->fetch($selected); $selected_input_value = $societetmp->name; unset($societetmp); } // mode 1 $urloption = 'htmlname=' . urlencode((string) (str_replace('.', '_', $htmlname))) . '&outjson=1&filter=' . urlencode((string) ($filter)) . (empty($excludeids) ? '' : '&excludeids=' . implode(',', $excludeids)) . ($showtype ? '&showtype=' . urlencode((string) ($showtype)) : '') . ($showcode ? '&showcode=' . urlencode((string) ($showcode)) : '') . ($limit ? '&limit='.$limit : ''); $out .= ''; if (empty($hidelabel)) { $out .= $langs->trans("RefOrLabel") . ' : '; } elseif ($hidelabel == 1 && !is_numeric($showempty)) { $placeholder = $langs->trans($showempty); } elseif ($hidelabel > 1) { $placeholder = $langs->trans("RefOrLabel"); if ($hidelabel == 2) { $out .= img_picto($langs->trans("Search"), 'search'); } } $out .= ''; if ($hidelabel == 3) { $out .= img_picto($langs->trans("Search"), 'search'); } $out .= ajax_event($htmlname, $events); $out .= ajax_autocompleter($selected, $htmlname, DOL_URL_ROOT.'/societe/ajax/company.php', $urloption, getDolGlobalInt('COMPANY_USE_SEARCH_TO_SELECT'), 0, $ajaxoptions); } else { // Immediate load of all database $out .= $this->select_thirdparty_list($selected, $htmlname, $filter, $showempty, $showtype, $forcecombo, $events, '', 0, $limit, $morecss, $moreparam, $multiple, $excludeids, $showcode); } return $out; } // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps /** * Output html form to select a contact * This call select_contacts() or ajax depending on setup. This component is not able to support multiple select. * * Return HTML code of the SELECT of list of all contacts (for a third party or all). * This also set the number of contacts found into $this->num if not using ajax mode. * * @param int $socid Id of third party or 0 for all or -1 for empty list * @param int|string $selected ID of preselected contact id * @param string $htmlname Name of HTML field ('none' for a not editable field) * @param int<0,3>|string $showempty 0=no empty value, 1=add an empty value, 2=add line 'Internal' (used by user edit), 3=add an empty value only if more than one record into list * @param string $exclude List of contacts id to exclude * @param string $limitto Not used * @param int<0,1> $showfunction Add function into label * @param string $morecss Add more class to class style * @param bool $nokeyifsocid When 1, we force the option "Press a key to show list" to 0 if there is a value for $socid * @param integer $showsoc Add company into label * @param int<0,1> $forcecombo 1=Force to use combo box (so no ajax beautify effect) * @param array}> $events Event options. Example: array(array('method'=>'getContacts', 'url'=>dol_buildpath('/core/ajax/contacts.php',1), 'htmlname'=>'contactid', 'params'=>array('add-customer-contact'=>'disabled'))) * @param string $moreparam Add more parameters onto the select tag. For example 'style="width: 95%"' to avoid select2 component to go over parent container * @param string $htmlid Html id to use instead of htmlname * @param string $selected_input_value Value of preselected input text (for use with ajax) * @param string $filter Optional filter criteria. WARNING: To avoid SQL injection, only few chars [.a-z0-9 =<>()] are allowed here. Example: ((s.client:IN:1,3) AND (s.status:=:1)). Do not use a filter coming from input of users. * @return int|string Return integer <0 if KO, HTML with select string if OK. */ public function select_contact($socid, $selected = '', $htmlname = 'contactid', $showempty = 0, $exclude = '', $limitto = '', $showfunction = 0, $morecss = '', $nokeyifsocid = true, $showsoc = 0, $forcecombo = 0, $events = array(), $moreparam = '', $htmlid = '', $selected_input_value = '', $filter = '') { // phpcs:enable global $conf, $langs; $out = ''; $sav = getDolGlobalString('CONTACT_USE_SEARCH_TO_SELECT'); if ($nokeyifsocid && $socid > 0) { $conf->global->CONTACT_USE_SEARCH_TO_SELECT = 0; } if (!empty($conf->use_javascript_ajax) && getDolGlobalString('CONTACT_USE_SEARCH_TO_SELECT') && !$forcecombo) { $ajaxoptions = array(); require_once DOL_DOCUMENT_ROOT . '/core/lib/ajax.lib.php'; // No immediate load of all database $placeholder = ''; if ($selected && empty($selected_input_value)) { require_once DOL_DOCUMENT_ROOT . '/contact/class/contact.class.php'; $contacttmp = new Contact($this->db); $contacttmp->fetch($selected); $selected_input_value = $contacttmp->getFullName($langs); unset($contacttmp); } if (!is_numeric($showempty)) { $placeholder = $showempty; } // mode 1 $urloption = 'htmlname=' . urlencode((string) (str_replace('.', '_', $htmlname))) . '&outjson=1&filter=' . urlencode((string) ($filter)) . (empty($exclude) ? '' : '&exclude=' . urlencode($exclude)) . ($showsoc ? '&showsoc=' . urlencode((string) ($showsoc)) : ''); $out .= ''; $out .= ''; $out .= ajax_event($htmlname, $events); $out .= ajax_autocompleter($selected, $htmlname, DOL_URL_ROOT.'/contact/ajax/contact.php', $urloption, getDolGlobalInt('CONTACT_USE_SEARCH_TO_SELECT'), 0, $ajaxoptions); } else { // Immediate load of all database $multiple = false; $disableifempty = 0; $options_only = 0; $limitto = ''; $out .= $this->selectcontacts($socid, $selected, $htmlname, $showempty, $exclude, $limitto, $showfunction, $morecss, $options_only, $showsoc, $forcecombo, $events, $moreparam, $htmlid, $multiple, $disableifempty, $filter); } $conf->global->CONTACT_USE_SEARCH_TO_SELECT = $sav; return $out; } // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps /** * Output html form to select a third party. * Note: you must use the select_company() to get the component to select a third party. This function must only be called by select_company. * * @param string $selected Preselected type * @param string $htmlname Name of field in form * @param string $filter Optional filter criteria. WARNING: Must be a USF syntax. * @param string|int<0,1> $showempty Add an empty field (Can be '1' or text to use on empty line like 'SelectThirdParty') * @param int<0,1> $showtype Show third party nature in combolist (customer, prospect or supplier) * @param int $forcecombo Force to use standard HTML select component without beautification * @param array}> $events Event options. Example: array(array('method'=>'getContacts', 'url'=>dol_buildpath('/core/ajax/contacts.php',1), 'htmlname'=>'contactid', 'params'=>array('add-customer-contact'=>'disabled'))) * @param string $filterkey Filter on key value * @param int<0,1> $outputmode 0=HTML select string, 1=Array * @param int $limit Limit number of answers * @param string $morecss Add more css styles to the SELECT component * @param string $moreparam Add more parameters onto the select tag. For example 'style="width: 95%"' to avoid select2 component to go over parent container * @param bool $multiple add [] in the name of element and add 'multiple' attribute * @param string[] $excludeids Exclude IDs from the select combo * @param int<0,1> $showcode Show code in list * @return array|string HTML string with * @see select_company() * @phpstan-return ($outputmode is 1 ? array : string) */ public function select_thirdparty_list($selected = '', $htmlname = 'socid', $filter = '', $showempty = '', $showtype = 0, $forcecombo = 0, $events = array(), $filterkey = '', $outputmode = 0, $limit = 0, $morecss = 'minwidth100', $moreparam = '', $multiple = false, $excludeids = array(), $showcode = 0) { // phpcs:enable global $user, $langs; global $hookmanager; $langs->loadLangs(array("companies", "suppliers")); $out = ''; $num = 0; $outarray = array(); if ($selected === '') { $selected = array(); } elseif (!is_array($selected)) { $selected = array($selected); } // Clean $filter that may contains sql conditions so sql code if (function_exists('testSqlAndScriptInject')) { if (testSqlAndScriptInject($filter, 3) > 0) { $filter = ''; return 'SQLInjectionTryDetected'; } } if ($filter != '') { // If a filter was provided $errormsg = ''; $filter = forgeSQLFromUniversalSearchCriteria($filter, $errormsg, 1); // Redo clean $filter that may contains sql conditions so sql code if (function_exists('testSqlAndScriptInject')) { if (testSqlAndScriptInject($filter, 3) > 0) { $filter = ''; return 'SQLInjectionTryDetected'; } } } // We search companies $sql = "SELECT s.rowid, s.nom as name, s.name_alias, s.tva_intra, s.client, s.fournisseur, s.code_client, s.code_fournisseur"; if (getDolGlobalString('COMPANY_SHOW_ADDRESS_SELECTLIST')) { $sql .= ", s.address, s.zip, s.town"; $sql .= ", dictp.code as country_code"; } $sql .= " FROM " . $this->db->prefix() . "societe as s"; if (getDolGlobalString('COMPANY_SHOW_ADDRESS_SELECTLIST')) { $sql .= " LEFT JOIN " . $this->db->prefix() . "c_country as dictp ON dictp.rowid = s.fk_pays"; } if (!$user->hasRight('societe', 'client', 'voir')) { $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc"; } $sql .= " WHERE s.entity IN (" . getEntity('societe') . ")"; if (!empty($user->socid)) { $sql .= " AND s.rowid = " . ((int) $user->socid); } if ($filter) { // $filter is safe because, it has been tested by testSqlAndScriptInject() and sanitized by forgeSQLFromUniversalSearchCriteria() $sqlwhere = $filter; // @phan-suppress-current-line SqlInjection $sql .= " AND (" . $sqlwhere . ")"; } if (!$user->hasRight('societe', 'client', 'voir')) { $sql .= " AND s.rowid = sc.fk_soc AND sc.fk_user = " . ((int) $user->id); } if (getDolGlobalString('COMPANY_HIDE_INACTIVE_IN_COMBOBOX')) { $sql .= " AND s.status <> 0"; } if (!empty($excludeids)) { $sql .= " AND s.rowid NOT IN (" . $this->db->sanitize(implode(',', $excludeids)) . ")"; } // Add where from hooks $parameters = array(); $reshook = $hookmanager->executeHooks('selectThirdpartyListWhere', $parameters); // Note that $action and $object may have been modified by hook $sql .= $hookmanager->resPrint; // Add criteria if ($filterkey && $filterkey != '') { $sql .= " AND ("; $prefix = !getDolGlobalString('COMPANY_DONOTSEARCH_ANYWHERE') ? '%' : ''; // Can use index if COMPANY_DONOTSEARCH_ANYWHERE is on // For natural search $search_crit = explode(' ', $filterkey); $i = 0; if (count($search_crit) > 1) { $sql .= "("; } foreach ($search_crit as $crit) { if ($i > 0) { $sql .= " AND "; } $sql .= "(s.nom LIKE '" . $this->db->escape($prefix . $crit) . "%')"; $i++; } if (count($search_crit) > 1) { $sql .= ")"; } if (isModEnabled('barcode')) { $sql .= " OR s.barcode LIKE '" . $this->db->escape($prefix . $filterkey) . "%'"; } $sql .= " OR s.code_client LIKE '" . $this->db->escape($prefix . $filterkey) . "%' OR s.code_fournisseur LIKE '" . $this->db->escape($prefix . $filterkey) . "%'"; $sql .= " OR s.name_alias LIKE '" . $this->db->escape($prefix . $filterkey) . "%' OR s.tva_intra LIKE '" . $this->db->escape($prefix . $filterkey) . "%'"; $sql .= ")"; } $sql .= $this->db->order("nom", "ASC"); $sql .= $this->db->plimit($limit, 0); // Build output string dol_syslog(get_class($this)."::select_thirdparty_list", LOG_DEBUG); $resql = $this->db->query($sql); if ($resql) { // Construct $out and $outarray $out .= '' . "\n"; if (!$forcecombo) { include_once DOL_DOCUMENT_ROOT . '/core/lib/ajax.lib.php'; $out .= ajax_combobox($htmlname, $events, getDolGlobalInt("COMPANY_USE_SEARCH_TO_SELECT")); } } else { dol_print_error($this->db); } $this->result = array('nbofthirdparties' => $num); if ($outputmode) { return $outarray; } return $out; } /** * Return HTML code of the SELECT of list of all contacts (for a third party or all). * This also set the number of contacts found into $this->num * Note: you must use the select_contact() to get the component to select a contact. This function must only be called by select_contact. * * @param int $socid Id of third party or 0 for all or -1 for empty list * @param string[]|int|string $selected Array of ID of preselected contact id * @param string $htmlname Name of HTML field ('none' for a not editable field) * @param int<0,3>|string $showempty 0=no empty value, 1=add an empty value, 2=add line 'Internal' (used by user edit), 3=add an empty value only if more than one record into list * @param string $exclude List of contacts id to exclude * @param string $limitto Disable answers that are not id in this array list * @param int<0,1> $showfunction Add function into label * @param string $morecss Add more class to class style * @param int $options_only 1=Return options only (for ajax treatment), 2=Return array * @param int<0,1> $showsoc Add company into label * @param int $forcecombo Force to use combo box (so no ajax beautify effect) * @param array}> $events Event options. Example: array(array('method'=>'getContacts', 'url'=>dol_buildpath('/core/ajax/contacts.php',1), 'htmlname'=>'contactid', 'params'=>array('add-customer-contact'=>'disabled'))) * @param string $moreparam Add more parameters onto the select tag. For example 'style="width: 95%"' to avoid select2 component to go over parent container * @param string $htmlid Html id to use instead of htmlname * @param bool $multiple add [] in the name of element and add 'multiple' attribute * @param integer $disableifempty Set tag 'disabled' on select if there is no choice * @param string $filter Optional filter criteria. You must use the USF (Universal Search Filter) syntax, example: '(s.client:in:1,3)' * Do not use a filter coming from input of users. * @return int|string|array Return integer <0 if KO, HTML with select string if OK. */ public function selectcontacts($socid, $selected = array(), $htmlname = 'contactid', $showempty = 0, $exclude = '', $limitto = '', $showfunction = 0, $morecss = '', $options_only = 0, $showsoc = 0, $forcecombo = 0, $events = array(), $moreparam = '', $htmlid = '', $multiple = false, $disableifempty = 0, $filter = '') { global $conf, $user, $langs, $hookmanager, $action; $langs->load('companies'); if (empty($htmlid)) { $htmlid = $htmlname; } $num = 0; $out = ''; $outarray = array(); if ($selected === '') { $selected = array(); } elseif (!is_array($selected)) { $selected = array((int) $selected); } // Clean $filter that may contains sql conditions so sql code if (function_exists('testSqlAndScriptInject')) { if (testSqlAndScriptInject($filter, 3) > 0) { $filter = ''; return 'SQLInjectionTryDetected'; } } if ($filter != '') { // If a filter was provided if (preg_match('/[\(\)]/', $filter)) { // If there is one parenthesis inside the criteria, we assume it is an Universal Filter Syntax. $errormsg = ''; $filter = forgeSQLFromUniversalSearchCriteria($filter, $errormsg, 1); // Redo clean $filter that may contains sql conditions so sql code if (function_exists('testSqlAndScriptInject')) { if (testSqlAndScriptInject($filter, 3) > 0) { $filter = ''; return 'SQLInjectionTryDetected'; } } } else { // If not, we do nothing. We already know that there is no parenthesis // TODO Disallow this case in a future by returning an error here. dol_syslog("Warning, select_thirdparty_list was called with a filter criteria not using the Universal Search Filter Syntax.", LOG_WARNING); } } if (!is_object($hookmanager)) { include_once DOL_DOCUMENT_ROOT . '/core/class/hookmanager.class.php'; $hookmanager = new HookManager($this->db); } // We search third parties $sql = "SELECT sp.rowid, sp.lastname, sp.statut, sp.firstname, sp.poste, sp.email, sp.phone, sp.phone_perso, sp.phone_mobile, sp.town AS contact_town"; if ($showsoc > 0 || getDolGlobalString('CONTACT_SHOW_EMAIL_PHONE_TOWN_SELECTLIST')) { $sql .= ", s.nom as company, s.town AS company_town"; } $sql .= " FROM " . $this->db->prefix() . "socpeople as sp"; if ($showsoc > 0 || getDolGlobalString('CONTACT_SHOW_EMAIL_PHONE_TOWN_SELECTLIST')) { $sql .= " LEFT JOIN " . $this->db->prefix() . "societe as s ON s.rowid = sp.fk_soc"; } $sql .= " WHERE sp.entity IN (" . getEntity('contact') . ")"; $sql .= " AND ((sp.fk_user_creat = ".((int) $user->id)." AND sp.priv = 1) OR sp.priv = 0)"; // check if this is a private contact if ($socid > 0 || $socid == -1) { $sql .= " AND sp.fk_soc = " . ((int) $socid); } if (getDolGlobalString('CONTACT_HIDE_INACTIVE_IN_COMBOBOX')) { $sql .= " AND sp.statut <> 0"; } // filter user access if (!$user->hasRight('societe', 'client', 'voir') && !$user->socid) { $sql .= " AND EXISTS (SELECT sc.fk_soc FROM ".MAIN_DB_PREFIX."societe_commerciaux as sc WHERE sc.fk_soc = sp.fk_soc AND sc.fk_user = ".(int) $user->id .")"; } if ($user->socid > 0) { $sql .= " AND sp.fk_soc = ".((int) $user->socid); } if ($filter) { // $filter is safe because, if it contains '(' or ')', it has been sanitized by testSqlAndScriptInject() and forgeSQLFromUniversalSearchCriteria() // if not, by testSqlAndScriptInject() only. $sanitizedfilter = $filter; // @phan-suppress-current-line SqlInjection $sql .= " AND (" . $sanitizedfilter . ")"; } // Add where from hooks $parameters = array(); $reshook = $hookmanager->executeHooks('selectContactListWhere', $parameters); // Note that $action and $object may have been modified by hook $sql .= $hookmanager->resPrint; $sql .= " ORDER BY sp.lastname ASC"; dol_syslog(get_class($this) . "::selectcontacts", LOG_DEBUG); $resql = $this->db->query($sql); if ($resql) { $num = $this->db->num_rows($resql); if ($htmlname != 'none' && !$options_only) { $out .= ''; } if ($conf->use_javascript_ajax && !$forcecombo && !$options_only) { include_once DOL_DOCUMENT_ROOT . '/core/lib/ajax.lib.php'; $out .= ajax_combobox($htmlid, $events, getDolGlobalInt("CONTACT_USE_SEARCH_TO_SELECT")); } $this->num = $num; if ($options_only === 2) { // Return array of options return $outarray; } else { return $out; } } else { dol_print_error($this->db); return -1; } } // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps /** * Return HTML combo list of absolute discounts * * @param string $selected Id Fixed reduction preselected * @param string $htmlname Name of the form field * @param string $filter Optional filter criteria. Must be a sanitized string. * @param int $socid Id of thirdparty * @param int $maxvalue Max value for lines that can be selected * @return int Return number of qualifed lines in list */ public function select_remises($selected, $htmlname, $filter, $socid, $maxvalue = 0) { // phpcs:enable global $langs, $conf; // On recherche les remises $sql = "SELECT re.rowid, re.amount_ht, re.amount_tva, re.amount_ttc,"; $sql .= " re.description, re.fk_facture_source"; $sql .= " FROM " . $this->db->prefix() . "societe_remise_except as re"; $sql .= " WHERE re.fk_soc = " . (int) $socid; $sql .= " AND re.entity = " . ((int) $conf->entity); if ($filter) { $sanitizedfilter = $filter; // @phan-suppress-current-line SqlInjection $sql .= " AND " . $sanitizedfilter; } $sql .= " ORDER BY re.description ASC"; dol_syslog(get_class($this) . "::select_remises", LOG_DEBUG); $resql = $this->db->query($sql); if ($resql) { print ''; print ajax_combobox('select_' . $htmlname); return $qualifiedlines; } else { dol_print_error($this->db); return -1; } } // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps /** * Return the HTML select list of users * * @param string $selected Id user preselected * @param string $htmlname Field name in form * @param int<0,1> $show_empty 0=list without null value, 1=add an unknown value * @param int[] $exclude Array list of users id to exclude * @param int<0,1> $disabled If select list must be disabled * @param int[]|''|'hierarchy'|'hierarchyme' $include Array list of users id to include. User '' for all users or 'hierarchy' to have only supervised users or 'hierarchyme' to have supervised + me * @param int[]|int $enableonly Array list of users id to be enabled. All other must be disabled * @param string $force_entity '0' or Ids of environment to force * @return void * @deprecated Use select_dolusers instead * @see select_dolusers() */ public function select_users($selected = '', $htmlname = 'userid', $show_empty = 0, $exclude = null, $disabled = 0, $include = '', $enableonly = array(), $force_entity = '0') { // phpcs:enable print $this->select_dolusers($selected, $htmlname, $show_empty, $exclude, $disabled, $include, $enableonly, $force_entity); } // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps /** * Return select list of users * * @param string|int|User|array $userselected User id or User object of user preselected or Array of user selected. If 0 or < -4, we use id of current user. If -1 or '', keep unselected (if empty is allowed) * @param string $htmlname Field name in form * @param int<0,1>|string $show_empty 0=list with no empty value, 1=add also an empty value into list * @param int[]|null $exclude Array list of users id to exclude * @param int $disabled If select list must be disabled * @param int[]|''|'hierarchy'|'hierarchyme' $include Array list of users id to include. Use '' for all users or 'hierarchy' to have only supervised users or 'hierarchyme' to have supervised + me * @param int[]|'' $enableonly Array list of users id to be enabled. If defined, it means that others will be disabled * @param string $force_entity '0' or list of Ids of environment to force, separated by a comma, or 'default' = do no extend to all entities allowed to superadmin. * @param int $maxlength Maximum length of string into list (0=no limit) * @param int<-1,1> $showstatus 0=show user status only if status is disabled, 1=always show user status into label, -1=never show user status * @param string $morefilter Add more filters into sql request (Example: '(employee:=:1)'). This value must not come from user input. * @param int<0,3> $showalso 0=default list, 1=add also a value "Everybody" at beginning of list, 2=add also a value "My team" at beginning of list (if user has at least 1 people), 3=add also "Everybody" + "My Team" * @param string $enableonlytext If option $enableonlytext is set, we use this text to explain into label why record is disabled. Not used if enableonly is empty. * @param string $morecss More css * @param int<0,1> $notdisabled Show only active users (note: this will also happen, whatever is this option, if USER_HIDE_INACTIVE_IN_COMBOBOX is on). * @param int<0,2> $outputmode 0=HTML select string, 1=Array, 2=Detailed array * @param bool $multiple add [] in the name of element and add 'multiple' attribute * @param int<0,1> $forcecombo Force the component to be a simple combo box without ajax * @return string|array HTML select string * @see select_dolgroups() */ public function select_dolusers($userselected = '', $htmlname = 'userid', $show_empty = 0, $exclude = null, $disabled = 0, $include = '', $enableonly = '', $force_entity = '', $maxlength = 0, $showstatus = 0, $morefilter = '', $showalso = 0, $enableonlytext = '', $morecss = '', $notdisabled = 0, $outputmode = 0, $multiple = false, $forcecombo = 0) { // phpcs:enable global $conf, $user, $langs, $hookmanager; global $action; // Convert $selected into an int (in case it is an object) if (is_object($userselected)) { $selected = (int) $userselected->id; } elseif (is_numeric($userselected)) { $selected = (int) $userselected; } elseif (is_array($userselected)) { $selected = $userselected; } else { $selected = -1; } // If no preselected user defined, we take current user if ((is_numeric($selected) && ((int) $selected < -4 || empty($selected))) && !getDolGlobalString('SOCIETE_DISABLE_DEFAULT_SALESREPRESENTATIVE')) { $selected = $user->id; } // Convert selected int into an array if (!is_array($selected)) { if ($selected === -1 || $selected === '') { $selected = array(); } else { $selected = array($selected); } } // Exclude some users in $excludeUsers string $excludeUsers = null; if (is_array($exclude)) { $excludeUsers = implode(",", $exclude); } // Include some users in $includeUsers string $includeUsers = null; $includeUsersArray = array(); if (is_array($include)) { $includeUsersArray = $include; } elseif ($include == 'hierarchy') { // Build list includeUsersArray to have only hierarchy $includeUsersArray = $user->getAllChildIds(0); } elseif ($include == 'hierarchyme') { // Build list includeUsersArray to have only hierarchy and current user $includeUsersArray = $user->getAllChildIds(1); } // Get list of allowed users /* We do not limit list of users. Because we should limit this only for combo list into HR features where we may be allowed to * see all other users and element in other. For example in agenda, we can have permission to read all event of otherusers. * So we disable this. if (!$user->hasRight('user', 'user', 'lire')) { if (empty($includeUsersArray)) { $includeUsers = implode(",", $user->getAllChildIds(1)); } else { $includeUsers = implode(",", array_intersect($includeUsersArray, $user->getAllChildIds(1))); } } else { $includeUsers = implode(",", $includeUsersArray); } */ $includeUsers = implode(",", $includeUsersArray); $num = 0; $out = ''; $outarray = array(); $outarray2 = array(); // Do we want to show the label of entity into the combo list ? $showlabelofentity = isModEnabled('multicompany') && !getDolGlobalInt('MULTICOMPANY_TRANSVERSE_MODE') && $conf->entity == 1 && !empty($user->admin) && empty($user->entity) && !preg_match('/^search_/', $htmlname); $userissuperadminentityone = isModEnabled('multicompany') && $conf->entity == 1 && $user->admin && empty($user->entity); // Forge request to select users $sql = "SELECT DISTINCT u.rowid, u.lastname as lastname, u.firstname, u.statut as status, u.login, u.admin, u.entity, u.gender, u.photo"; if ($showlabelofentity) { $sql .= ", e.label"; } $sql .= " FROM " . $this->db->prefix() . "user as u"; if ($showlabelofentity) { $sql .= " LEFT JOIN " . $this->db->prefix() . "entity as e ON e.rowid = u.entity"; } // Condition here should be the same than into societe->getSalesRepresentatives(). if ($userissuperadminentityone && $force_entity !== 'default') { if (!empty($force_entity)) { $sql .= " WHERE u.entity IN (0, " . $this->db->sanitize($force_entity) . ")"; } else { $sql .= " WHERE u.entity IS NOT NULL"; } } else { if (isModEnabled('multicompany') && getDolGlobalInt('MULTICOMPANY_TRANSVERSE_MODE')) { $sql .= " WHERE u.rowid IN (SELECT ug.fk_user FROM ".$this->db->prefix()."usergroup_user as ug WHERE ug.entity IN (".getEntity('usergroup')."))"; } else { $sql .= " WHERE u.entity IN (" . getEntity('user') . ")"; } } if (!empty($user->socid)) { $sql .= " AND u.fk_soc = " . ((int) $user->socid); } if (is_array($exclude) && $excludeUsers) { $sql .= " AND u.rowid NOT IN (" . $this->db->sanitize($excludeUsers) . ")"; } if ($includeUsers) { $sql .= " AND u.rowid IN (" . $this->db->sanitize($includeUsers) . ")"; } if (getDolGlobalString('USER_HIDE_INACTIVE_IN_COMBOBOX') || $notdisabled) { $sql .= " AND (u.statut <> 0"; if (!empty($selected)) { $sql .= " OR u.rowid IN (".$this->db->sanitize(implode(',', $selected)).")"; // We must always keep the selected users to avoid to loose it/them when updating } $sql .= ")"; } if (getDolGlobalString('USER_HIDE_NONEMPLOYEE_IN_COMBOBOX')) { $sql .= " AND u.employee <> 0"; } if (getDolGlobalString('USER_HIDE_EXTERNAL_IN_COMBOBOX')) { $sql .= " AND u.fk_soc IS NULL"; } if (!empty($morefilter)) { $errormessage = ''; $sql .= forgeSQLFromUniversalSearchCriteria($morefilter, $errormessage); if ($errormessage) { $this->errors[] = $errormessage; dol_syslog(__METHOD__.' '.implode(',', $this->errors), LOG_ERR); if ($outputmode == 0) { return 'Error bad param $morefilter'; } else { return array(); } } } //Add hook to filter on user (for example on usergroup define in custom modules) $reshook = $hookmanager->executeHooks('addSQLWhereFilterOnSelectUsers', array(), $this, $action); if (!empty($reshook)) { $sql .= $hookmanager->resPrint; } if (!getDolGlobalString('MAIN_FIRSTNAME_NAME_POSITION')) { // MAIN_FIRSTNAME_NAME_POSITION is 0 means firstname+lastname $sql .= " ORDER BY u.statut DESC, u.firstname ASC, u.lastname ASC"; } else { $sql .= " ORDER BY u.statut DESC, u.lastname ASC, u.firstname ASC"; } dol_syslog(get_class($this) . "::select_dolusers", LOG_DEBUG); $resql = $this->db->query($sql); if ($resql) { $num = $this->db->num_rows($resql); $i = 0; if ($num) { // do not use maxwidthonsmartphone by default. Set it by caller so auto size to 100% will work when not defined $out .= ''; $out .= ''; } $out .= ''; if ($num && !$forcecombo) { // Enhance with select2 include_once DOL_DOCUMENT_ROOT . '/core/lib/ajax.lib.php'; $out .= ajax_combobox($htmlname); } } else { dol_print_error($this->db); } $this->num = $num; if ($outputmode == 2) { return $outarray2; } elseif ($outputmode) { return $outarray; } return $out; } // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps /** * Return select list of users. Selected users are stored into session. * List of users are provided into $_SESSION['assignedtouser']. * * @param string $action Value for $action * @param string $htmlname Field name in form * @param int<0,1> $show_empty 0=list without the empty value, 1=add empty value * @param int[] $exclude Array list of users id to exclude * @param int<0,1> $disabled If select list must be disabled * @param int[]|''|'hierarchy'|'hierarchyme' $include Array list of users id to include or 'hierarchy' to have only supervised users * @param int[]|int $enableonly Array list of users id to be enabled. All other must be disabled * @param string $force_entity '0' or Ids of environment to force * @param int $maxlength Maximum length of string into list (0=no limit) * @param int<0,1> $showstatus 0=show user status only if status is disabled, 1=always show user status into label, -1=never show user status * @param string $morefilter Add more filters into sql request (Example: '(employee:=:1)'). This value must not come from user input. * @param int $showproperties Show properties of each attendees * @param int[] $listofuserid Array with properties of each user * @param int[] $listofcontactid Array with properties of each contact * @param int[] $listofotherid Array with properties of each other contact * @param int $canremoveowner 1 if we can remove owner, 0=no way * @return string HTML select string * @see select_dolgroups() */ public function select_dolusers_forevent($action = '', $htmlname = 'userid', $show_empty = 0, $exclude = null, $disabled = 0, $include = array(), $enableonly = array(), $force_entity = '0', $maxlength = 0, $showstatus = 0, $morefilter = '', $showproperties = 0, $listofuserid = array(), $listofcontactid = array(), $listofotherid = array(), $canremoveowner = 1) { // phpcs:enable global $langs, $user; $userstatic = new User($this->db); $out = ''; if (!empty($_SESSION['assignedtouser'])) { $assignedtouser = json_decode($_SESSION['assignedtouser'], true); if (!is_array($assignedtouser)) { $assignedtouser = array(); } } else { $assignedtouser = array(); } $nbassignetouser = count($assignedtouser); //if ($nbassignetouser && $action != 'view') $out .= '
'; if ($nbassignetouser) { $out .= '
    '; } $i = 0; $ownerid = 0; foreach ($assignedtouser as $key => $value) { if ($value['id'] == $ownerid) { continue; } $out .= '
  • '; $userstatic->fetch($value['id']); $out .= $userstatic->getNomUrl(-4); if ($i == 0) { $ownerid = $value['id']; $out .= ' (' . $langs->trans("Owner") . ')'; } // Add picto to delete owner/assignee if ($nbassignetouser > 1 && $action != 'view') { $canremoveassignee = 1; if ($i == 0) { // We are on the owner of the event if (!$canremoveowner) { $canremoveassignee = 0; } if (!$user->hasRight('agenda', 'allactions', 'create')) { $canremoveassignee = 0; // Can't remove the owner } } else { // We are not on the owner of the event but on a secondary assignee } if ($canremoveassignee) { // If user has all permission, he should be ableto remove a assignee. // If user has not all permission, he can onlyremove assignee of other (he can't remove itself) $out .= ' '; } } // Show my availability if ($showproperties) { if ($ownerid == $value['id'] && is_array($listofuserid) && count($listofuserid) && in_array($ownerid, array_keys($listofuserid))) { $out .= '
    '; $out .= ' - '; //$out .= '' . $langs->trans("Availability") . ':'; $out .= ''; $out .= ' '; $out .= '
    '; } } //$out.=' '.($value['mandatory']?$langs->trans("Mandatory"):$langs->trans("Optional")); //$out.=' '.($value['transparency']?$langs->trans("Busy"):$langs->trans("NotBusy")); $out .= '
  • '; $i++; } if ($nbassignetouser) { $out .= '
'; } // Method with no ajax if ($action != 'view') { // Section to add another user $out .= '
'; $out .= ''; $out .= ''; $out .= img_picto('', 'user', 'class="pictofixedwidth"'); $out .= $this->select_dolusers('', $htmlname, $show_empty, $exclude, $disabled, $include, $enableonly, $force_entity, $maxlength, $showstatus, $morefilter, 0, '', 'minwidth200'); $out .= ' '; $out .= '
'; //$out .= '
'; } return $out; } // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps /** * Return select list of resources. Selected resources are stored into session. * List of resources are provided into $_SESSION['assignedtoresource']. * * @param string $action Value for $action * @param string $htmlname Field name in form * @param int<0,1> $show_empty 0=list without the empty value, 1=add empty value * @param int[] $exclude Array list of users id to exclude * @param int<0,1> $disabled If select list must be disabled * @param int[]|string $include Array list of users id to include or 'hierarchy' to have only supervised users * @param int[] $enableonly Array list of users id to be enabled. All other must be disabled * @param string $force_entity '0' or Ids of environment to force * @param int $maxlength Maximum length of string into list (0=no limit) * @param int<-1,1> $showstatus 0=show user status only if status is disabled, 1=always show user status into label, -1=never show user status * @param string $morefilter Add more filters into sql request * @param int<0,1> $showproperties Show properties of each attendees * @param array}> $listofresourceid Array with properties of each resource * @return string HTML select string */ public function select_dolresources_forevent($action = '', $htmlname = 'userid', $show_empty = 0, $exclude = null, $disabled = 0, $include = array(), $enableonly = array(), $force_entity = '0', $maxlength = 0, $showstatus = 0, $morefilter = '', $showproperties = 0, $listofresourceid = array()) { // phpcs:enable global $langs; require_once DOL_DOCUMENT_ROOT.'/resource/class/html.formresource.class.php'; require_once DOL_DOCUMENT_ROOT.'/resource/class/dolresource.class.php'; $formresources = new FormResource($this->db); $resourcestatic = new Dolresource($this->db); $out = ''; if (!empty($_SESSION['assignedtoresource'])) { $assignedtoresource = json_decode($_SESSION['assignedtoresource'], true); if (!is_array($assignedtoresource)) { $assignedtoresource = array(); } } else { $assignedtoresource = array(); } $nbassignetoresource = count($assignedtoresource); //if ($nbassignetoresource && $action != 'view') $out .= '
'; if ($nbassignetoresource) { $out .= '
    '; } $i = 0; foreach ($assignedtoresource as $key => $value) { $out .= '
  • '; $resourcestatic->fetch($value['id']); $out .= $resourcestatic->getNomUrl(-1); if ($nbassignetoresource >= 1 && $action != 'view') { $out .= ' '; } // Show my availability if ($showproperties) { if (is_array($listofresourceid) && count($listofresourceid)) { $out .= '
    '; $out .= ' - '; //$out .= '' . $langs->trans("Availability") . ': '; $out .= ''; $out .= ' '; $out .= '
    '; } } //$out.=' '.($value['mandatory']?$langs->trans("Mandatory"):$langs->trans("Optional")); //$out.=' '.($value['transparency']?$langs->trans("Busy"):$langs->trans("NotBusy")); $out .= '
  • '; $i++; } if ($nbassignetoresource) { $out .= '
'; } // Method with no ajax if ($action != 'view') { $out .= ''; $out .= ''; $events = array(); if ($nbassignetoresource) { //$out .= img_picto('', 'add', 'class="pictofixedwidth"'); } else { $out .= img_picto('', 'resource', 'class="pictofixedwidth"'); } $out .= $formresources->select_resource_list(0, $htmlname, '', 1, 1, 0, $events, '', 2, 0, 'minwidth200'); //$out .= $this->select_dolusers('', $htmlname, $show_empty, $exclude, $disabled, $include, $enableonly, $force_entity, $maxlength, $showstatus, $morefilter); $out .= ' '; $out .= '
'; } return $out; } // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps /** * Return list of products. * Use Ajax if Ajax activated or go to select_produits_list * * @param int $selected Preselected products * @param string $htmlname Name of HTML select field (must be unique in page). * @param int|'' $filtertype Filter on product type (''=nofilter, 0=product, 1=service) * @param int $limit Limit on number of returned lines * @param int $price_level Level of price to show * @param int<-1,1> $status Sell status: -1=No filter on sell status, 0=Products not on sell, 1=Products on sell * @param int<0,2> $finished 2=all, 1=finished, 0=raw material * @param string $selected_input_value Value of preselected input text (for use with ajax) * @param int<0,3> $hidelabel Hide label (0=no, 1=yes, 2=show search icon (before) and placeholder, 3 search icon after) * @param array $ajaxoptions Options for ajax_autocompleter * @param int $socid Thirdparty Id (to get also price dedicated to this customer) * @param string|int<0,1> $showempty '' to not show empty line. Translation key to show an empty line. '1' show empty line with no text. * @param int<0,1> $forcecombo Force to use combo box. * @param string $morecss Add more css on select * @param int<0,1> $hidepriceinlabel 1=Hide prices in label * @param string $warehouseStatus Warehouse status filter to count the quantity in stock. Following comma separated filter options can be used * 'warehouseopen' = count products from open warehouses, * 'warehouseclosed' = count products from closed warehouses, * 'warehouseinternal' = count products from warehouses for internal correct/transfer only * @param ?array $selected_combinations Selected combinations. Format: array([attrid] => attrval, [...]) * @param int<0,1> $nooutput No print if 1, return the output into a string * @param int<-1,1> $status_purchase Purchase status: -1=No filter on purchase status, 0=Products not on purchase, 1=Products on purchase * @param int $warehouseId Filter by Warehouses Id where there is real stock * @return void|string */ public function select_produits($selected = 0, $htmlname = 'productid', $filtertype = '', $limit = 0, $price_level = 0, $status = 1, $finished = 2, $selected_input_value = '', $hidelabel = 0, $ajaxoptions = array(), $socid = 0, $showempty = '1', $forcecombo = 0, $morecss = '', $hidepriceinlabel = 0, $warehouseStatus = '', $selected_combinations = null, $nooutput = 0, $status_purchase = -1, $warehouseId = 0) { // phpcs:enable global $langs, $conf; $out = ''; // check parameters $price_level = (!empty($price_level) ? $price_level : 0); if (is_null($ajaxoptions)) { $ajaxoptions = array(); } if (strval($filtertype) === '' && (isModEnabled("product") || isModEnabled("service"))) { if (isModEnabled("product") && !isModEnabled('service')) { $filtertype = '0'; } elseif (!isModEnabled('product') && isModEnabled("service")) { $filtertype = '1'; } } if (!empty($conf->use_javascript_ajax) && getDolGlobalString('PRODUIT_USE_SEARCH_TO_SELECT')) { $placeholder = (is_numeric($showempty) ? '' : 'placeholder="'.dolPrintHTML($showempty).'"'); if ($selected && empty($selected_input_value)) { require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php'; $producttmpselect = new Product($this->db); $producttmpselect->fetch($selected); $selected_input_value = $producttmpselect->ref; unset($producttmpselect); } // handle case where product or service module is disabled + no filter specified if ($filtertype == '') { if (!isModEnabled('product')) { // when product module is disabled, show services only $filtertype = 1; } elseif (!isModEnabled('service')) { // when service module is disabled, show products only $filtertype = 0; } } // mode=1 means customers products $urloption = ($socid > 0 ? 'socid=' . $socid . '&' : '') . 'htmlname=' . $htmlname . '&outjson=1&price_level=' . $price_level . '&type=' . $filtertype . '&mode=1&status=' . $status . '&status_purchase=' . $status_purchase . '&finished=' . $finished . '&hidepriceinlabel=' . $hidepriceinlabel . '&warehousestatus=' . $warehouseStatus; if ((int) $warehouseId > 0) { $urloption .= '&warehouseid=' . (int) $warehouseId; } if (isModEnabled('variants') && is_array($selected_combinations)) { // Code to automatically insert with javascript the select of attributes under the select of product // when a parent of variant has been selected. // Note: Samecode than for product input using select $htmltag = 'input'; $out .= ' '; } if (empty($hidelabel)) { $placeholder = ' placeholder="' . dolPrintHTMLForAttribute($langs->trans("RefOrLabel")) . '"'; } elseif ($hidelabel > 1) { $placeholder = ' placeholder="' . dolPrintHTMLForAttribute($langs->trans("RefOrLabel")) . '"'; if ($hidelabel == 2) { $out .= img_picto($langs->trans("Search"), 'search'); } } $out .= ''; if ($hidelabel == 3) { $out .= img_picto($langs->trans("Search"), 'search'); } $out .= ajax_autocompleter((string) $selected, $htmlname, DOL_URL_ROOT . '/product/ajax/products.php', $urloption, getDolGlobalInt('PRODUIT_USE_SEARCH_TO_SELECT'), getDolGlobalInt('PRODUCT_SEARCH_AUTO_SELECT_IF_ONLY_ONE', 1), $ajaxoptions); } else { $out .= $this->select_produits_list($selected, $htmlname, $filtertype, $limit, $price_level, '', $status, $finished, 0, $socid, $showempty, $forcecombo, $morecss, $hidepriceinlabel, $warehouseStatus, $status_purchase, $warehouseId); if (isModEnabled('variants') && is_array($selected_combinations)) { // Code to automatically insert with javascript the select of attributes under the select of product // when a parent of variant has been selected. // Note: Samecode than for product input using Ajax $htmltag = 'select'; $out .= ' '; } } if (empty($nooutput)) { print $out; } else { return $out; } } // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps /** * Return list of BOM for customer in Ajax if Ajax activated or go to select_produits_list * * @param string $selected Preselected BOM id * @param string $htmlname Name of HTML select field (must be unique in page). * @param int $limit Limit on number of returned lines * @param int $status Sell status -1=Return all bom, 0=Draft BOM, 1=Validated BOM * @param int $type Type of the BOM (-1=Return all BOM, 0=Return disassemble BOM, 1=Return manufacturing BOM) * @param string|int<0,1> $showempty '' to not show empty line. Translation key to show an empty line. '1' show empty line with no text. * @param string $morecss Add more css on select * @param string $nooutput No print, return the output into a string * @param int $forcecombo Force to use combo box * @param string[] $TProducts Add filter on a defined product * @return void|string */ public function select_bom($selected = '', $htmlname = 'bom_id', $limit = 0, $status = 1, $type = 0, $showempty = '1', $morecss = '', $nooutput = '', $forcecombo = 0, $TProducts = []) { // phpcs:enable require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php'; $error = 0; $out = ''; if (!$forcecombo) { include_once DOL_DOCUMENT_ROOT . '/core/lib/ajax.lib.php'; $events = array(); $out .= ajax_combobox($htmlname, $events, getDolGlobalInt("BOM_USE_SEARCH_TO_SELECT")); } $out .= ''; if (empty($nooutput)) { print $out; } else { return $out; } } // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps /** * Return list of products for a customer. * Not optimized for high number of product, use instead select_produits() for large databases (so select_produits will call this function with a $limit parameter). * * @param int $selected Preselected product * @param string $htmlname Name of select html * @param string $filtertype Filter on product type (''=nofilter, 0=product, 1=service) * @param int $limit Limit on number of returned lines * @param int $price_level Level of price to show * @param string $filterkey Filter on product * @param int<-1,1> $status -1=Return all products, 0=Products not on sell, 1=Products on sell * @param int $finished Filter on finished field: 2=No filter * @param int $outputmode 0=HTML select string, 1=Array * @param int $socid Thirdparty Id (to get also price dedicated to this customer) * @param string|int<0,1> $showempty '' to not show empty line. Translation key to show an empty line. '1' show empty line with no text. * @param int $forcecombo Force to use combo box * @param string $morecss Add more css on select * @param int<0,1> $hidepriceinlabel 1=Hide prices in label * @param ''|'warehouseopen'|'warehouseclosed'|'warehouseinternal' $warehouseStatus Warehouse status filter to group/count stock. Following comma separated filter options can be used. * 'warehouseopen' = count products from open warehouses, * 'warehouseclosed' = count products from closed warehouses, * 'warehouseinternal' = count products from warehouses for internal correct/transfer only * @param int<-1,1> $status_purchase Purchase status -1=Return all products, 0=Products not on purchase, 1=Products on purchase * @param int $warehouseId Filter by Warehouses Id where there is real stock * @return string|array Array of keys for json */ public function select_produits_list($selected = 0, $htmlname = 'productid', $filtertype = '', $limit = 1000, $price_level = 0, $filterkey = '', $status = 1, $finished = 2, $outputmode = 0, $socid = 0, $showempty = '1', $forcecombo = 0, $morecss = 'maxwidth500', $hidepriceinlabel = 0, $warehouseStatus = '', $status_purchase = -1, $warehouseId = 0) { // phpcs:enable global $langs; global $hookmanager; $out = ''; $outarray = array(); // Units if (getDolGlobalInt('PRODUCT_USE_UNITS')) { $langs->load('other'); } $warehouseStatusArray = array(); if (!empty($warehouseStatus)) { require_once DOL_DOCUMENT_ROOT . '/product/stock/class/entrepot.class.php'; if (preg_match('/warehouseclosed/', $warehouseStatus)) { $warehouseStatusArray[] = Entrepot::STATUS_CLOSED; } if (preg_match('/warehouseopen/', $warehouseStatus)) { $warehouseStatusArray[] = Entrepot::STATUS_OPEN_ALL; } if (preg_match('/warehouseinternal/', $warehouseStatus)) { $warehouseStatusArray[] = Entrepot::STATUS_OPEN_INTERNAL; } } $selectFields = "p.rowid, p.ref, p.label, p.description, p.barcode, p.fk_country, p.fk_product_type, p.price, p.price_ttc, p.price_base_type, p.tva_tx, p.default_vat_code, p.duration, p.fk_price_expression"; if (count($warehouseStatusArray)) { $selectFieldsGrouped = ", SUM(" . $this->db->ifsql("e.statut IS NULL", "0", "ps.reel") . ") as stock"; // e.statut is null if there is no record in a qualified stock } else { $selectFieldsGrouped = ", " . $this->db->ifsql("p.stock IS NULL", '0', "p.stock") . " AS stock"; } $sql = "SELECT "; // Add select from hooks $parameters = array(); $reshook = $hookmanager->executeHooks('selectProductsListSelect', $parameters); // Note that $action and $object may have been modified by hook if (empty($reshook)) { $sql .= $selectFields.$selectFieldsGrouped.$hookmanager->resPrint; } else { $sql .= $hookmanager->resPrint; } if (getDolGlobalString('PRODUCT_SORT_BY_CATEGORY')) { // Take randomly the first category of product to allow a sort on it. Bugged feature ! $sql .= ", (SELECT " . $this->db->prefix() . "categorie_product.fk_categorie FROM " . $this->db->prefix() . "categorie_product WHERE " . $this->db->prefix() . "categorie_product.fk_product = p.rowid LIMIT 1 ) AS categorie_product_id"; } // Price by customer if ((getDolGlobalString('PRODUIT_CUSTOMER_PRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && !empty($socid)) { $sql .= ', pcp.rowid as idprodcustprice, pcp.price as custprice, pcp.price_ttc as custprice_ttc,'; $sql .= ' pcp.price_base_type as custprice_base_type, pcp.tva_tx as custtva_tx, pcp.default_vat_code as custdefault_vat_code, pcp.ref_customer as custref, pcp.discount_percent as custdiscount_percent'; $selectFields .= ", idprodcustprice, custprice, custprice_ttc, custprice_base_type, custtva_tx, custdefault_vat_code, custref, custdiscount_percent"; } // Units if (getDolGlobalInt('PRODUCT_USE_UNITS')) { $sql .= ", u.label as unit_long, u.short_label as unit_short, p.weight, p.weight_units, p.length, p.length_units, p.width, p.width_units, p.height, p.height_units, p.surface, p.surface_units, p.volume, p.volume_units"; $selectFields .= ', unit_long, unit_short, p.weight, p.weight_units, p.length, p.length_units, p.width, p.width_units, p.height, p.height_units, p.surface, p.surface_units, p.volume, p.volume_units'; } // Multilang : we add translation if (getDolGlobalInt('MAIN_MULTILANGS')) { $sql .= ", pl.label as label_translated"; $sql .= ", pl.description as description_translated"; $selectFields .= ", label_translated"; $selectFields .= ", description_translated"; } // Price by quantity if (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES')) { $sql .= ", (SELECT pp.rowid FROM " . $this->db->prefix() . "product_price as pp WHERE pp.fk_product = p.rowid"; if ($price_level >= 1 && getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES')) { $sql .= " AND price_level = " . ((int) $price_level); } $sql .= " ORDER BY date_price"; $sql .= " DESC LIMIT 1) as price_rowid"; $sql .= ", (SELECT pp.price_by_qty FROM " . $this->db->prefix() . "product_price as pp WHERE pp.fk_product = p.rowid"; // price_by_qty is 1 if some prices by qty exists in subtable if ($price_level >= 1 && getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES')) { $sql .= " AND price_level = " . ((int) $price_level); } $sql .= " ORDER BY date_price"; $sql .= " DESC LIMIT 1) as price_by_qty"; $selectFields .= ", price_rowid, price_by_qty"; } //$sqlfields = $sql; // $sql fields to remove for count total $sql .= " FROM ".$this->db->prefix()."product as p"; if (getDolGlobalString('MAIN_SEARCH_PRODUCT_FORCE_INDEX')) { $sql .= " USE INDEX (" . $this->db->sanitize(getDolGlobalString('MAIN_PRODUCT_FORCE_INDEX')) . ")"; } // Add from (left join) from hooks $parameters = array( 'socid' => $socid, ); $reshook = $hookmanager->executeHooks('selectProductsListFrom', $parameters); // Note that $action and $object may have been modified by hook $sql .= $hookmanager->resPrint; if (count($warehouseStatusArray)) { // Return line if product is inside the selected stock. If not, e.* and p.* will be null so we will count 0. // Replace this with a AND EXISTS ? Not possible as we need the ps.reel field for the SUM or 0 if no link. $sql .= " LEFT JOIN " . $this->db->prefix() . "product_stock as ps ON ps.fk_product = p.rowid"; $sql .= " LEFT JOIN " . $this->db->prefix() . "entrepot as e ON ps.fk_entrepot = e.rowid AND e.entity IN (" . getEntity('stock') . ")"; $sql .= ' AND e.statut IN (' . $this->db->sanitize($this->db->escape(implode(',', $warehouseStatusArray))) . ')'; } // Price by customer (Add field pcp for the older price for couple product/thirdparty. if ((getDolGlobalString('PRODUIT_CUSTOMER_PRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && !empty($socid)) { $now = dol_now(); $sql .= " LEFT JOIN ("; $sql .= " SELECT pcp1.*"; $sql .= " FROM " . $this->db->prefix() . "product_customer_price AS pcp1"; $sql .= " LEFT JOIN ("; $sql .= " SELECT fk_soc, fk_product, MIN(date_begin) AS date_begin"; $sql .= " FROM " . $this->db->prefix() . "product_customer_price"; $sql .= " WHERE fk_soc = " . ((int) $socid); $sql .= " AND date_begin <= '" . $this->db->idate($now) . "'"; $sql .= " AND (date_end IS NULL OR '" . $this->db->idate($now) . "' <= date_end)"; $sql .= " GROUP BY fk_soc, fk_product"; $sql .= " ) AS pcp2 ON pcp1.fk_soc = pcp2.fk_soc AND pcp1.fk_product = pcp2.fk_product AND pcp1.date_begin = pcp2.date_begin"; $sql .= " WHERE pcp2.fk_soc IS NOT NULL"; $sql .= " ) AS pcp ON pcp.fk_soc = " . ((int) $socid) . " AND pcp.fk_product = p.rowid"; } // Units : we add unit properties with a link on the primary key of unit if (getDolGlobalInt('PRODUCT_USE_UNITS')) { $sql .= " LEFT JOIN " . $this->db->prefix() . "c_units as u ON u.rowid = p.fk_unit"; } // Multilang : we add translation fields with a link on unique key fk_product/lang. if (getDolGlobalInt('MAIN_MULTILANGS')) { $sql .= " LEFT JOIN " . $this->db->prefix() . "product_lang as pl ON pl.fk_product = p.rowid"; if (getDolGlobalString('PRODUIT_TEXTS_IN_THIRDPARTY_LANGUAGE') && !empty($socid)) { require_once DOL_DOCUMENT_ROOT . '/societe/class/societe.class.php'; $soc = new Societe($this->db); $result = $soc->fetch($socid); if ($result > 0 && !empty($soc->default_lang)) { $sql .= " AND pl.lang = '" . $this->db->escape($soc->default_lang) . "'"; } else { $sql .= " AND pl.lang = '" . $this->db->escape($langs->getDefaultLang()) . "'"; } } else { $sql .= " AND pl.lang = '" . $this->db->escape($langs->getDefaultLang()) . "'"; } } // Add WHERE conditions $sql .= ' WHERE p.entity IN (' . getEntity('product') . ')'; if (getDolGlobalString('PRODUIT_ATTRIBUTES_HIDECHILD')) { if (getDolGlobalString('PRODUIT_ATTRIBUTES_HIDECHILD_BUT_ALLOW_SEARCH_IN_EAN13')) { if (strlen($filterkey) != 13) { $sql .= " AND NOT EXISTS (SELECT pac.rowid FROM ".$this->db->prefix()."product_attribute_combination as pac WHERE pac.fk_product_child = p.rowid)"; } } else { $sql .= " AND NOT EXISTS (SELECT pac.rowid FROM ".$this->db->prefix()."product_attribute_combination as pac WHERE pac.fk_product_child = p.rowid)"; } } if ($finished == 0) { $sql .= " AND p.finished = " . ((int) $finished); } elseif ($finished == 1) { $sql .= " AND p.finished = ".((int) $finished); } if ($status >= 0) { $sql .= " AND p.tosell = ".((int) $status); } if ($status_purchase >= 0) { $sql .= " AND p.tobuy = " . ((int) $status_purchase); } // Filter by product type if (strval($filtertype) != '') { $sql .= " AND p.fk_product_type = " . ((int) $filtertype); } elseif (!isModEnabled('product')) { // when product module is disabled, show services only $sql .= " AND p.fk_product_type = 1"; } elseif (!isModEnabled('service')) { // when service module is disabled, show products only $sql .= " AND p.fk_product_type = 0"; } if ((int) $warehouseId > 0) { $sql .= " AND EXISTS (SELECT psw.fk_product FROM " . $this->db->prefix() . "product_stock as psw WHERE psw.reel > 0 AND psw.fk_entrepot = ".(int) $warehouseId." AND psw.fk_product = p.rowid)"; } // Add where from hooks $parameters = array( 'filterkey' => &$filterkey, 'socid' => $socid, ); $reshook = $hookmanager->executeHooks('selectProductsListWhere', $parameters); // Note that $action and $object may have been modified by hook $sql .= $hookmanager->resPrint; // Add criteria on ref/label if ($filterkey != '') { $sqlSupplierSearch = ''; $sql .= ' AND ('; $prefix = getDolGlobalString('PRODUCT_DONOTSEARCH_ANYWHERE') ? '' : '%'; // Can use index if PRODUCT_DONOTSEARCH_ANYWHERE is on // For natural search $search_crit = explode(' ', $filterkey); $i = 0; if (count($search_crit) > 1) { $sql .= "("; } foreach ($search_crit as $crit) { if ($i > 0) { $sql .= " AND "; } $sql .= "(p.ref LIKE '" . $this->db->escape($prefix . $crit) . "%' OR p.label LIKE '" . $this->db->escape($prefix . $crit) . "%'"; if (getDolGlobalInt('MAIN_MULTILANGS')) { $sql .= " OR pl.label LIKE '" . $this->db->escape($prefix . $crit) . "%'"; } if ((getDolGlobalString('PRODUIT_CUSTOMER_PRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && !empty($socid)) { $sql .= " OR pcp.ref_customer LIKE '" . $this->db->escape($prefix . $crit) . "%'"; } if (getDolGlobalString('PRODUCT_AJAX_SEARCH_ON_DESCRIPTION')) { $sql .= " OR p.description LIKE '" . $this->db->escape($prefix . $crit) . "%'"; if (getDolGlobalInt('MAIN_MULTILANGS')) { $sql .= " OR pl.description LIKE '" . $this->db->escape($prefix . $crit) . "%'"; } } // include search in supplier ref if (getDolGlobalString('MAIN_SEARCH_PRODUCT_BY_FOURN_REF')) { $sqlSupplierSearch .= !empty($sqlSupplierSearch) ? ' AND ' : ''; $sqlSupplierSearch .= " pfp.ref_fourn LIKE '" . $this->db->escape($prefix . $crit) . "%'"; } $sql .= ")"; $i++; } if (count($search_crit) > 1) { $sql .= ")"; } if (isModEnabled('barcode')) { $sql .= " OR p.barcode LIKE '" . $this->db->escape($prefix . $filterkey) . "%'"; } // include search in supplier ref if (getDolGlobalString('MAIN_SEARCH_PRODUCT_BY_FOURN_REF')) { $sql .= " OR EXISTS (SELECT pfp.fk_product FROM " . $this->db->prefix() . "product_fournisseur_price as pfp WHERE p.rowid = pfp.fk_product"; $sql .= " AND ("; $sql .= $sqlSupplierSearch; $sql .= "))"; } $sql .= ')'; } if (count($warehouseStatusArray)) { $sql .= " GROUP BY " . $this->db->sanitize($selectFields, 0, 0, 1); // To have the SUM on ps.reel working in the select. } // Sort by category if (getDolGlobalString('PRODUCT_SORT_BY_CATEGORY')) { $sql .= " ORDER BY categorie_product_id ".(getDolGlobalInt('PRODUCT_SORT_BY_CATEGORY') == 1 ? "ASC" : "DESC"); } else { $sql .= $this->db->order("p.ref"); } $limit = getDolGlobalInt('SEARCH_LIMIT_AJAX') ?: $limit; // SEARCH_LIMIT_AJAX is a hidden option that has priority on visible option PRODUIT_LIMIT_SIZE if set. $sql .= $this->db->plimit($limit, 0); /* The fast and low memory method to get and count full list converts the sql into a sql count */ /* $nbtotalofrecords = 0; $sqlforcount = preg_replace('/^'.preg_quote($sqlfields, '/').'/', 'SELECT COUNT(*) as nbtotalofrecords', $sql); $sqlforcount = preg_replace('/GROUP BY .*$/', '', $sqlforcount); $resql = $this->db->query($sqlforcount); if ($resql) { $objforcount = $this->db->fetch_object($resql); $nbtotalofrecords = $objforcount->nbtotalofrecords; } else { dol_print_error($this->db); } */ // Build output string dol_syslog(get_class($this) . "::select_produits_list search products", LOG_DEBUG); // If we have no $limit parameter, this request may hang dur to high number of lines returned. // This should not happen because this method should not be called directly, iIt is called by select_produit() that always add a $limit parameter. $result = $this->db->query($sql); if ($result) { require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php'; require_once DOL_DOCUMENT_ROOT . '/product/dynamic_price/class/price_parser.class.php'; require_once DOL_DOCUMENT_ROOT . '/core/lib/product.lib.php'; $num = $this->db->num_rows($result); $events = array(); if (!$forcecombo) { include_once DOL_DOCUMENT_ROOT . '/core/lib/ajax.lib.php'; $out .= ajax_combobox($htmlname, $events, getDolGlobalInt("PRODUIT_USE_SEARCH_TO_SELECT")); } $out .= ''; $this->db->free($result); if (empty($outputmode)) { return $out; } return $outarray; } else { dol_print_error($this->db); } return ''; } /** * Function to forge the string with OPTIONs of SELECT. * This define value for &$opt and &$optJson. * This function is called by select_produits_list(). * * @param stdClass $objp Resultset of fetch * @param string $opt Option (var used for returned value in string option format) * @param array{key?:string,value?:string,label?:string,label2?:string,desc?:string,type?:string,price_ht?:string,price_ttc?:string,price_ht_locale?:string,price_ttc_locale?:string,pricebasetype?:string,tva_tx?:string,default_vat_code?:string,qty?:string,discount?:string,duration_value?:string,duration_unit?:string,pbq?:string,labeltrans?:string,desctrans?:string,ref_customer?:string} $optJson Option (var used for returned value in json format) * @param int $price_level Price level * @param int $selected Preselected value * @param int<0,1> $hidepriceinlabel Hide price in label * @param string $filterkey Filter key to highlight * @param int<0,1> $novirtualstock Do not load virtual stock, even if slow option STOCK_SHOW_VIRTUAL_STOCK_IN_PRODUCTS_COMBO is on. * @return void */ protected function constructProductListOption(&$objp, &$opt, &$optJson, $price_level, $selected, $hidepriceinlabel = 0, $filterkey = '', $novirtualstock = 0) { global $langs, $conf, $user; global $hookmanager; $outkey = ''; $outval = ''; $outref = ''; $outlabel = ''; $outlabel_translated = ''; $outdesc = ''; $outdesc_translated = ''; $outbarcode = ''; $outorigin = ''; $outtype = ''; $outprice_ht = ''; $outprice_ttc = ''; $outpricebasetype = ''; $outtva_tx = ''; $outdefault_vat_code = ''; $outqty = 1; $outdiscount = '0'; $maxlengtharticle = getDolGlobalInt('PRODUCT_MAX_LENGTH_COMBO', 48); $productlabel = $objp->label; if (!empty($objp->label_translated)) { $productlabel = $objp->label_translated; } $label = $productlabel; if (!empty($filterkey) && $filterkey != '') { $label = preg_replace('/(' . preg_quote($filterkey, '/') . ')/i', '$1', $label, 1); } $outkey = $objp->rowid; $outref = $objp->ref; $outrefcust = empty($objp->custref) ? '' : $objp->custref; $outlabel = $objp->label; $outdesc = $objp->description; if (getDolGlobalInt('MAIN_MULTILANGS')) { $outlabel_translated = $objp->label_translated; $outdesc_translated = $objp->description_translated; } $outbarcode = $objp->barcode; $outorigin = $objp->fk_country; $outpbq = empty($objp->price_by_qty_rowid) ? '' : $objp->price_by_qty_rowid; $outtype = $objp->fk_product_type; $outdurationvalue = $outtype == Product::TYPE_SERVICE ? substr($objp->duration, 0, dol_strlen($objp->duration) - 1) : ''; $outdurationunit = $outtype == Product::TYPE_SERVICE ? substr($objp->duration, -1) : ''; if ($outorigin && getDolGlobalString('PRODUCT_SHOW_ORIGIN_IN_COMBO')) { require_once DOL_DOCUMENT_ROOT . '/core/lib/company.lib.php'; } // Units $outvalUnits = ''; if (getDolGlobalInt('PRODUCT_USE_UNITS')) { if (!empty($objp->unit_short)) { $outvalUnits .= ' - ' . $objp->unit_short; } } if (getDolGlobalString('PRODUCT_SHOW_DIMENSIONS_IN_COMBO')) { if (!empty($objp->weight) && $objp->weight_units !== null) { $unitToShow = showDimensionInBestUnit($objp->weight, $objp->weight_units, 'weight', $langs); $outvalUnits .= ' - ' . $unitToShow; } if ((!empty($objp->length) || !empty($objp->width) || !empty($objp->height)) && $objp->length_units !== null) { $unitToShow = $objp->length . ' x ' . $objp->width . ' x ' . $objp->height . ' ' . measuringUnitString(0, 'size', $objp->length_units); $outvalUnits .= ' - ' . $unitToShow; } if (!empty($objp->surface) && $objp->surface_units !== null) { $unitToShow = showDimensionInBestUnit($objp->surface, $objp->surface_units, 'surface', $langs); $outvalUnits .= ' - ' . $unitToShow; } if (!empty($objp->volume) && $objp->volume_units !== null) { $unitToShow = showDimensionInBestUnit($objp->volume, $objp->volume_units, 'volume', $langs); $outvalUnits .= ' - ' . $unitToShow; } } if ($outdurationvalue && $outdurationunit) { $da = array( 'h' => $langs->trans('Hour'), 'd' => $langs->trans('Day'), 'w' => $langs->trans('Week'), 'm' => $langs->trans('Month'), 'y' => $langs->trans('Year') ); if (isset($da[$outdurationunit])) { $outvalUnits .= ' - ' . $outdurationvalue . ' ' . $langs->transnoentities($da[$outdurationunit] . ($outdurationvalue > 1 ? 's' : '')); } } // Set stocktag (stock too low or not or unknown) $stocktag = 0; if (isModEnabled('stock') && isset($objp->stock) && ($objp->fk_product_type == Product::TYPE_PRODUCT || getDolGlobalString('STOCK_SUPPORTS_SERVICES'))) { if ($user->hasRight('stock', 'lire')) { if ($objp->stock > 0) { $stocktag = 1; } elseif ($objp->stock <= 0) { $stocktag = -1; } } } // Set full plain label for the native