<?php
/* Copyright (C) 2002-2003  Rodolphe Quiedeville    <rodolphe@quiedeville.org>
 * Copyright (C) 2002-2003  Jean-Louis Bergamo      <jlb@j1b.org>
 * Copyright (C) 2004       Sebastien Di Cintio     <sdicintio@ressource-toi.org>
 * Copyright (C) 2004       Benoit Mortier          <benoit.mortier@opensides.be>
 * Copyright (C) 2009-2012  Laurent Destailleur     <eldy@users.sourceforge.net>
 * Copyright (C) 2009-2012  Regis Houssin           <regis.houssin@inodbox.com>
 * Copyright (C) 2013       Florian Henry           <forian.henry@open-concept.pro>
 * Copyright (C) 2015-2023  Charlene BENKE          <charlene@patas-monkey.com>
 * Copyright (C) 2016       Raphaël Doursenaud      <rdoursenaud@gpcsolutions.fr>
 * Copyright (C) 2017       Nicolas ZABOURI         <info@inovea-conseil.com>
 * Copyright (C) 2018-2022  Frédéric France         <frederic.france@netlogic.fr>
 * Copyright (C) 2022 		Antonin MARCHAL         <antonin@letempledujeu.fr>
 *
 * 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 <https://www.gnu.org/licenses/>.
 */

/**
 * 	\file 		htdocs/core/class/extrafields.class.php
 *	\ingroup    core
 *	\brief      File of class to manage extra fields
 */


/**
 *	Class to manage standard extra fields
 */
class ExtraFields
{
	/**
	 * @var DoliDB Database handler.
	 */
	public $db;

	/**
	 * @var array New array to store extrafields definition
	 */
	public $attributes;

	/**
	 * @var array	Array with boolean of status of groups
	 */
	public $expand_display;

	/**
	 * @var string Error code (or message)
	 */
	public $error = '';

	/**
	 * @var string[] Array of Error code (or message)
	 */
	public $errors = array();

	/**
	 * @var string 	DB Error number
	 */
	public $errno;

	/**
	 * @var array 	Array of type to label
	 */
	public static $type2label = array(
		'varchar'=>'String1Line',
		'text'=>'TextLongNLines',
		'html'=>'HtmlText',
		'int'=>'Int',
		'double'=>'Float',
		'date'=>'Date',
		'datetime'=>'DateAndTime',
		//'datetimegmt'=>'DateAndTimeUTC',
		'boolean'=>'Boolean',
		'price'=>'ExtrafieldPrice',
		'pricecy'=>'ExtrafieldPriceWithCurrency',
		'phone'=>'ExtrafieldPhone',
		'mail'=>'ExtrafieldMail',
		'url'=>'ExtrafieldUrl',
		'ip'=>'ExtrafieldIP',
		'icon'=>'Icon',
		'password' => 'ExtrafieldPassword',
		'select' => 'ExtrafieldSelect',
		'sellist' => 'ExtrafieldSelectList',
		'radio' => 'ExtrafieldRadio',
		'checkbox' => 'ExtrafieldCheckBox',
		'chkbxlst' => 'ExtrafieldCheckBoxFromList',
		'link' => 'ExtrafieldLink',
		'separate' => 'ExtrafieldSeparator',
	);

	/**
	 *	Constructor
	 *
	 *  @param		DoliDB		$db      Database handler
	 */
	public function __construct($db)
	{
		$this->db = $db;
		$this->error = '';
		$this->errors = array();
		$this->attributes = array();
	}

	/**
	 *  Add a new extra field parameter
	 *
	 *  @param	string			$attrname           Code of attribute
	 *  @param  string			$label              label of attribute
	 *  @param  string			$type               Type of attribute ('boolean','int','varchar','text','html','date','datetime','price', 'pricecy', 'phone','mail','password','url','select','checkbox','separate',...)
	 *  @param  int				$pos                Position of attribute
	 *  @param  string			$size               Size/length definition of attribute ('5', '24,8', ...). For float, it contains 2 numeric separated with a comma.
	 *  @param  string			$elementtype        Element type. Same value than object->table_element (Example 'member', 'product', 'thirdparty', ...)
	 *  @param	int				$unique				Is field unique or not
	 *  @param	int				$required			Is field required or not
	 *  @param	string			$default_value		Defaulted value (In database. use the default_value feature for default value on screen. Example: '', '0', 'null', 'avalue')
	 *  @param  array|string	$param				Params for field (ex for select list : array('options' => array(value'=>'label of option')) )
	 *  @param  int				$alwayseditable		Is attribute always editable regardless of the document status
	 *  @param	string			$perms				Permission to check
	 *  @param	string			$list				Visibility ('0'=never visible, '1'=visible on list+forms, '2'=list only, '3'=form only or 'eval string')
	 *  @param	string			$help				Text with help tooltip
	 *  @param  string  		$computed           Computed value
	 *  @param  string  		$entity    		 	Entity of extrafields (for multicompany modules)
	 *  @param  string  		$langfile  		 	Language file
	 *  @param  string  		$enabled  		 	Condition to have the field enabled or not
	 *  @param	int				$totalizable		Is a measure. Must show a total on lists
	 *  @param  int             $printable          Is extrafield displayed on PDF
	 *  @param	array			$moreparams			More parameters. Example: array('css'=>, 'csslist'=>Css on list, 'cssview'=>...)
	 *  @return int      							Return integer <=0 if KO, >0 if OK
	 */
	public function addExtraField($attrname, $label, $type, $pos, $size, $elementtype, $unique = 0, $required = 0, $default_value = '', $param = '', $alwayseditable = 0, $perms = '', $list = '-1', $help = '', $computed = '', $entity = '', $langfile = '', $enabled = '1', $totalizable = 0, $printable = 0, $moreparams = array())
	{
		if (empty($attrname)) {
			return -1;
		}
		if (empty($label)) {
			return -1;
		}

		$result = 0;

		if ($type == 'separate') {
			$unique = 0;
			$required = 0;
		}	// Force unique and not required if this is a separator field to avoid troubles.
		if ($elementtype == 'thirdparty') {
			$elementtype = 'societe';
		}
		if ($elementtype == 'contact') {
			$elementtype = 'socpeople';
		}

		// Create field into database except for separator type which is not stored in database
		if ($type != 'separate') {
			$result = $this->create($attrname, $type, $size, $elementtype, $unique, $required, $default_value, $param, $perms, $list, $computed, $help, $moreparams);
		}
		$err1 = $this->errno;
		if ($result > 0 || $err1 == 'DB_ERROR_COLUMN_ALREADY_EXISTS' || $type == 'separate') {
			// Add declaration of field into table
			$result2 = $this->create_label($attrname, $label, $type, $pos, $size, $elementtype, $unique, $required, $param, $alwayseditable, $perms, $list, $help, $default_value, $computed, $entity, $langfile, $enabled, $totalizable, $printable, $moreparams);
			$err2 = $this->errno;
			if ($result2 > 0 || ($err1 == 'DB_ERROR_COLUMN_ALREADY_EXISTS' && $err2 == 'DB_ERROR_RECORD_ALREADY_EXISTS')) {
				$this->error = '';
				$this->errno = 0;
				return 1;
			} else {
				return -2;
			}
		} else {
			return -1;
		}
	}

	/**
	 *	Add a new optional attribute.
	 *  This is a private method. For public method, use addExtraField.
	 *
	 *	@param	string	$attrname			code of attribute
	 *  @param	string	$type				Type of attribute ('boolean', 'int', 'varchar', 'text', 'html', 'date', 'datetime', 'price', 'pricecy', 'phone', 'mail', 'password', 'url', 'select', 'checkbox', ...)
	 *  @param	string	$length				Size/length of attribute ('5', '24,8', ...)
	 *  @param  string	$elementtype        Element type ('member', 'product', 'thirdparty', 'contact', ...)
	 *  @param	int		$unique				Is field unique or not
	 *  @param	int		$required			Is field required or not
	 *  @param  string  $default_value		Default value for field (in database)
	 *  @param  array	$param				Params for field  (ex for select list : array('options'=>array('value'=>'label of option'))
	 *  @param	string	$perms				Permission
	 *	@param	string	$list				Into list view by default
	 *  @param  string  $computed           Computed value
	 *  @param	string	$help				Help on tooltip
	 *  @param	array	$moreparams			More parameters. Example: array('css'=>, 'csslist'=>, 'cssview'=>...)
	 *  @return int      	           		Return integer <=0 if KO, >0 if OK
	 */
	private function create($attrname, $type = 'varchar', $length = '255', $elementtype = 'member', $unique = 0, $required = 0, $default_value = '', $param = array(), $perms = '', $list = '0', $computed = '', $help = '', $moreparams = array())
	{
		if ($elementtype == 'thirdparty') {
			$elementtype = 'societe';
		}
		if ($elementtype == 'contact') {
			$elementtype = 'socpeople';
		}

		$table = $elementtype.'_extrafields';
		if ($elementtype == 'categorie') {
			$table = 'categories_extrafields';
		}

		if (!empty($attrname) && preg_match("/^\w[a-zA-Z0-9_]*$/", $attrname) && !is_numeric($attrname)) {
			if ($type == 'boolean') {
				$typedb = 'int';
				$lengthdb = '1';
			} elseif ($type == 'price') {
				$typedb = 'double';
				$lengthdb = '24,8';
			} elseif ($type == 'pricecy') {
				$typedb = 'varchar';
				$lengthdb = '64';
			} elseif ($type == 'phone') {
				$typedb = 'varchar';
				$lengthdb = '20';
			} elseif ($type == 'mail' || $type == 'ip' || $type == 'icon') {
				$typedb = 'varchar';
				$lengthdb = '128';
			} elseif ($type == 'url') {
				$typedb = 'varchar';
				$lengthdb = '255';
			} elseif (($type == 'select') || ($type == 'sellist') || ($type == 'radio') || ($type == 'checkbox') || ($type == 'chkbxlst')) {
				$typedb = 'varchar';
				$lengthdb = '255';
			} elseif ($type == 'link') {
				$typedb = 'int';
				$lengthdb = '11';
			} elseif ($type == 'html') {
				$typedb = 'text';
				$lengthdb = $length;
			} elseif ($type == 'password') {
				$typedb = 'varchar';
				$lengthdb = '128';
			} else {
				$typedb = $type;
				$lengthdb = $length;
				if ($type == 'varchar' && empty($lengthdb)) {
					$lengthdb = '255';
				}
			}
			$field_desc = array(
				'type'=>$typedb,
				'value'=>$lengthdb,
				'null'=>($required ? 'NOT NULL' : 'NULL'),
				'default' => $default_value
			);

			$result = $this->db->DDLAddField($this->db->prefix().$table, $attrname, $field_desc);
			if ($result > 0) {
				if ($unique) {
					$sql = "ALTER TABLE ".$this->db->prefix().$table." ADD UNIQUE INDEX uk_".$table."_".$attrname." (".$attrname.")";
					$resql = $this->db->query($sql, 1, 'dml');
				}
				return 1;
			} else {
				$this->error = $this->db->lasterror();
				$this->errno = $this->db->lasterrno();
				return -1;
			}
		} else {
			return 0;
		}
	}

	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
	/**
	 *	Add description of a new optional attribute
	 *
	 *	@param	string			$attrname		code of attribute
	 *	@param	string			$label			label of attribute
	 *  @param	string			$type			Type of attribute ('int', 'varchar', 'text', 'html', 'date', 'datehour', 'float')
	 *  @param	int				$pos			Position of attribute
	 *  @param	string			$size			Size/length of attribute ('5', '24,8', ...)
	 *  @param  string			$elementtype	Element type ('member', 'product', 'thirdparty', ...)
	 *  @param	int				$unique			Is field unique or not
	 *  @param	int				$required		Is field required or not
	 *  @param  array|string	$param			Params for field  (ex for select list : array('options' => array(value'=>'label of option')) )
	 *  @param  int				$alwayseditable	Is attribute always editable regardless of the document status
	 *  @param	string			$perms			Permission to check
	 *  @param	string			$list			Visibily
	 *  @param	string			$help			Help on tooltip
	 *  @param  string          $default        Default value (in database. use the default_value feature for default value on screen).
	 *  @param  string          $computed       Computed value
	 *  @param  string          $entity     	Entity of extrafields
	 *  @param	string			$langfile		Language file
	 *  @param  string  		$enabled  		Condition to have the field enabled or not
	 *  @param	int				$totalizable	Is a measure. Must show a total on lists
	 *  @param  int             $printable      Is extrafield displayed on PDF
	 *  @param	array			$moreparams		More parameters. Example: array('css'=>, 'csslist'=>, 'cssview'=>...)
	 *  @return	int								Return integer <=0 if KO, >0 if OK
	 *  @throws Exception
	 */
	private function create_label($attrname, $label = '', $type = '', $pos = 0, $size = '', $elementtype = 'member', $unique = 0, $required = 0, $param = '', $alwayseditable = 0, $perms = '', $list = '-1', $help = '', $default = '', $computed = '', $entity = '', $langfile = '', $enabled = '1', $totalizable = 0, $printable = 0, $moreparams = array())
	{
		// phpcs:enable
		global $conf, $user;

		if ($elementtype == 'thirdparty') {
			$elementtype = 'societe';
		}
		if ($elementtype == 'contact') {
			$elementtype = 'socpeople';
		}

		// Clean parameters
		if (empty($pos)) {
			$pos = 0;
		}
		if (empty($list)) {
			$list = '0';
		}
		if (empty($required)) {
			$required = 0;
		}
		if (empty($unique)) {
			$unique = 0;
		}
		if (empty($printable)) {
			$printable = 0;
		}
		if (empty($alwayseditable)) {
			$alwayseditable = 0;
		}
		if (empty($totalizable)) {
			$totalizable = 0;
		}

		$css = '';
		if (!empty($moreparams) && !empty($moreparams['css'])) {
			$css = $moreparams['css'];
		}
		$csslist = '';
		if (!empty($moreparams) && !empty($moreparams['csslist'])) {
			$csslist = $moreparams['csslist'];
		}
		$cssview = '';
		if (!empty($moreparams) && !empty($moreparams['cssview'])) {
			$cssview = $moreparams['cssview'];
		}

		if (!empty($attrname) && preg_match("/^\w[a-zA-Z0-9-_]*$/", $attrname) && !is_numeric($attrname)) {
			if (is_array($param) && count($param) > 0) {
				$params = serialize($param);
			} elseif (strlen($param) > 0) {
				$params = trim($param);
			} else {
				$params = '';
			}

			$sql = "INSERT INTO ".$this->db->prefix()."extrafields(";
			$sql .= " name,";
			$sql .= " label,";
			$sql .= " type,";
			$sql .= " pos,";
			$sql .= " size,";
			$sql .= " entity,";
			$sql .= " elementtype,";
			$sql .= " fieldunique,";
			$sql .= " fieldrequired,";
			$sql .= " param,";
			$sql .= " alwayseditable,";
			$sql .= " perms,";
			$sql .= " langs,";
			$sql .= " list,";
			$sql .= " printable,";
			$sql .= " fielddefault,";
			$sql .= " fieldcomputed,";
			$sql .= " fk_user_author,";
			$sql .= " fk_user_modif,";
			$sql .= " datec,";
			$sql .= " enabled,";
			$sql .= " help,";
			$sql .= " totalizable,";
			$sql .= " css,";
			$sql .= " csslist,";
			$sql .= " cssview";
			$sql .= " )";
			$sql .= " VALUES('".$this->db->escape($attrname)."',";
			$sql .= " '".$this->db->escape($label)."',";
			$sql .= " '".$this->db->escape($type)."',";
			$sql .= " ".((int) $pos).",";
			$sql .= " '".$this->db->escape($size)."',";
			$sql .= " ".($entity === '' ? $conf->entity : $entity).",";
			$sql .= " '".$this->db->escape($elementtype)."',";
			$sql .= " ".((int) $unique).",";
			$sql .= " ".((int) $required).",";
			$sql .= " '".$this->db->escape($params)."',";
			$sql .= " ".((int) $alwayseditable).",";
			$sql .= " ".($perms ? "'".$this->db->escape($perms)."'" : "null").",";
			$sql .= " ".($langfile ? "'".$this->db->escape($langfile)."'" : "null").",";
			$sql .= " '".$this->db->escape($list)."',";
			$sql .= " '".$this->db->escape($printable)."',";
			$sql .= " ".($default ? "'".$this->db->escape($default)."'" : "null").",";
			$sql .= " ".($computed ? "'".$this->db->escape($computed)."'" : "null").",";
			$sql .= " ".(is_object($user) ? $user->id : 0).",";
			$sql .= " ".(is_object($user) ? $user->id : 0).",";
			$sql .= "'".$this->db->idate(dol_now())."',";
			$sql .= " ".($enabled ? "'".$this->db->escape($enabled)."'" : "1").",";
			$sql .= " ".($help ? "'".$this->db->escape($help)."'" : "null").",";
			$sql .= " ".($totalizable ? 'TRUE' : 'FALSE').",";
			$sql .= " ".($css ? "'".$this->db->escape($css)."'" : "null").",";
			$sql .= " ".($csslist ? "'".$this->db->escape($csslist)."'" : "null").",";
			$sql .= " ".($cssview ? "'".$this->db->escape($cssview)."'" : "null");
			$sql .= ')';

			dol_syslog(get_class($this)."::create_label", LOG_DEBUG);
			if ($this->db->query($sql)) {
				return 1;
			} else {
				$this->error = $this->db->lasterror();
				$this->errno = $this->db->lasterrno();
				return -1;
			}
		}
		return -1;
	}

	/**
	 *	Delete an optional attribute
	 *
	 *	@param	string	$attrname		Code of attribute to delete
	 *  @param  string	$elementtype    Element type ('member', 'product', 'thirdparty', 'contact', ...)
	 *  @return int              		Return integer < 0 if KO, 0 if nothing is done, 1 if OK
	 */
	public function delete($attrname, $elementtype = 'member')
	{
		if ($elementtype == 'thirdparty') {
			$elementtype = 'societe';
		}
		if ($elementtype == 'contact') {
			$elementtype = 'socpeople';
		}

		$table = $elementtype.'_extrafields';
		if ($elementtype == 'categorie') {
			$table = 'categories_extrafields';
		}

		$error = 0;

		if (!empty($attrname) && preg_match("/^\w[a-zA-Z0-9-_]*$/", $attrname)) {
			$result = $this->delete_label($attrname, $elementtype);
			if ($result < 0) {
				$this->error = $this->db->lasterror();
				$this->errors[] = $this->db->lasterror();
				$error++;
			}

			if (!$error) {
				$sql = "SELECT COUNT(rowid) as nb";
				$sql .= " FROM ".$this->db->prefix()."extrafields";
				$sql .= " WHERE elementtype = '".$this->db->escape($elementtype)."'";
				$sql .= " AND name = '".$this->db->escape($attrname)."'";
				//$sql.= " AND entity IN (0,".$conf->entity.")";      Do not test on entity here. We want to see if there is still on field remaning in other entities before deleting field in table
				$resql = $this->db->query($sql);
				if ($resql) {
					$obj = $this->db->fetch_object($resql);
					if ($obj->nb <= 0) {
						$result = $this->db->DDLDropField($this->db->prefix().$table, $attrname); // This also drop the unique key
						if ($result < 0) {
							$this->error = $this->db->lasterror();
							$this->errors[] = $this->db->lasterror();
							$error++;
						}
					}
				}
			}

			return $result;
		} else {
			return 0;
		}
	}

	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
	/**
	 *	Delete description of an optional attribute
	 *
	 *	@param	string	$attrname			Code of attribute to delete
	 *  @param  string	$elementtype        Element type ('member', 'product', 'thirdparty', ...)
	 *  @return int              			Return integer < 0 if KO, 0 if nothing is done, 1 if OK
	 */
	private function delete_label($attrname, $elementtype = 'member')
	{
		// phpcs:enable
		global $conf;

		if ($elementtype == 'thirdparty') {
			$elementtype = 'societe';
		}
		if ($elementtype == 'contact') {
			$elementtype = 'socpeople';
		}

		if (isset($attrname) && $attrname != '' && preg_match("/^\w[a-zA-Z0-9-_]*$/", $attrname)) {
			$sql = "DELETE FROM ".$this->db->prefix()."extrafields";
			$sql .= " WHERE name = '".$this->db->escape($attrname)."'";
			$sql .= " AND entity IN  (0,".$conf->entity.')';
			$sql .= " AND elementtype = '".$this->db->escape($elementtype)."'";

			dol_syslog(get_class($this)."::delete_label", LOG_DEBUG);
			$resql = $this->db->query($sql);
			if ($resql) {
				return 1;
			} else {
				dol_print_error($this->db);
				return -1;
			}
		} else {
			return 0;
		}
	}

	/**
	 * 	Modify type of a personalized attribute
	 *
	 *  @param	string	$attrname			Name of attribute
	 *  @param	string	$label				Label of attribute
	 *  @param	string	$type				Type of attribute ('boolean', 'int', 'varchar', 'text', 'html', 'date', 'datetime','price','phone','mail','password','url','select','checkbox', ...)
	 *  @param	int		$length				Length of attribute
	 *  @param  string	$elementtype        Element type ('member', 'product', 'thirdparty', 'contact', ...)
	 *  @param	int		$unique				Is field unique or not
	 *  @param	int		$required			Is field required or not
	 *  @param	int		$pos				Position of attribute
	 *  @param  array	$param				Params for field (ex for select list : array('options' => array(value'=>'label of option')) )
	 *  @param  int		$alwayseditable		Is attribute always editable regardless of the document status
	 *  @param	string	$perms				Permission to check
	 *  @param	string	$list				Visibility
	 *  @param	string	$help				Help on tooltip
	 *  @param  string  $default            Default value (in database. use the default_value feature for default value on screen).
	 *  @param  string  $computed           Computed value
	 *  @param  string  $entity	            Entity of extrafields
	 *  @param	string	$langfile			Language file
	 *  @param  string  $enabled  			Condition to have the field enabled or not
	 *  @param  int     $totalizable        Is extrafield totalizable on list
	 *  @param  int     $printable        	Is extrafield displayed on PDF
	 *  @param	array	$moreparams			More parameters. Example: array('css'=>, 'csslist'=>, 'cssview'=>...)
	 * 	@return	int							>0 if OK, <=0 if KO
	 *  @throws Exception
	 */
	public function update($attrname, $label, $type, $length, $elementtype, $unique = 0, $required = 0, $pos = 0, $param = array(), $alwayseditable = 0, $perms = '', $list = '', $help = '', $default = '', $computed = '', $entity = '', $langfile = '', $enabled = '1', $totalizable = 0, $printable = 0, $moreparams = array())
	{
		global $action, $hookmanager;

		if ($elementtype == 'thirdparty') {
			$elementtype = 'societe';
		}
		if ($elementtype == 'contact') {
			$elementtype = 'socpeople';
		}

		$table = $elementtype.'_extrafields';
		if ($elementtype == 'categorie') {
			$table = 'categories_extrafields';
		}

		if (isset($attrname) && $attrname != '' && preg_match("/^\w[a-zA-Z0-9-_]*$/", $attrname)) {
			if ($type == 'boolean') {
				$typedb = 'int';
				$lengthdb = '1';
			} elseif ($type == 'price') {
				$typedb = 'double';
				$lengthdb = '24,8';
			} elseif ($type == 'pricecy') {
				$typedb = 'varchar';
				$lengthdb = '64';
			} elseif ($type == 'phone') {
				$typedb = 'varchar';
				$lengthdb = '20';
			} elseif ($type == 'mail' || $type == 'ip' || $type == 'icon') {
				$typedb = 'varchar';
				$lengthdb = '128';
			} elseif ($type == 'url') {
				$typedb = 'varchar';
				$lengthdb = '255';
			} elseif (($type == 'select') || ($type == 'sellist') || ($type == 'radio') || ($type == 'checkbox') || ($type == 'chkbxlst')) {
				$typedb = 'varchar';
				$lengthdb = '255';
			} elseif ($type == 'html') {
				$typedb = 'text';
			} elseif ($type == 'link') {
				$typedb = 'int';
				$lengthdb = '11';
			} elseif ($type == 'password') {
				$typedb = 'varchar';
				$lengthdb = '128';
			} else {
				$typedb = $type;
				$lengthdb = $length;
			}
			$field_desc = array('type'=>$typedb, 'value'=>$lengthdb, 'null'=>($required ? 'NOT NULL' : 'NULL'), 'default'=>$default);

			if (is_object($hookmanager)) {
				$hookmanager->initHooks(array('extrafieldsdao'));
				$parameters = array('field_desc'=>&$field_desc, 'table'=>$table, 'attr_name'=>$attrname, 'label'=>$label, 'type'=>$type, 'length'=>$length, 'unique'=>$unique, 'required'=>$required, 'pos'=>$pos, 'param'=>$param, 'alwayseditable'=>$alwayseditable, 'perms'=>$perms, 'list'=>$list, 'help'=>$help, 'default'=>$default, 'computed'=>$computed, 'entity'=>$entity, 'langfile'=>$langfile, 'enabled'=>$enabled, 'totalizable'=>$totalizable, 'printable'=>$printable);
				$reshook = $hookmanager->executeHooks('updateExtrafields', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks

				if ($reshook < 0) {
					$this->error = $this->db->lasterror();
					return -1;
				}
			}

			if ($type != 'separate') { // No table update when separate type
				$result = $this->db->DDLUpdateField($this->db->prefix().$table, $attrname, $field_desc);
			}
			if ($result > 0 || $type == 'separate') {
				if ($label) {
					$result = $this->update_label($attrname, $label, $type, $length, $elementtype, $unique, $required, $pos, $param, $alwayseditable, $perms, $list, $help, $default, $computed, $entity, $langfile, $enabled, $totalizable, $printable, $moreparams);
				}
				if ($result > 0) {
					$sql = '';
					if ($unique) {
						$sql = "ALTER TABLE ".$this->db->prefix().$table." ADD UNIQUE INDEX uk_".$table."_".$attrname." (".$attrname.")";
					} else {
						$sql = "ALTER TABLE ".$this->db->prefix().$table." DROP INDEX IF EXISTS uk_".$table."_".$attrname;
					}
					dol_syslog(get_class($this).'::update', LOG_DEBUG);
					$resql = $this->db->query($sql, 1, 'dml');
					/*if ($resql < 0) {
						$this->error = $this->db->lasterror();
						return -1;
					}*/
					return 1;
				} else {
					$this->error = $this->db->lasterror();
					return -1;
				}
			} else {
				$this->error = $this->db->lasterror();
				return -1;
			}
		} else {
			return 0;
		}
	}

	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
	/**
	 *  Modify description of personalized attribute
	 *
	 *  @param	string	$attrname			Name of attribute
	 *  @param	string	$label				Label of attribute
	 *  @param  string	$type               Type of attribute
	 *  @param  int		$size		        Length of attribute
	 *  @param  string	$elementtype		Element type ('member', 'product', 'thirdparty', ...)
	 *  @param	int		$unique				Is field unique or not
	 *  @param	int		$required			Is field required or not
	 *  @param	int		$pos				Position of attribute
	 *  @param  array	$param				Params for field  (ex for select list : array('options' => array(value'=>'label of option')) )
	 *  @param  int		$alwayseditable		Is attribute always editable regardless of the document status
	 *  @param	string	$perms				Permission to check
	 *  @param	string	$list				Visiblity
	 *  @param	string	$help				Help on tooltip.
	 *  @param  string  $default            Default value (in database. use the default_value feature for default value on screen).
	 *  @param  string  $computed           Computed value
	 *  @param  string  $entity     		Entity of extrafields
	 *  @param	string	$langfile			Language file
	 *  @param  string  $enabled  			Condition to have the field enabled or not
	 *  @param  int     $totalizable        Is extrafield totalizable on list
	 *  @param  int     $printable        	Is extrafield displayed on PDF
	 *  @param	array	$moreparams			More parameters. Example: array('css'=>, 'csslist'=>, 'cssview'=>...)
	 *  @return	int							Return integer <=0 if KO, >0 if OK
	 *  @throws Exception
	 */
	private function update_label($attrname, $label, $type, $size, $elementtype, $unique = 0, $required = 0, $pos = 0, $param = array(), $alwayseditable = 0, $perms = '', $list = '0', $help = '', $default = '', $computed = '', $entity = '', $langfile = '', $enabled = '1', $totalizable = 0, $printable = 0, $moreparams = array())
	{
		// phpcs:enable
		global $conf, $user;
		dol_syslog(get_class($this)."::update_label ".$attrname.", ".$label.", ".$type.", ".$size.", ".$elementtype.", ".$unique.", ".$required.", ".$pos.", ".$alwayseditable.", ".$perms.", ".$list.", ".$default.", ".$computed.", ".$entity.", ".$langfile.", ".$enabled.", ".$totalizable.", ".$printable);

		// Clean parameters
		if ($elementtype == 'thirdparty') {
			$elementtype = 'societe';
		}
		if ($elementtype == 'contact') {
			$elementtype = 'socpeople';
		}

		if (empty($pos)) {
			$pos = 0;
		}
		if (empty($list)) {
			$list = '0';
		}
		if (empty($totalizable)) {
			$totalizable = 0;
		}
		if (empty($required)) {
			$required = 0;
		}
		if (empty($unique)) {
			$unique = 0;
		}
		if (empty($alwayseditable)) {
			$alwayseditable = 0;
		}

		$css = '';
		if (!empty($moreparams) && !empty($moreparams['css'])) {
			$css = $moreparams['css'];
		}
		$csslist = '';
		if (!empty($moreparams) && !empty($moreparams['csslist'])) {
			$csslist = $moreparams['csslist'];
		}
		$cssview = '';
		if (!empty($moreparams) && !empty($moreparams['cssview'])) {
			$cssview = $moreparams['cssview'];
		}

		if (isset($attrname) && $attrname != '' && preg_match("/^\w[a-zA-Z0-9-_]*$/", $attrname)) {
			$this->db->begin();

			if (is_array($param) && count($param) > 0) {
				$params = serialize($param);
			} elseif (is_array($param)) {
				$params = '';
			} elseif (strlen($param) > 0) {
				$params = trim($param);
			} else {
				$params = '';
			}

			if ($entity === '' || $entity != '0') {
				// We dont want on all entities, we delete all and current
				$sql_del = "DELETE FROM ".$this->db->prefix()."extrafields";
				$sql_del .= " WHERE name = '".$this->db->escape($attrname)."'";
				$sql_del .= " AND entity IN (0, ".($entity === '' ? $conf->entity : $entity).")";
				$sql_del .= " AND elementtype = '".$this->db->escape($elementtype)."'";
			} else {
				// We want on all entities ($entities = '0'), we delete on all only (we keep setup specific to each entity)
				$sql_del = "DELETE FROM ".$this->db->prefix()."extrafields";
				$sql_del .= " WHERE name = '".$this->db->escape($attrname)."'";
				$sql_del .= " AND entity = 0";
				$sql_del .= " AND elementtype = '".$this->db->escape($elementtype)."'";
			}
			$resql1 = $this->db->query($sql_del);

			$sql = "INSERT INTO ".$this->db->prefix()."extrafields(";
			$sql .= " name,"; // This is code
			$sql .= " entity,";
			$sql .= " label,";
			$sql .= " type,";
			$sql .= " size,";
			$sql .= " elementtype,";
			$sql .= " fieldunique,";
			$sql .= " fieldrequired,";
			$sql .= " perms,";
			$sql .= " langs,";
			$sql .= " pos,";
			$sql .= " alwayseditable,";
			$sql .= " param,";
			$sql .= " list,";
			$sql .= " printable,";
			$sql .= " totalizable,";
			$sql .= " fielddefault,";
			$sql .= " fieldcomputed,";
			$sql .= " fk_user_author,";
			$sql .= " fk_user_modif,";
			$sql .= " datec,";
			$sql .= " enabled,";
			$sql .= " help,";
			$sql .= " css,";
			$sql .= " csslist,";
			$sql .= " cssview";
			$sql .= ") VALUES (";
			$sql .= "'".$this->db->escape($attrname)."',";
			$sql .= " ".($entity === '' ? $conf->entity : $entity).",";
			$sql .= " '".$this->db->escape($label)."',";
			$sql .= " '".$this->db->escape($type)."',";
			$sql .= " '".$this->db->escape($size)."',";
			$sql .= " '".$this->db->escape($elementtype)."',";
			$sql .= " ".$unique.",";
			$sql .= " ".$required.",";
			$sql .= " ".($perms ? "'".$this->db->escape($perms)."'" : "null").",";
			$sql .= " ".($langfile ? "'".$this->db->escape($langfile)."'" : "null").",";
			$sql .= " ".$pos.",";
			$sql .= " '".$this->db->escape($alwayseditable)."',";
			$sql .= " '".$this->db->escape($params)."',";
			$sql .= " '".$this->db->escape($list)."', ";
			$sql .= " '".$this->db->escape($printable)."', ";
			$sql .= " ".($totalizable ? 'TRUE' : 'FALSE').",";
			$sql .= " ".(($default != '') ? "'".$this->db->escape($default)."'" : "null").",";
			$sql .= " ".($computed ? "'".$this->db->escape($computed)."'" : "null").",";
			$sql .= " ".$user->id.",";
			$sql .= " ".$user->id.",";
			$sql .= "'".$this->db->idate(dol_now())."',";
			$sql .= "'".$this->db->escape($enabled)."',";
			$sql .= " ".($help ? "'".$this->db->escape($help)."'" : "null").",";
			$sql .= " ".($css ? "'".$this->db->escape($css)."'" : "null").",";
			$sql .= " ".($csslist ? "'".$this->db->escape($csslist)."'" : "null").",";
			$sql .= " ".($cssview ? "'".$this->db->escape($cssview)."'" : "null");
			$sql .= ")";

			$resql2 = $this->db->query($sql);

			if ($resql1 && $resql2) {
				$this->db->commit();
				return 1;
			} else {
				$this->db->rollback();
				dol_print_error($this->db);
				return -1;
			}
		} else {
			return 0;
		}
	}


	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
	/**
	 * 	Load the array of extrafields defintion $this->attributes
	 *
	 * 	@param	string		$elementtype		Type of element ('all' = all or $object->table_element like 'adherent', 'commande', 'thirdparty', 'facture', 'propal', 'product', ...).
	 * 	@param	boolean		$forceload			Force load of extra fields whatever is status of cache.
	 * 	@return	array							Array of attributes keys+label for all extra fields.
	 */
	public function fetch_name_optionals_label($elementtype, $forceload = false)
	{
		// phpcs:enable
		global $conf;

		if (empty($elementtype)) {
			return array();
		}

		if ($elementtype == 'thirdparty') {
			$elementtype = 'societe';
		}
		if ($elementtype == 'contact') {
			$elementtype = 'socpeople';
		}
		if ($elementtype == 'order_supplier') {
			$elementtype = 'commande_fournisseur';
		}

		// Test cache $this->attributes[$elementtype]['loaded'] to see if we must do something
		// TODO

		$array_name_label = array();

		// We should not have several time this request. If we have, there is some optimization to do by calling a simple $extrafields->fetch_optionals() in top of code and not into subcode
		$sql = "SELECT rowid, name, label, type, size, elementtype, fieldunique, fieldrequired, param, pos, alwayseditable, perms, langs, list, printable, totalizable, fielddefault, fieldcomputed, entity, enabled, help,";
		$sql .= " css, cssview, csslist";
		$sql .= " FROM ".$this->db->prefix()."extrafields";
		//$sql.= " WHERE entity IN (0,".$conf->entity.")";    // Filter is done later
		if ($elementtype && $elementtype != 'all') {
			$sql .= " WHERE elementtype = '".$this->db->escape($elementtype)."'"; // Filed with object->table_element
		}
		$sql .= " ORDER BY pos";

		$resql = $this->db->query($sql);
		if ($resql) {
			$count = 0;
			if ($this->db->num_rows($resql)) {
				while ($tab = $this->db->fetch_object($resql)) {
					if ($tab->entity != 0 && $tab->entity != $conf->entity) {
						// This field is not in current entity. We discard but before we save it into the array of mandatory fields if it is a mandatory field without default value
						if ($tab->fieldrequired && is_null($tab->fielddefault)) {
							$this->attributes[$tab->elementtype]['mandatoryfieldsofotherentities'][$tab->name] = $tab->type;
						}
						continue;
					}

					// We can add this attribute to object. TODO Remove this and return $this->attributes[$elementtype]['label']
					if ($tab->type != 'separate') {
						$array_name_label[$tab->name] = $tab->label;
					}

					$this->attributes[$tab->elementtype]['type'][$tab->name] = $tab->type;
					$this->attributes[$tab->elementtype]['label'][$tab->name] = $tab->label;
					$this->attributes[$tab->elementtype]['size'][$tab->name] = $tab->size;
					$this->attributes[$tab->elementtype]['elementtype'][$tab->name] = $tab->elementtype;
					$this->attributes[$tab->elementtype]['default'][$tab->name] = $tab->fielddefault;
					$this->attributes[$tab->elementtype]['computed'][$tab->name] = $tab->fieldcomputed;
					$this->attributes[$tab->elementtype]['unique'][$tab->name] = $tab->fieldunique;
					$this->attributes[$tab->elementtype]['required'][$tab->name] = $tab->fieldrequired;
					$this->attributes[$tab->elementtype]['param'][$tab->name] = ($tab->param ? jsonOrUnserialize($tab->param) : '');
					$this->attributes[$tab->elementtype]['pos'][$tab->name] = $tab->pos;
					$this->attributes[$tab->elementtype]['alwayseditable'][$tab->name] = $tab->alwayseditable;
					$this->attributes[$tab->elementtype]['perms'][$tab->name] = ((is_null($tab->perms) || strlen($tab->perms) == 0) ? 1 : $tab->perms);
					$this->attributes[$tab->elementtype]['langfile'][$tab->name] = $tab->langs;
					$this->attributes[$tab->elementtype]['list'][$tab->name] = $tab->list;
					$this->attributes[$tab->elementtype]['printable'][$tab->name] = $tab->printable;
					$this->attributes[$tab->elementtype]['totalizable'][$tab->name] = ($tab->totalizable ? 1 : 0);
					$this->attributes[$tab->elementtype]['entityid'][$tab->name] = $tab->entity;
					$this->attributes[$tab->elementtype]['enabled'][$tab->name] = $tab->enabled;
					$this->attributes[$tab->elementtype]['help'][$tab->name] = $tab->help;
					$this->attributes[$tab->elementtype]['css'][$tab->name] = $tab->css;
					$this->attributes[$tab->elementtype]['cssview'][$tab->name] = $tab->cssview;
					$this->attributes[$tab->elementtype]['csslist'][$tab->name] = $tab->csslist;

					$this->attributes[$tab->elementtype]['loaded'] = 1;
					$count++;
				}
			}
			if ($elementtype) {
				$this->attributes[$elementtype]['loaded'] = 1; // Note: If nothing is found, we also set the key 'loaded' to 1.
				$this->attributes[$elementtype]['count'] = $count;
			}
		} else {
			$this->error = $this->db->lasterror();
			dol_syslog(get_class($this)."::fetch_name_optionals_label ".$this->error, LOG_ERR);
		}

		return $array_name_label;
	}


	/**
	 * Return HTML string to put an input field into a page
	 * Code very similar with showInputField of common object
	 *
	 * @param  string        $key            		Key of attribute
	 * @param  string|array  $value 			    Preselected value to show (for date type it must be in timestamp format, for amount or price it must be a php numeric value); for dates in filter mode, a range array('start'=><timestamp>, 'end'=><timestamp>) should be provided
	 * @param  string        $moreparam      		To add more parameters on html input tag
	 * @param  string        $keysuffix      		Prefix string to add after name and id of field (can be used to avoid duplicate names)
	 * @param  string        $keyprefix      		Suffix string to add before name and id of field (can be used to avoid duplicate names)
	 * @param  string        $morecss        		More css (to defined size of field. Old behaviour: may also be a numeric)
	 * @param  int           $objectid       		Current object id
	 * @param  string        $extrafieldsobjectkey	The key to use to store retreived data (commonly $object->table_element)
	 * @param  int	         $mode                  1=Used for search filters
	 * @return string
	 */
	public function showInputField($key, $value, $moreparam = '', $keysuffix = '', $keyprefix = '', $morecss = '', $objectid = 0, $extrafieldsobjectkey = '', $mode = 0)
	{
		global $conf, $langs, $form;

		if (!is_object($form)) {
			require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
			$form = new Form($this->db);
		}

		$out = '';

		if (!preg_match('/options_$/', $keyprefix)) {	// Because we work on extrafields, we add 'options_' to prefix if not already added
			$keyprefix = $keyprefix.'options_';
		}

		if (empty($extrafieldsobjectkey)) {
			dol_syslog(get_class($this).'::showInputField extrafieldsobjectkey required', LOG_ERR);
			return 'BadValueForParamExtraFieldsObjectKey';
		}

		$label = $this->attributes[$extrafieldsobjectkey]['label'][$key];
		$type = $this->attributes[$extrafieldsobjectkey]['type'][$key];
		$size = $this->attributes[$extrafieldsobjectkey]['size'][$key];
		$default = $this->attributes[$extrafieldsobjectkey]['default'][$key];
		$computed = $this->attributes[$extrafieldsobjectkey]['computed'][$key];
		$unique = $this->attributes[$extrafieldsobjectkey]['unique'][$key];
		$required = $this->attributes[$extrafieldsobjectkey]['required'][$key];
		$param = $this->attributes[$extrafieldsobjectkey]['param'][$key];
		$perms = dol_eval($this->attributes[$extrafieldsobjectkey]['perms'][$key], 1, 1, '2');
		$langfile = $this->attributes[$extrafieldsobjectkey]['langfile'][$key];
		$list = dol_eval($this->attributes[$extrafieldsobjectkey]['list'][$key], 1, 1, '2');
		$totalizable = $this->attributes[$extrafieldsobjectkey]['totalizable'][$key];
		$help = $this->attributes[$extrafieldsobjectkey]['help'][$key];
		$hidden = (empty($list) ? 1 : 0); // If empty, we are sure it is hidden, otherwise we show. If it depends on mode (view/create/edit form or list, this must be filtered by caller)

		if ($computed) {
			if (!preg_match('/^search_/', $keyprefix)) {
				return '<span class="opacitymedium">'.$langs->trans("AutomaticallyCalculated").'</span>';
			} else {
				return '';
			}
		}

		//
		// 'css' is used in creation and update. 'cssview' is used in view mode. 'csslist' is used for columns in lists. For example: 'css'=>'minwidth300 maxwidth500 widthcentpercentminusx', 'cssview'=>'wordbreak', 'csslist'=>'tdoverflowmax200'
		if (empty($morecss)) {
			// Add automatic css
			if ($type == 'date') {
				$morecss = 'minwidth100imp';
			} elseif ($type == 'datetime' || $type == 'datetimegmt' || $type == 'link') {
				$morecss = 'minwidth200imp';
			} elseif (in_array($type, array('int', 'integer', 'double', 'price'))) {
				$morecss = 'maxwidth75';
			} elseif ($type == 'password') {
				$morecss = 'maxwidth100';
			} elseif ($type == 'url') {
				$morecss = 'minwidth400';
			} elseif ($type == 'boolean') {
				$morecss = '';
			} elseif ($type == 'radio') {
				$morecss = 'width25';
			} else {
				if (empty($size) || round((float) $size) < 12) {
					$morecss = 'minwidth100';
				} elseif (round((float) $size) <= 48) {
					$morecss = 'minwidth200';
				} else {
					$morecss = 'minwidth400';
				}
			}
			// If css forced in attribute, we use this one
			if (!empty($this->attributes[$extrafieldsobjectkey]['css'][$key])) {
				$morecss = $this->attributes[$extrafieldsobjectkey]['css'][$key];
			}
		}

		if (in_array($type, array('date'))) {
			$tmp = explode(',', $size);
			$newsize = $tmp[0];
			$showtime = 0;

			// Do not show current date when field not required (see selectDate() method)
			if (!$required && $value == '') {
				$value = '-1';
			}

			if ($mode == 1) {
				// search filter on a date extrafield shows two inputs to select a date range
				$prefill = array(
					'start' => isset($value['start']) ? $value['start'] : '',
					'end'   => isset($value['end']) ? $value['end'] : ''
				);
				$out = '<div ' . ($moreparam ? $moreparam : '') . '><div class="nowrap">';
				$out .= $form->selectDate($prefill['start'], $keyprefix.$key.$keysuffix.'_start', 0, 0, 1, '', 1, 0, 0, '', '', '', '', 1, '', $langs->trans("From"));
				$out .= '</div><div class="nowrap">';
				$out .= $form->selectDate($prefill['end'], $keyprefix.$key.$keysuffix.'_end', 0, 0, 1, '', 1, 0, 0, '', '', '', '', 1, '', $langs->trans("to"));
				$out .= '</div></div>';
			} else {
				// TODO Must also support $moreparam
				$out = $form->selectDate($value, $keyprefix.$key.$keysuffix, $showtime, $showtime, $required, '', 1, (($keyprefix != 'search_' && $keyprefix != 'search_options_') ? 1 : 0), 0, 1);
			}
		} elseif (in_array($type, array('datetime', 'datetimegmt'))) {
			$tmp = explode(',', $size);
			$newsize = $tmp[0];
			$showtime = 1;

			// Do not show current date when field not required (see selectDate() method)
			if (!$required && $value == '') {
				$value = '-1';
			}

			if ($mode == 1) {
				// search filter on a date extrafield shows two inputs to select a date range
				$prefill = array(
					'start' => isset($value['start']) ? $value['start'] : '',
					'end'   => isset($value['end']) ? $value['end'] : ''
				);
				$out = '<div ' . ($moreparam ? $moreparam : '') . '><div class="nowrap">';
				$out .= $form->selectDate($prefill['start'], $keyprefix.$key.$keysuffix.'_start', 1, 1, 1, '', 1, 0, 0, '', '', '', '', 1, '', $langs->trans("From"), 'tzuserrel');
				$out .= '</div><div class="nowrap">';
				$out .= $form->selectDate($prefill['end'], $keyprefix.$key.$keysuffix.'_end', 1, 1, 1, '', 1, 0, 0, '', '', '', '', 1, '', $langs->trans("to"), 'tzuserrel');
				$out .= '</div></div>';
			} else {
				// TODO Must also support $moreparam
				$out = $form->selectDate($value, $keyprefix.$key.$keysuffix, $showtime, $showtime, $required, '', 1, (($keyprefix != 'search_' && $keyprefix != 'search_options_') ? 1 : 0), 0, 1, '', '', '', 1, '', '', 'tzuserrel');
			}
		} elseif (in_array($type, array('int', 'integer'))) {
			$tmp = explode(',', $size);
			$newsize = $tmp[0];
			$out = '<input type="text" class="flat '.$morecss.' maxwidthonsmartphone" name="'.$keyprefix.$key.$keysuffix.'" id="'.$keyprefix.$key.$keysuffix.'" maxlength="'.$newsize.'" value="'.dol_escape_htmltag($value).'"'.($moreparam ? $moreparam : '').'>';
		} elseif (preg_match('/varchar/', $type)) {
			$out = '<input type="text" class="flat '.$morecss.' maxwidthonsmartphone" name="'.$keyprefix.$key.$keysuffix.'" id="'.$keyprefix.$key.$keysuffix.'" maxlength="'.$size.'" value="'.dol_escape_htmltag($value).'"'.($moreparam ? $moreparam : '').'>';
		} elseif (in_array($type, array('mail', 'ip', 'phone', 'url'))) {
			$out = '<input type="text" class="flat '.$morecss.' maxwidthonsmartphone" name="'.$keyprefix.$key.$keysuffix.'" id="'.$keyprefix.$key.$keysuffix.'" value="'.dol_escape_htmltag($value).'" '.($moreparam ? $moreparam : '').'>';
		} elseif ($type == 'icon') {
			/* External lib inclusion are not allowed in backoffice. Also lib is included several time if there is several icon file.
			Some code must be added into main when MAIN_ADD_ICONPICKER_JS is set to add of lib in html header
			$out ='<link rel="stylesheet" href="'.dol_buildpath('/myfield/css/fontawesome-iconpicker.min.css', 1).'">';
			$out.='<script src="'.dol_buildpath('/myfield/js/fontawesome-iconpicker.min.js', 1).'"></script>';
			*/
			$out.= '<input type="text" class="form-control icp icp-auto iconpicker-element iconpicker-input flat '.$morecss.' maxwidthonsmartphone"';
			$out.= ' name="'.$keyprefix.$key.$keysuffix.'" id="'.$keyprefix.$key.$keysuffix.'" value="'.dol_escape_htmltag($value).'" '.($moreparam ? $moreparam : '').'>';
			if (getDolGlobalInt('MAIN_ADD_ICONPICKER_JS')) {
				$out.='<script>';
				$options="{ title: '<b>".$langs->trans("IconFieldSelector")."</b>', placement: 'right', showFooter: false, templates: {";
				$options.="iconpicker: '<div class=\"iconpicker\"><div style=\"background-color:#EFEFEF;\" class=\"iconpicker-items\"></div></div>',";
				$options.="iconpickerItem: '<a role=\"button\" href=\"#\" class=\"iconpicker-item\" style=\"background-color:#DDDDDD;\"><i></i></a>',";
				// $options.="buttons: '<button style=\"background-color:#FFFFFF;\" class=\"iconpicker-btn iconpicker-btn-cancel btn btn-default btn-sm\">".$langs->trans("Cancel")."</button>";
				// $options.="<button style=\"background-color:#FFFFFF;\" class=\"iconpicker-btn iconpicker-btn-accept btn btn-primary btn-sm\">".$langs->trans("Save")."</button>',";
				$options.="footer: '<div class=\"popover-footer\" style=\"background-color:#EFEFEF;\"></div>',";
				$options.="search: '<input type=\"search\" class\"form-control iconpicker-search\" placeholder=\"".$langs->trans("TypeToFilter")."\" />',";
				$options.="popover: '<div class=\"iconpicker-popover popover\">";
				$options.="   <div class=\"arrow\" ></div>";
				$options.="   <div class=\"popover-title\" style=\"text-align:center;background-color:#EFEFEF;\"></div>";
				$options.="   <div class=\"popover-content \" ></div>";
				$options.="</div>'}}";
				$out.="$('#".$keyprefix.$key.$keysuffix."').iconpicker(".$options.");";
				$out.='</script>';
			}
		} elseif ($type == 'text') {
			if (!preg_match('/search_/', $keyprefix)) {		// If keyprefix is search_ or search_options_, we must just use a simple text field
				require_once DOL_DOCUMENT_ROOT.'/core/class/doleditor.class.php';
				$doleditor = new DolEditor($keyprefix.$key.$keysuffix, $value, '', 200, 'dolibarr_notes', 'In', false, false, false, ROWS_5, '90%');
				$out = $doleditor->Create(1);
			} else {
				$out = '<input type="text" class="flat '.$morecss.' maxwidthonsmartphone" name="'.$keyprefix.$key.$keysuffix.'" id="'.$keyprefix.$key.$keysuffix.'" value="'.dol_escape_htmltag($value).'" '.($moreparam ? $moreparam : '').'>';
			}
		} elseif ($type == 'html') {
			if (!preg_match('/search_/', $keyprefix)) {		// If keyprefix is search_ or search_options_, we must just use a simple text field
				require_once DOL_DOCUMENT_ROOT.'/core/class/doleditor.class.php';
				$doleditor = new DolEditor($keyprefix.$key.$keysuffix, $value, '', 200, 'dolibarr_notes', 'In', false, false, isModEnabled('fckeditor') && $conf->global->FCKEDITOR_ENABLE_SOCIETE, ROWS_5, '90%');
				$out = $doleditor->Create(1);
			} else {
				$out = '<input type="text" class="flat '.$morecss.' maxwidthonsmartphone" name="'.$keyprefix.$key.$keysuffix.'" id="'.$keyprefix.$key.$keysuffix.'" value="'.dol_escape_htmltag($value).'" '.($moreparam ? $moreparam : '').'>';
			}
		} elseif ($type == 'boolean') {
			if (empty($mode)) {
				$checked = '';
				if (!empty($value)) {
					$checked = ' checked value="1" ';
				} else {
					$checked = ' value="1" ';
				}

				$out = '<input type="checkbox" class="flat valignmiddle'.($morecss ? ' '.$morecss : '').' maxwidthonsmartphone" name="'.$keyprefix.$key.$keysuffix.'" id="'.$keyprefix.$key.$keysuffix.'" '.$checked.' '.($moreparam ? $moreparam : '').'>';
			} else {
				$out = $form->selectyesno($keyprefix.$key.$keysuffix, $value, 1, false, 1);
			}
			$out .= '<input type="hidden" name="'.$keyprefix.$key.$keysuffix.'_boolean" value="1">';	// A hidden field ending with "_boolean" that is always set to 1.
		} elseif ($type == 'price') {
			if (!empty($value)) {		// $value in memory is a php numeric, we format it into user number format.
				$value = price($value);
			}
			$out = '<input type="text" class="flat '.$morecss.' maxwidthonsmartphone" name="'.$keyprefix.$key.$keysuffix.'" id="'.$keyprefix.$key.$keysuffix.'" value="'.$value.'" '.($moreparam ? $moreparam : '').'> '.$langs->getCurrencySymbol($conf->currency);
		} elseif ($type == 'pricecy') {
			$currency = $conf->currency;
			if (!empty($value)) {
				// $value in memory is a php string like '10.01:USD'
				$pricetmp = explode(':', $value);
				$currency = !empty($pricetmp[1]) ? $pricetmp[1] : $conf->currency;
				$value = price($pricetmp[0]);
			}
			$out = '<input type="text" class="flat '.$morecss.' maxwidthonsmartphone" name="'.$keyprefix.$key.$keysuffix.'" id="'.$keyprefix.$key.$keysuffix.'" value="'.$value.'" '.($moreparam ? $moreparam : '').'> ';
			$out .= $form->selectCurrency($currency, $keyprefix.$key.$keysuffix.'currency_id');
		} elseif ($type == 'double') {
			if (!empty($value)) {		// $value in memory is a php numeric, we format it into user number format.
				$value = price($value);
			}
			$out = '<input type="text" class="flat '.$morecss.' maxwidthonsmartphone" name="'.$keyprefix.$key.$keysuffix.'" id="'.$keyprefix.$key.$keysuffix.'" value="'.$value.'" '.($moreparam ? $moreparam : '').'> ';
		} elseif ($type == 'select') {
			$out = '';
			if ($mode) {
				$options = array();
				foreach ($param['options'] as $okey => $val) {
					if ((string) $okey == '') {
						continue;
					}

					$valarray = explode('|', $val);
					$val = $valarray[0];

					if ($langfile && $val) {
						$options[$okey] = $langs->trans($val);
					} else {
						$options[$okey] = $val;
					}
				}
				$selected = array();
				if (!is_array($value)) {
					$selected = explode(',', $value);
				}

				$out .= $form->multiselectarray($keyprefix.$key.$keysuffix, $options, $selected, 0, 0, $morecss, 0, 0, '', '', '', !empty($conf->use_javascript_ajax) && !getDolGlobalString('MAIN_EXTRAFIELDS_DISABLE_SELECT2'));
			} else {
				if (!empty($conf->use_javascript_ajax) && !getDolGlobalString('MAIN_EXTRAFIELDS_DISABLE_SELECT2')) {
					include_once DOL_DOCUMENT_ROOT.'/core/lib/ajax.lib.php';
					$out .= ajax_combobox($keyprefix.$key.$keysuffix, array(), 0);
				}

				$out .= '<select class="flat '.$morecss.' maxwidthonsmartphone" name="'.$keyprefix.$key.$keysuffix.'" id="'.$keyprefix.$key.$keysuffix.'" '.($moreparam ? $moreparam : '').'>';
				$out .= '<option value="0">&nbsp;</option>';
				foreach ($param['options'] as $key2 => $val2) {
					if ((string) $key2 == '') {
						continue;
					}
					$valarray = explode('|', $val2);
					$val2 = $valarray[0];
					$parent = '';
					if (!empty($valarray[1])) {
						$parent = $valarray[1];
					}
					$out .= '<option value="'.$key2.'"';
					$out .= (((string) $value == (string) $key2) ? ' selected' : '');
					$out .= (!empty($parent) ? ' parent="'.$parent.'"' : '');
					$out .= '>';
					if ($langfile && $val2) {
						$out .= $langs->trans($val2);
					} else {
						$out .= $val2;
					}
					$out .= '</option>';
				}
				$out .= '</select>';
			}
		} elseif ($type == 'sellist') {
			$out = '';
			if (!empty($conf->use_javascript_ajax) && !getDolGlobalString('MAIN_EXTRAFIELDS_DISABLE_SELECT2')) {
				include_once DOL_DOCUMENT_ROOT.'/core/lib/ajax.lib.php';
				$out .= ajax_combobox($keyprefix.$key.$keysuffix, array(), 0);
			}

			$out .= '<select class="flat '.$morecss.' maxwidthonsmartphone" name="'.$keyprefix.$key.$keysuffix.'" id="'.$keyprefix.$key.$keysuffix.'" '.($moreparam ? $moreparam : '').'>';
			if (is_array($param['options'])) {
				$param_list = array_keys($param['options']);
				$InfoFieldList = explode(":", $param_list[0]);
				$parentName = '';
				$parentField = '';
				// 0 : tableName
				// 1 : label field name
				// 2 : key fields name (if differ of rowid)
				// 3 : key field parent (for dependent lists)
				// 4 : where clause filter on column or table extrafield, syntax field='value' or extra.field=value
				// 5 : id category type
				// 6 : ids categories list separated by comma for category root
				// 7 : sort by (to be close to common object)
				$keyList = (empty($InfoFieldList[2]) ? 'rowid' : $InfoFieldList[2].' as rowid');


				if (count($InfoFieldList) > 4 && !empty($InfoFieldList[4])) {
					if (strpos($InfoFieldList[4], 'extra.') !== false) {
						$keyList = 'main.'.$InfoFieldList[2].' as rowid';
					} else {
						$keyList = $InfoFieldList[2].' as rowid';
					}
				}
				if (count($InfoFieldList) > 3 && !empty($InfoFieldList[3])) {
					list($parentName, $parentField) = explode('|', $InfoFieldList[3]);
					$keyList .= ', '.$parentField;
				}

				$filter_categorie = false;
				if (count($InfoFieldList) > 5) {
					if ($InfoFieldList[0] == 'categorie') {
						$filter_categorie = true;
					}
				}

				if ($filter_categorie === false) {
					$fields_label = explode('|', $InfoFieldList[1]);
					if (is_array($fields_label)) {
						$keyList .= ', ';
						$keyList .= implode(', ', $fields_label);
					}

					$sqlwhere = '';
					$sql = "SELECT ".$keyList;
					$sql .= ' FROM '.$this->db->prefix().$InfoFieldList[0];
					if (!empty($InfoFieldList[4])) {
						// can use current entity filter
						if (strpos($InfoFieldList[4], '$ENTITY$') !== false) {
							$InfoFieldList[4] = str_replace('$ENTITY$', $conf->entity, $InfoFieldList[4]);
						}
						// can use SELECT request
						if (strpos($InfoFieldList[4], '$SEL$') !== false) {
							$InfoFieldList[4] = str_replace('$SEL$', 'SELECT', $InfoFieldList[4]);
						}

						// current object id can be use into filter
						if (strpos($InfoFieldList[4], '$ID$') !== false && !empty($objectid)) {
							$InfoFieldList[4] = str_replace('$ID$', $objectid, $InfoFieldList[4]);
						} else {
							$InfoFieldList[4] = str_replace('$ID$', '0', $InfoFieldList[4]);
						}
						//We have to join on extrafield table
						if (strpos($InfoFieldList[4], 'extra.') !== false) {
							$sql .= ' as main, '.$this->db->prefix().$InfoFieldList[0].'_extrafields as extra';
							$sqlwhere .= " WHERE extra.fk_object=main.".$InfoFieldList[2]." AND ".$InfoFieldList[4];
						} else {
							$sqlwhere .= " WHERE ".$InfoFieldList[4];
						}
					} else {
						$sqlwhere .= ' WHERE 1=1';
					}
					// Some tables may have field, some other not. For the moment we disable it.
					if (in_array($InfoFieldList[0], array('tablewithentity'))) {
						$sqlwhere .= ' AND entity = '.((int) $conf->entity);
					}
					$sql .= $sqlwhere;
					//print $sql;

					$sql .= ' ORDER BY '.implode(', ', $fields_label);

					dol_syslog(get_class($this).'::showInputField type=sellist', LOG_DEBUG);
					$resql = $this->db->query($sql);
					if ($resql) {
						$out .= '<option value="0">&nbsp;</option>';
						$num = $this->db->num_rows($resql);
						$i = 0;
						while ($i < $num) {
							$labeltoshow = '';
							$obj = $this->db->fetch_object($resql);

							// Several field into label (eq table:code|label:rowid)
							$notrans = false;
							$fields_label = explode('|', $InfoFieldList[1]);
							if (is_array($fields_label) && count($fields_label) > 1) {
								$notrans = true;
								foreach ($fields_label as $field_toshow) {
									$labeltoshow .= $obj->$field_toshow.' ';
								}
							} else {
								$labeltoshow = $obj->{$InfoFieldList[1]};
							}
							$labeltoshow = $labeltoshow;

							if ($value == $obj->rowid) {
								if (!$notrans) {
									foreach ($fields_label as $field_toshow) {
										$translabel = $langs->trans($obj->$field_toshow);
										$labeltoshow = $translabel.' ';
									}
								}
								$out .= '<option value="'.$obj->rowid.'" selected>'.$labeltoshow.'</option>';
							} else {
								if (!$notrans) {
									$translabel = $langs->trans($obj->{$InfoFieldList[1]});
									$labeltoshow = $translabel;
								}
								if (empty($labeltoshow)) {
									$labeltoshow = '(not defined)';
								}

								if (!empty($InfoFieldList[3]) && $parentField) {
									$parent = $parentName.':'.$obj->{$parentField};
								}

								$out .= '<option value="'.$obj->rowid.'"';
								$out .= ($value == $obj->rowid ? ' selected' : '');
								$out .= (!empty($parent) ? ' parent="'.$parent.'"' : '');
								$out .= '>'.$labeltoshow.'</option>';
							}

							$i++;
						}
						$this->db->free($resql);
					} else {
						print 'Error in request '.$sql.' '.$this->db->lasterror().'. Check setup of extra parameters.<br>';
					}
				} else {
					require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php';
					$data = $form->select_all_categories(Categorie::$MAP_ID_TO_CODE[$InfoFieldList[5]], '', 'parent', 64, $InfoFieldList[6], 1, 1);
					$out .= '<option value="0">&nbsp;</option>';
					if (is_array($data)) {
						foreach ($data as $data_key => $data_value) {
							$out .= '<option value="'.$data_key.'"';
							$out .= ($value == $data_key ? ' selected' : '');
							$out .= '>'.$data_value.'</option>';
						}
					}
				}
			}
			$out .= '</select>';
		} elseif ($type == 'checkbox') {
			$value_arr = $value;
			if (!is_array($value)) {
				$value_arr = explode(',', $value);
			}
			$out = $form->multiselectarray($keyprefix.$key.$keysuffix, (empty($param['options']) ? null : $param['options']), $value_arr, '', 0, '', 0, '100%');
		} elseif ($type == 'radio') {
			$out = '';
			foreach ($param['options'] as $keyopt => $val) {
				$out .= '<input class="flat '.$morecss.'" type="radio" name="'.$keyprefix.$key.$keysuffix.'" id="'.$keyprefix.$key.$keysuffix.'" '.($moreparam ? $moreparam : '');
				$out .= ' value="'.$keyopt.'"';
				$out .= ' id="'.$keyprefix.$key.$keysuffix.'_'.$keyopt.'"';
				$out .= ($value == $keyopt ? 'checked' : '');
				$out .= '/><label for="'.$keyprefix.$key.$keysuffix.'_'.$keyopt.'">'.$langs->trans($val).'</label><br>';
			}
		} elseif ($type == 'chkbxlst') {
			if (is_array($value)) {
				$value_arr = $value;
			} else {
				$value_arr = explode(',', $value);
			}

			if (is_array($param['options'])) {
				$param_list = array_keys($param['options']);
				$InfoFieldList = explode(":", $param_list[0]);
				$parentName = '';
				$parentField = '';
				// 0 : tableName
				// 1 : label field name
				// 2 : key fields name (if differ of rowid)
				// 3 : key field parent (for dependent lists)
				// 4 : where clause filter on column or table extrafield, syntax field='value' or extra.field=value
				// 5 : id category type
				// 6 : ids categories list separated by comma for category root
				// 7 : sort by (to be close to common object)
				$keyList = (empty($InfoFieldList[2]) ? 'rowid' : $InfoFieldList[2].' as rowid');

				if (count($InfoFieldList) > 3 && !empty($InfoFieldList[3])) {
					list($parentName, $parentField) = explode('|', $InfoFieldList[3]);
					$keyList .= ', '.$parentField;
				}
				if (count($InfoFieldList) > 4 && !empty($InfoFieldList[4])) {
					if (strpos($InfoFieldList[4], 'extra.') !== false) {
						$keyList = 'main.'.$InfoFieldList[2].' as rowid';
					} else {
						$keyList = $InfoFieldList[2].' as rowid';
					}
				}

				$filter_categorie = false;
				if (count($InfoFieldList) > 5) {
					if ($InfoFieldList[0] == 'categorie') {
						$filter_categorie = true;
					}
				}

				if ($filter_categorie === false) {
					$fields_label = explode('|', $InfoFieldList[1]);
					if (is_array($fields_label)) {
						$keyList .= ', ';
						$keyList .= implode(', ', $fields_label);
					}

					$sqlwhere = '';
					$sql = "SELECT ".$keyList;
					$sql .= ' FROM '.$this->db->prefix().$InfoFieldList[0];
					if (!empty($InfoFieldList[4])) {
						// can use current entity filter
						if (strpos($InfoFieldList[4], '$ENTITY$') !== false) {
							$InfoFieldList[4] = str_replace('$ENTITY$', $conf->entity, $InfoFieldList[4]);
						}
						// can use SELECT request
						if (strpos($InfoFieldList[4], '$SEL$') !== false) {
							$InfoFieldList[4] = str_replace('$SEL$', 'SELECT', $InfoFieldList[4]);
						}

						// current object id can be use into filter
						if (strpos($InfoFieldList[4], '$ID$') !== false && !empty($objectid)) {
							$InfoFieldList[4] = str_replace('$ID$', $objectid, $InfoFieldList[4]);
						} elseif (preg_match("#^.*list.php$#", $_SERVER["PHP_SELF"])) {
							// Pattern for word=$ID$
							$word = '\b[a-zA-Z0-9-\.-_]+\b=\$ID\$';

							// Removing space arount =, ( and )
							$InfoFieldList[4] = preg_replace('# *(=|\(|\)) *#', '$1', $InfoFieldList[4]);

							$nbPreg = 1;
							// While we have parenthesis
							while ($nbPreg != 0) {
								// Init des compteurs
								$nbPregRepl = $nbPregSel = 0;
								// On retire toutes les parenthèses sans = avant
								$InfoFieldList[4] = preg_replace('#([^=])(\([^)^(]*('.$word.')[^)^(]*\))#', '$1 $3 ', $InfoFieldList[4], -1, $nbPregRepl);
								// On retire les espaces autour des = et parenthèses
								$InfoFieldList[4] = preg_replace('# *(=|\(|\)) *#', '$1', $InfoFieldList[4]);
								// On retire toutes les parenthèses avec = avant
								$InfoFieldList[4] = preg_replace('#\b[a-zA-Z0-9-\.-_]+\b=\([^)^(]*('.$word.')[^)^(]*\)#', '$1 ', $InfoFieldList[4], -1, $nbPregSel);
								// On retire les espaces autour des = et parenthèses
								$InfoFieldList[4] = preg_replace('# *(=|\(|\)) *#', '$1', $InfoFieldList[4]);

								// Calcul du compteur général pour la boucle
								$nbPreg = $nbPregRepl + $nbPregSel;
							}

							// Si l'on a un AND ou un OR, avant ou après
							$matchCondition = array();
							preg_match('#(AND|OR|) *('.$word.') *(AND|OR|)#', $InfoFieldList[4], $matchCondition);
							while (!empty($matchCondition[0])) {
								// If the two sides differ but are not empty
								if (!empty($matchCondition[1]) && !empty($matchCondition[3]) && $matchCondition[1] != $matchCondition[3]) {
									// Nobody sain would do that without parentheses
									$InfoFieldList[4] = str_replace('$ID$', '0', $InfoFieldList[4]);
								} else {
									if (!empty($matchCondition[1])) {
										$boolCond = (($matchCondition[1] == "AND") ? ' AND TRUE ' : ' OR FALSE ');
										$InfoFieldList[4] = str_replace($matchCondition[0], $boolCond.$matchCondition[3], $InfoFieldList[4]);
									} elseif (!empty($matchCondition[3])) {
										$boolCond = (($matchCondition[3] == "AND") ? ' TRUE AND ' : ' FALSE OR');
										$InfoFieldList[4] = str_replace($matchCondition[0], $boolCond, $InfoFieldList[4]);
									} else {
										$InfoFieldList[4] = " TRUE ";
									}
								}

								// Si l'on a un AND ou un OR, avant ou après
								preg_match('#(AND|OR|) *('.$word.') *(AND|OR|)#', $InfoFieldList[4], $matchCondition);
							}
						} else {
							$InfoFieldList[4] = str_replace('$ID$', '0', $InfoFieldList[4]);
						}

						// We have to join on extrafield table
						if (strpos($InfoFieldList[4], 'extra.') !== false) {
							$sql .= ' as main, '.$this->db->prefix().$InfoFieldList[0].'_extrafields as extra';
							$sqlwhere .= " WHERE extra.fk_object=main.".$InfoFieldList[2]." AND ".$InfoFieldList[4];
						} else {
							$sqlwhere .= " WHERE ".$InfoFieldList[4];
						}
					} else {
						$sqlwhere .= ' WHERE 1=1';
					}
					// Some tables may have field, some other not. For the moment we disable it.
					if (in_array($InfoFieldList[0], array('tablewithentity'))) {
						$sqlwhere .= " AND entity = ".((int) $conf->entity);
					}
					// $sql.=preg_replace('/^ AND /','',$sqlwhere);
					// print $sql;

					$sql .= $sqlwhere;
					dol_syslog(get_class($this).'::showInputField type=chkbxlst', LOG_DEBUG);
					$resql = $this->db->query($sql);
					if ($resql) {
						$num = $this->db->num_rows($resql);
						$i = 0;

						$data = array();

						while ($i < $num) {
							$labeltoshow = '';
							$obj = $this->db->fetch_object($resql);

							$notrans = false;
							// Several field into label (eq table:code|label:rowid)
							$fields_label = explode('|', $InfoFieldList[1]);
							if (is_array($fields_label)) {
								$notrans = true;
								foreach ($fields_label as $field_toshow) {
									$labeltoshow .= $obj->$field_toshow.' ';
								}
							} else {
								$labeltoshow = $obj->{$InfoFieldList[1]};
							}
							$labeltoshow = dol_trunc($labeltoshow, 45);

							if (is_array($value_arr) && in_array($obj->rowid, $value_arr)) {
								$labeltoshow = '';
								foreach ($fields_label as $field_toshow) {
									$translabel = $langs->trans($obj->$field_toshow);
									if ($translabel != $obj->$field_toshow) {
										$labeltoshow .= ' '.dol_trunc($translabel, 18).' ';
									} else {
										$labeltoshow .= ' '.dol_trunc($obj->$field_toshow, 18).' ';
									}
								}
								$data[$obj->rowid] = $labeltoshow;
							} else {
								if (!$notrans) {
									$translabel = $langs->trans($obj->{$InfoFieldList[1]});
									if ($translabel != $obj->{$InfoFieldList[1]}) {
										$labeltoshow = dol_trunc($translabel, 18);
									} else {
										$labeltoshow = dol_trunc($obj->{$InfoFieldList[1]}, 18);
									}
								}
								if (empty($labeltoshow)) {
									$labeltoshow = '(not defined)';
								}

								if (is_array($value_arr) && in_array($obj->rowid, $value_arr)) {
									$data[$obj->rowid] = $labeltoshow;
								}

								if (!empty($InfoFieldList[3]) && $parentField) {
									$parent = $parentName.':'.$obj->{$parentField};
								}

								$data[$obj->rowid] = $labeltoshow;
							}

							$i++;
						}
						$this->db->free($resql);

						$out = $form->multiselectarray($keyprefix.$key.$keysuffix, $data, $value_arr, '', 0, '', 0, '100%');
					} else {
						print 'Error in request '.$sql.' '.$this->db->lasterror().'. Check setup of extra parameters.<br>';
					}
				} else {
					require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php';
					$data = $form->select_all_categories(Categorie::$MAP_ID_TO_CODE[$InfoFieldList[5]], '', 'parent', 64, $InfoFieldList[6], 1, 1);
					$out = $form->multiselectarray($keyprefix.$key.$keysuffix, $data, $value_arr, '', 0, '', 0, '100%');
				}
			}
		} elseif ($type == 'link') {
			$param_list = array_keys($param['options']); // $param_list='ObjectName:classPath'
			/* Removed.
			The selectForForms is called with parameter $objectfield defined, so tha app can retreive the filter inside the ajax component instead of being provided as parameters. The
			filter was use to pass SQL requests leading to serious SQL injection problem. This should not be possible. Also the call of the ajax was broken by some WAF.
			if (strpos($param_list[0], '$ID$') !== false && !empty($objectid)) {
				$param_list[0] = str_replace('$ID$', $objectid, $param_list[0]);
			}*/
			$showempty = (($required && $default != '') ? 0 : 1);

			$tmparray = explode(':', $param_list[0]);

			$element = $extrafieldsobjectkey;		// $extrafieldsobjectkey comes from $object->table_element but we need $object->element
			if ($element == 'socpeople') {
				$element = 'contact';
			} elseif ( $element == 'projet' ) {
				$element = 'project';
			}

			//$out = $form->selectForForms($param_list[0], $keyprefix.$key.$keysuffix, $value, $showempty, '', '', $morecss, '', 0, 0, '');
			$out = $form->selectForForms($tmparray[0], $keyprefix.$key.$keysuffix, $value, $showempty, '', '', $morecss, '', 0, 0, '', $element.':options_'.$key);
		} elseif ($type == 'password') {
			// If prefix is 'search_', field is used as a filter, we use a common text field.
			$out = '<input style="display:none" type="text" name="fakeusernameremembered">'; // Hidden field to reduce impact of evil Google Chrome autopopulate bug.
			$out .= '<input autocomplete="new-password" type="'.($keyprefix == 'search_' ? 'text' : 'password').'" class="flat '.$morecss.'" name="'.$keyprefix.$key.$keysuffix.'" id="'.$keyprefix.$key.$keysuffix.'" value="'.$value.'" '.($moreparam ? $moreparam : '').'>';
		}
		if (!empty($hidden)) {
			$out = '<input type="hidden" value="'.$value.'" name="'.$keyprefix.$key.$keysuffix.'" id="'.$keyprefix.$key.$keysuffix.'"/>';
		}
		/* Add comments
		 if ($type == 'date') $out.=' (YYYY-MM-DD)';
		 elseif ($type == 'datetime') $out.=' (YYYY-MM-DD HH:MM:SS)';
		 */
		/*if (!empty($help) && $keyprefix != 'search_options_') {
			$out .= $form->textwithpicto('', $help, 1, 'help', '', 0, 3);
		}*/
		return $out;
	}


	/**
	 * Return HTML string to put an output field into a page
	 *
	 * @param   string	$key            		Key of attribute
	 * @param   string	$value          		Value to show
	 * @param	string	$moreparam				To add more parameters on html input tag (only checkbox use html input for output rendering)
	 * @param	string	$extrafieldsobjectkey	Required (for example $object->table_element).
	 * @return	string							Formated value
	 */
	public function showOutputField($key, $value, $moreparam = '', $extrafieldsobjectkey = '')
	{
		global $conf, $langs;

		if (empty($extrafieldsobjectkey)) {
			dol_syslog(get_class($this).'::showOutputField extrafieldsobjectkey required', LOG_ERR);
			return 'BadValueForParamExtraFieldsObjectKey';
		}

		$label = $this->attributes[$extrafieldsobjectkey]['label'][$key];
		$type = $this->attributes[$extrafieldsobjectkey]['type'][$key];
		$size = $this->attributes[$extrafieldsobjectkey]['size'][$key];			// Can be '255', '24,8'...
		$default = $this->attributes[$extrafieldsobjectkey]['default'][$key];
		$computed = $this->attributes[$extrafieldsobjectkey]['computed'][$key];
		$unique = $this->attributes[$extrafieldsobjectkey]['unique'][$key];
		$required = $this->attributes[$extrafieldsobjectkey]['required'][$key];
		$param = $this->attributes[$extrafieldsobjectkey]['param'][$key];
		$perms = dol_eval($this->attributes[$extrafieldsobjectkey]['perms'][$key], 1, 1, '2');
		$langfile = $this->attributes[$extrafieldsobjectkey]['langfile'][$key];
		$list = dol_eval($this->attributes[$extrafieldsobjectkey]['list'][$key], 1, 1, '2');
		$help = $this->attributes[$extrafieldsobjectkey]['help'][$key];
		$hidden = (empty($list) ? 1 : 0); // If $list empty, we are sure it is hidden, otherwise we show. If it depends on mode (view/create/edit form or list, this must be filtered by caller)

		if ($hidden) {
			return ''; // This is a protection. If field is hidden, we should just not call this method.
		}

		//if ($computed) $value =		// $value is already calculated into $value before calling this method

		$showsize = 0;
		if ($type == 'date') {
			$showsize = 10;
			if ($value !== '') {
				$value = dol_print_date($value, 'day');	// For date without hour, date is always GMT for storage and output
			}
		} elseif ($type == 'datetime') {
			$showsize = 19;
			if ($value !== '') {
				$value = dol_print_date($value, 'dayhour', 'tzuserrel');
			}
		} elseif ($type == 'datetimegmt') {
			$showsize = 19;
			if ($value !== '') {
				$value = dol_print_date($value, 'dayhour', 'gmt');
			}
		} elseif ($type == 'int') {
			$showsize = 10;
		} elseif ($type == 'double') {
			if (!empty($value)) {
				//$value=price($value);
				$sizeparts = explode(",", $size);
				$number_decimals = array_key_exists(1, $sizeparts) ? $sizeparts[1] : 0;
				$value = price($value, 0, $langs, 0, 0, $number_decimals, '');
			}
		} elseif ($type == 'boolean') {
			$checked = '';
			if (!empty($value)) {
				$checked = ' checked ';
			}
			$value = '<input type="checkbox" '.$checked.' '.($moreparam ? $moreparam : '').' readonly disabled>';
		} elseif ($type == 'mail') {
			$value = dol_print_email($value, 0, 0, 0, 64, 1, 1);
		} elseif ($type == 'ip') {
			$value = dol_print_ip($value, 0);
		} elseif ($type == 'icon') {
			$value = '<span class="'.$value.'"></span>';
		} elseif ($type == 'url') {
			$value = dol_print_url($value, '_blank', 32, 1);
		} elseif ($type == 'phone') {
			$value = dol_print_phone($value, '', 0, 0, '', '&nbsp;', 'phone');
		} elseif ($type == 'price') {
			//$value = price($value, 0, $langs, 0, 0, -1, $conf->currency);
			if ($value || $value == '0') {
				$value = price($value, 0, $langs, 0, $conf->global->MAIN_MAX_DECIMALS_TOT, -1).' '.$langs->getCurrencySymbol($conf->currency);
			}
		} elseif ($type == 'pricecy') {
			$currency = $conf->currency;
			if (!empty($value)) {
				// $value in memory is a php string like '0.01:EUR'
				$pricetmp = explode(':', $value);
				$currency = !empty($pricetmp[1]) ? $pricetmp[1] : $conf->currency;
				$value = $pricetmp[0];
			}
			if ($value || $value == '0') {
				$value = price($value, 0, $langs, 0, $conf->global->MAIN_MAX_DECIMALS_TOT, -1, $currency);
			}
		} elseif ($type == 'select') {
			$valstr = (!empty($param['options'][$value]) ? $param['options'][$value] : '');
			if (($pos = strpos($valstr, "|")) !== false) {
				$valstr = substr($valstr, 0, $pos);
			}
			if ($langfile && $valstr) {
				$value = $langs->trans($valstr);
			} else {
				$value = $valstr;
			}
		} elseif ($type == 'sellist') {
			$param_list = array_keys($param['options']);
			$InfoFieldList = explode(":", $param_list[0]);

			$selectkey = "rowid";
			$keyList = 'rowid';

			if (count($InfoFieldList) >= 3) {
				$selectkey = $InfoFieldList[2];
				$keyList = $InfoFieldList[2].' as rowid';
			}

			$fields_label = explode('|', $InfoFieldList[1]);
			if (is_array($fields_label)) {
				$keyList .= ', ';
				$keyList .= implode(', ', $fields_label);
			}

			$filter_categorie = false;
			if (count($InfoFieldList) > 5) {
				if ($InfoFieldList[0] == 'categorie') {
					$filter_categorie = true;
				}
			}

			$sql = "SELECT ".$keyList;
			$sql .= ' FROM '.$this->db->prefix().$InfoFieldList[0];
			if (!empty($InfoFieldList[4]) && strpos($InfoFieldList[4], 'extra.') !== false) {
				$sql .= ' as main';
			}
			if ($selectkey == 'rowid' && empty($value)) {
				$sql .= " WHERE ".$selectkey." = 0";
			} elseif ($selectkey == 'rowid') {
				$sql .= " WHERE ".$selectkey." = ".((int) $value);
			} else {
				$sql .= " WHERE ".$selectkey." = '".$this->db->escape($value)."'";
			}

			//$sql.= ' AND entity = '.$conf->entity;

			dol_syslog(get_class($this).':showOutputField:$type=sellist', LOG_DEBUG);
			$resql = $this->db->query($sql);
			if ($resql) {
				if ($filter_categorie === false) {
					$value = ''; // value was used, so now we reste it to use it to build final output

					$obj = $this->db->fetch_object($resql);

					// Several field into label (eq table:code|label:rowid)
					$fields_label = explode('|', $InfoFieldList[1]);

					if (is_array($fields_label) && count($fields_label) > 1) {
						foreach ($fields_label as $field_toshow) {
							$translabel = '';
							if (!empty($obj->$field_toshow)) {
								$translabel = $langs->trans($obj->$field_toshow);

								if ($translabel != $obj->$field_toshow) {
									$value .= dol_trunc($translabel, 24).' ';
								} else {
									$value .= $obj->$field_toshow.' ';
								}
							}
						}
					} else {
						$translabel = '';
						$tmppropname = $InfoFieldList[1];
						//$obj->$tmppropname = '';
						if (!empty(isset($obj->$tmppropname) ? $obj->$tmppropname : '')) {
							$translabel = $langs->trans($obj->$tmppropname);
						}
						if ($translabel != (isset($obj->$tmppropname) ? $obj->$tmppropname : '')) {
							$value = dol_trunc($translabel, 18);
						} else {
							$value = isset($obj->$tmppropname) ? $obj->$tmppropname : '';
						}
					}
				} else {
					$toprint = array();
					$obj = $this->db->fetch_object($resql);
					if ($obj->rowid) {
						require_once DOL_DOCUMENT_ROOT . '/categories/class/categorie.class.php';
						$c = new Categorie($this->db);
						$result = $c->fetch($obj->rowid);
						if ($result > 0) {
							$ways = $c->print_all_ways(); // $ways[0] = "ccc2 >> ccc2a >> ccc2a1" with html formatted text
							foreach ($ways as $way) {
								$toprint[] = '<li class="select2-search-choice-dolibarr noborderoncategories"' . ($c->color ? ' style="background: #' . $c->color . ';"' : ' style="background: #bbb"') . '>' . img_object('', 'category') . ' ' . $way . '</li>';
							}
						}
					}
					$value = '<div class="select2-container-multi-dolibarr" style="width: 90%;"><ul class="select2-choices-dolibarr">'.implode(' ', $toprint).'</ul></div>';
				}
			} else {
				dol_syslog(get_class($this).'::showOutputField error '.$this->db->lasterror(), LOG_WARNING);
			}
		} elseif ($type == 'radio') {
			if (!isset($param['options'][$value])) {
				$langs->load('errors');
				$value = $langs->trans('ErrorNoValueForRadioType');
			} else {
				$value = $langs->trans($param['options'][$value]);
			}
		} elseif ($type == 'checkbox') {
			$value_arr = explode(',', $value);
			$value = '';
			$toprint = array();
			if (is_array($value_arr)) {
				foreach ($value_arr as $keyval => $valueval) {
					if (!empty($valueval)) {
						$toprint[] = '<li class="select2-search-choice-dolibarr noborderoncategories" style="background: #bbb">'.$param['options'][$valueval].'</li>';
					}
				}
			}
			$value = '<div class="select2-container-multi-dolibarr" style="width: 90%;"><ul class="select2-choices-dolibarr">'.implode(' ', $toprint).'</ul></div>';
		} elseif ($type == 'chkbxlst') {
			$value_arr = explode(',', $value);

			$param_list = array_keys($param['options']);
			$InfoFieldList = explode(":", $param_list[0]);

			$selectkey = "rowid";
			$keyList = 'rowid';

			if (count($InfoFieldList) >= 3) {
				$selectkey = $InfoFieldList[2];
				$keyList = $InfoFieldList[2].' as rowid';
			}

			$fields_label = explode('|', $InfoFieldList[1]);
			if (is_array($fields_label)) {
				$keyList .= ', ';
				$keyList .= implode(', ', $fields_label);
			}

			$filter_categorie = false;
			if (count($InfoFieldList) > 5) {
				if ($InfoFieldList[0] == 'categorie') {
					$filter_categorie = true;
				}
			}

			$sql = "SELECT ".$keyList;
			$sql .= " FROM ".$this->db->prefix().$InfoFieldList[0];
			if (strpos($InfoFieldList[4], 'extra.') !== false) {
				$sql .= ' as main';
			}
			// $sql.= " WHERE ".$selectkey."='".$this->db->escape($value)."'";
			// $sql.= ' AND entity = '.$conf->entity;

			dol_syslog(get_class($this).':showOutputField:$type=chkbxlst', LOG_DEBUG);
			$resql = $this->db->query($sql);
			if ($resql) {
				if ($filter_categorie === false) {
					$value = ''; // value was used, so now we reste it to use it to build final output
					$toprint = array();
					while ($obj = $this->db->fetch_object($resql)) {
						// Several field into label (eq table:code|label:rowid)
						$fields_label = explode('|', $InfoFieldList[1]);
						if (is_array($value_arr) && in_array($obj->rowid, $value_arr)) {
							if (is_array($fields_label) && count($fields_label) > 1) {
								$label = '<li class="select2-search-choice-dolibarr noborderoncategories" style="background: #bbb">';
								foreach ($fields_label as $field_toshow) {
									$translabel = '';
									if (!empty($obj->$field_toshow)) {
										$translabel = $langs->trans($obj->$field_toshow);
									}
									if ($translabel != $field_toshow) {
										$label .= ' '.dol_trunc($translabel, 18);
									} else {
										$label .= ' '.$obj->$field_toshow;
									}
								}
								$label .= '</li>';
								$toprint[] = $label;
							} else {
								$translabel = '';
								if (!empty($obj->{$InfoFieldList[1]})) {
									$translabel = $langs->trans($obj->{$InfoFieldList[1]});
								}
								if ($translabel != $obj->{$InfoFieldList[1]}) {
									$toprint[] = '<li class="select2-search-choice-dolibarr noborderoncategories" style="background: #bbb">'.dol_trunc($translabel, 18).'</li>';
								} else {
									$toprint[] = '<li class="select2-search-choice-dolibarr noborderoncategories" style="background: #bbb">'.$obj->{$InfoFieldList[1]}.'</li>';
								}
							}
						}
					}
				} else {
					require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php';

					$toprint = array();
					while ($obj = $this->db->fetch_object($resql)) {
						if (is_array($value_arr) && in_array($obj->rowid, $value_arr)) {
							$c = new Categorie($this->db);
							$c->fetch($obj->rowid);
							$ways = $c->print_all_ways(); // $ways[0] = "ccc2 >> ccc2a >> ccc2a1" with html formatted text
							foreach ($ways as $way) {
								$toprint[] = '<li class="select2-search-choice-dolibarr noborderoncategories"'.($c->color ? ' style="background: #'.$c->color.';"' : ' style="background: #bbb"').'>'.img_object('', 'category').' '.$way.'</li>';
							}
						}
					}
				}
				if (!empty($toprint)) {
					$value = '<div class="select2-container-multi-dolibarr" style="width: 90%;"><ul class="select2-choices-dolibarr">'.implode(' ', $toprint).'</ul></div>';
				}
			} else {
				dol_syslog(get_class($this).'::showOutputField error '.$this->db->lasterror(), LOG_WARNING);
			}
		} elseif ($type == 'link') {
			$out = '';

			// Only if something to display (perf)
			if ($value) {		// If we have -1 here, pb is into insert, not into ouptut (fix insert instead of changing code here to compensate)
				$param_list = array_keys($param['options']); // $param_list='ObjectName:classPath'

				$InfoFieldList = explode(":", $param_list[0]);
				$classname = $InfoFieldList[0];
				$classpath = $InfoFieldList[1];
				if (!empty($classpath)) {
					dol_include_once($InfoFieldList[1]);
					if ($classname && class_exists($classname)) {
						$object = new $classname($this->db);
						$object->fetch($value);
						$value = $object->getNomUrl(3);
					}
				} else {
					dol_syslog('Error bad setup of extrafield', LOG_WARNING);
					return 'Error bad setup of extrafield';
				}
			}
		} elseif ($type == 'text') {
			$value = dol_htmlentitiesbr($value);
		} elseif ($type == 'html') {
			$value = dol_htmlentitiesbr($value);
		} elseif ($type == 'password') {
			$value = dol_trunc(preg_replace('/./i', '*', $value), 8, 'right', 'UTF-8', 1);
		} else {
			$showsize = round((float) $size);
			if ($showsize > 48) {
				$showsize = 48;
			}
		}

		//print $type.'-'.$size;
		$out = $value;

		return $out;
	}

	/**
	 * Return the CSS to use for this extrafield into list
	 *
	 * @param   string	$key            		Key of attribute
	 * @param	string	$extrafieldsobjectkey	If defined, use the new method to get extrafields data
	 * @return	string							Formated value
	 */
	public function getAlignFlag($key, $extrafieldsobjectkey = '')
	{
		global $conf, $langs;

		$type = 'varchar';
		if (!empty($extrafieldsobjectkey)) {
			$type = $this->attributes[$extrafieldsobjectkey]['type'][$key];
		}

		$cssstring = '';

		if (in_array($type, array('date', 'datetime', 'datetimegmt',))) {
			$cssstring = "center";
		} elseif (in_array($type, array('int', 'price', 'double'))) {
			$cssstring = "right";
		} elseif (in_array($type, array('boolean', 'radio', 'checkbox', 'ip', 'icon'))) {
			$cssstring = "center";
		}

		if (!empty($this->attributes[$extrafieldsobjectkey]['csslist'][$key])) {
			$cssstring .= ($cssstring ? ' ' : '').$this->attributes[$extrafieldsobjectkey]['csslist'][$key];
		} else {
			if (in_array($type, array('ip'))) {
				$cssstring .= ($cssstring ? ' ' : '').'tdoverflowmax150';
			}
		}

		return $cssstring;
	}

	/**
	 * Return HTML string to print separator extrafield
	 *
	 * @param   string	$key            Key of attribute
	 * @param	object	$object			Object
	 * @param	int		$colspan		Value of colspan to use (it must includes the first column with title)
	 * @param	string	$display_type	"card" for form display, "line" for document line display (extrafields on propal line, order line, etc...)
	 * @param 	string  $mode           Show output ('view') or input ('create' or 'edit') for extrafield
	 * @return 	string					HTML code with line for separator
	 */
	public function showSeparator($key, $object, $colspan = 2, $display_type = 'card', $mode = '')
	{
		global $conf, $langs;

		$tagtype='tr';
		$tagtype_dyn='td';

		if ($display_type=='line') {
			$tagtype='div';
			$tagtype_dyn='span';
			$colspan=0;
		}

		$extrafield_param = $this->attributes[$object->table_element]['param'][$key];
		$extrafield_param_list = array();
		if (!empty($extrafield_param) && is_array($extrafield_param)) {
			$extrafield_param_list = array_keys($extrafield_param['options']);
		}

		// Set $extrafield_collapse_display_value (do we have to collapse/expand the group after the separator)
		$extrafield_collapse_display_value = -1;
		$expand_display = false;
		if (is_array($extrafield_param_list) && count($extrafield_param_list) > 0) {
			$extrafield_collapse_display_value = intval($extrafield_param_list[0]);
			$expand_display = ((isset($_COOKIE['DOLCOLLAPSE_'.$object->table_element.'_extrafields_'.$key]) || GETPOST('ignorecollapsesetup', 'int')) ? (empty($_COOKIE['DOLCOLLAPSE_'.$object->table_element.'_extrafields_'.$key]) ? false : true) : ($extrafield_collapse_display_value == 2 ? false : true));
		}
		$disabledcookiewrite = 0;
		if ($mode == 'create') {
			// On create mode, force separator group to not be collapsable
			$extrafield_collapse_display_value = 1;
			$expand_display = true;	// We force group to be shown expanded
			$disabledcookiewrite = 1; // We keep status of group unchanged into the cookie
		}

		$out = '<'.$tagtype.' id="trextrafieldseparator'.$key.(!empty($object->id) ? '_'.$object->id : '').'" class="trextrafieldseparator trextrafieldseparator'.$key.(!empty($object->id) ? '_'.$object->id : '').'">';
		$out .= '<'.$tagtype_dyn.' '.(!empty($colspan) ? 'colspan="' . $colspan . '"' : '').'>';
		// Some js code will be injected here to manage the collapsing of extrafields
		// Output the picto
		$out .= '<span class="'.($extrafield_collapse_display_value ? 'cursorpointer ' : '').($extrafield_collapse_display_value == 0 ? 'fas fa-square opacitymedium' : 'far fa-'.(($expand_display ? 'minus' : 'plus').'-square')).'"></span>';
		$out .= '&nbsp;';
		$out .= '<strong>';
		$out .= $langs->trans($this->attributes[$object->table_element]['label'][$key]);
		$out .= '</strong>';
		$out .= '</'.$tagtype_dyn.'>';
		$out .= '</'.$tagtype.'>';

		$collapse_group = $key.(!empty($object->id) ? '_'.$object->id : '');
		//$extrafields_collapse_num = $this->attributes[$object->table_element]['pos'][$key].(!empty($object->id)?'_'.$object->id:'');

		if ($extrafield_collapse_display_value == 1 || $extrafield_collapse_display_value == 2) {
			// Set the collapse_display status to cookie in priority or if ignorecollapsesetup is 1, if cookie and ignorecollapsesetup not defined, use the setup.
			$this->expand_display[$collapse_group] = $expand_display;

			if (!empty($conf->use_javascript_ajax)) {
				$out .= '<!-- Add js script to manage the collapse/uncollapse of extrafields separators '.$key.' -->'."\n";
				$out .= '<script nonce="'.getNonce().'" type="text/javascript">'."\n";
				$out .= 'jQuery(document).ready(function(){'."\n";
				if (empty($disabledcookiewrite)) {
					if ($expand_display === false) {
						$out .= '   console.log("Inject js for the collapsing of extrafield '.$key.' - hide");'."\n";
						$out .= '   jQuery(".trextrafields_collapse'.$collapse_group.'").hide();'."\n";
					} else {
						$out .= '   console.log("Inject js for collapsing of extrafield '.$key.' - keep visible and set cookie");'."\n";
						$out .= '   document.cookie = "DOLCOLLAPSE_'.$object->table_element.'_extrafields_'.$key.'=1; path='.$_SERVER["PHP_SELF"].'"'."\n";
					}
				}
				$out .= '   jQuery("#trextrafieldseparator'.$key.(!empty($object->id) ? '_'.$object->id : '').'").click(function(){'."\n";
				$out .= '       console.log("We click on collapse/uncollapse to hide/show .trextrafields_collapse'.$collapse_group.'");'."\n";
				$out .= '       jQuery(".trextrafields_collapse'.$collapse_group.'").toggle(100, function(){'."\n";
				$out .= '           if (jQuery(".trextrafields_collapse'.$collapse_group.'").is(":hidden")) {'."\n";
				$out .= '               jQuery("#trextrafieldseparator'.$key.(!empty($object->id) ? '_'.$object->id : '').' '.$tagtype_dyn.' span").addClass("fa-plus-square").removeClass("fa-minus-square");'."\n";
				$out .= '               document.cookie = "DOLCOLLAPSE_'.$object->table_element.'_extrafields_'.$key.'=0; path='.$_SERVER["PHP_SELF"].'"'."\n";
				$out .= '           } else {'."\n";
				$out .= '               jQuery("#trextrafieldseparator'.$key.(!empty($object->id) ? '_'.$object->id : '').' '.$tagtype_dyn.' span").addClass("fa-minus-square").removeClass("fa-plus-square");'."\n";
				$out .= '               document.cookie = "DOLCOLLAPSE_'.$object->table_element.'_extrafields_'.$key.'=1; path='.$_SERVER["PHP_SELF"].'"'."\n";
				$out .= '           }'."\n";
				$out .= '       });'."\n";
				$out .= '   });'."\n";
				$out .= '});'."\n";
				$out .= '</script>'."\n";
			}
		} else {
			$this->expand_display[$collapse_group] = 1;
		}

		return $out;
	}

	/**
	 * Fill array_options property of object by extrafields value (using for data sent by forms)
	 *
	 * @param   array|null	$extralabels    	Deprecated (old $array of extrafields, now set this to null)
	 * @param   object		$object         	Object
	 * @param	string		$onlykey			Only some keys are filled:
	 *                      	            	'string' => When we make update of only one extrafield ($action = 'update_extras'), calling page can set this to avoid to have other extrafields being reset.
	 *                          	        	'@GETPOSTISSET' => When we make update of several extrafields ($action = 'update'), calling page can set this to avoid to have fields not into POST being reset.
	 * @param	int			$todefaultifmissing 1=Set value to the default value in database if value is mandatory and missing
	 * @return	int								1 if array_options set, 0 if no value, -1 if error (field required missing for example)
	 */
	public function setOptionalsFromPost($extralabels, &$object, $onlykey = '', $todefaultifmissing = 0)
	{
		global $conf, $_POST, $langs;

		$nofillrequired = 0; // For error when required field left blank
		$error_field_required = array();

		if (isset($this->attributes[$object->table_element]['label']) && is_array($this->attributes[$object->table_element]['label'])) {
			$extralabels = $this->attributes[$object->table_element]['label'];
		}

		if (is_array($extralabels)) {
			// Get extra fields
			foreach ($extralabels as $key => $value) {
				if (!empty($onlykey) && $onlykey != '@GETPOSTISSET' && $key != $onlykey) {
					continue;
				}

				if (!empty($onlykey) && $onlykey == '@GETPOSTISSET' && !GETPOSTISSET('options_'.$key) && (! in_array($this->attributes[$object->table_element]['type'][$key], array('boolean', 'checkbox', 'chkbxlst')))) {
					//when unticking boolean field, it's not set in POST
					continue;
				}

				$key_type = $this->attributes[$object->table_element]['type'][$key];
				if ($key_type == 'separate') {
					continue;
				}

				$enabled = 1;
				if (isset($this->attributes[$object->table_element]['enabled'][$key])) {	// 'enabled' is often a condition on module enabled or not
					$enabled = dol_eval($this->attributes[$object->table_element]['enabled'][$key], 1, 1, '2');
				}

				$visibility = 1;
				if (isset($this->attributes[$object->table_element]['list'][$key])) {		// 'list' is option for visibility
					$visibility = intval(dol_eval($this->attributes[$object->table_element]['list'][$key], 1, 1, '2'));
				}

				$perms = 1;
				if (isset($this->attributes[$object->table_element]['perms'][$key])) {
					$perms = dol_eval($this->attributes[$object->table_element]['perms'][$key], 1, 1, '2');
				}
				if (empty($enabled)
					|| (
						$onlykey === '@GETPOSTISSET'
						&& in_array($this->attributes[$object->table_element]['type'][$key], array('boolean', 'checkbox', 'chkbxlst'))
						&& in_array(abs($enabled), array(2, 5))
						&& ! GETPOSTISSET('options_' . $key) // Update hidden checkboxes and multiselect only if they are provided
					)
				) {
					continue;
				}

				$visibility_abs = abs($visibility);
				// not modify if extra field is not in update form (0 : never, 2 or -2 : list only, 5 or - 5 : list and view only)
				if (empty($visibility_abs) || $visibility_abs == 2 || $visibility_abs == 5) {
					continue;
				}
				if (empty($perms)) {
					continue;
				}

				if ($this->attributes[$object->table_element]['required'][$key]) {	// Value is required
					// Check if functionally empty without using GETPOST (depending on the type of extrafield, a
					// technically non-empty value may be treated as empty functionally).
					// value can be alpha, int, array, etc...
					$v = $_POST["options_".$key] ?? null;
					$type = $this->attributes[$object->table_element]['type'][$key];
					if (self::isEmptyValue($v, $type)) {
						//print 'ccc'.$value.'-'.$this->attributes[$object->table_element]['required'][$key];

						// Field is not defined. We mark this as an error. We may fix it later if there is a default value and $todefaultifmissing is set.

						$nofillrequired++;
						if (!empty($this->attributes[$object->table_element]['langfile'][$key])) {
							$langs->load($this->attributes[$object->table_element]['langfile'][$key]);
						}
						$error_field_required[$key] = $langs->transnoentitiesnoconv($value);
					}
				}

				if (in_array($key_type, array('date'))) {
					// Clean parameters
					$value_key = dol_mktime(12, 0, 0, GETPOST("options_".$key."month", 'int'), GETPOST("options_".$key."day", 'int'), GETPOST("options_".$key."year", 'int'));
				} elseif (in_array($key_type, array('datetime'))) {
					// Clean parameters
					$value_key = dol_mktime(GETPOST("options_".$key."hour", 'int'), GETPOST("options_".$key."min", 'int'), GETPOST("options_".$key."sec", 'int'), GETPOST("options_".$key."month", 'int'), GETPOST("options_".$key."day", 'int'), GETPOST("options_".$key."year", 'int'), 'tzuserrel');
				} elseif (in_array($key_type, array('datetimegmt'))) {
					// Clean parameters
					$value_key = dol_mktime(GETPOST("options_".$key."hour", 'int'), GETPOST("options_".$key."min", 'int'), GETPOST("options_".$key."sec", 'int'), GETPOST("options_".$key."month", 'int'), GETPOST("options_".$key."day", 'int'), GETPOST("options_".$key."year", 'int'), 'gmt');
				} elseif (in_array($key_type, array('checkbox', 'chkbxlst'))) {
					$value_arr = GETPOST("options_".$key, 'array'); // check if an array
					if (!empty($value_arr)) {
						$value_key = implode(',', $value_arr);
					} else {
						$value_key = '';
					}
				} elseif (in_array($key_type, array('price', 'double'))) {
					$value_arr = GETPOST("options_".$key, 'alpha');
					$value_key = price2num($value_arr);
				} elseif (in_array($key_type, array('pricecy', 'double'))) {
					$value_key = price2num(GETPOST("options_".$key, 'alpha')).':'.GETPOST("options_".$key."currency_id", 'alpha');
				} elseif (in_array($key_type, array('html'))) {
					$value_key = GETPOST("options_".$key, 'restricthtml');
				} elseif (in_array($key_type, array('text'))) {
					$label_security_check = 'alphanohtml';
					// by default 'alphanohtml' (better security); hidden conf MAIN_SECURITY_ALLOW_UNSECURED_LABELS_WITH_HTML allows basic html
					if (getDolGlobalString('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
						$label_security_check = 'nohtml';
					} else {
						$label_security_check = !getDolGlobalString('MAIN_SECURITY_ALLOW_UNSECURED_LABELS_WITH_HTML') ? 'alphanohtml' : 'restricthtml';
					}
					$value_key = GETPOST("options_".$key, $label_security_check);
				} else {
					$value_key = GETPOST("options_".$key);
					if (in_array($key_type, array('link')) && $value_key == '-1') {
						$value_key = '';
					}
				}

				if (!empty($error_field_required[$key]) && $todefaultifmissing) {
					// Value is required but we have a default value and we asked to set empty value to the default value
					if (!empty($this->attributes[$object->table_element]['default']) && !is_null($this->attributes[$object->table_element]['default'][$key])) {
						$value_key = $this->attributes[$object->table_element]['default'][$key];
						unset($error_field_required[$key]);
						$nofillrequired--;
					}
				}

				$object->array_options["options_".$key] = $value_key;
			}

			if ($nofillrequired) {
				$langs->load('errors');
				$this->error = $langs->trans('ErrorFieldsRequired').' : '.implode(', ', $error_field_required);
				setEventMessages($this->error, null, 'errors');
				return -1;
			} else {
				return 1;
			}
		} else {
			return 0;
		}
	}

	/**
	 * return array_options array of data of extrafields value of object sent by a search form
	 *
	 * @param  array|string		$extrafieldsobjectkey  	array of extrafields (old usage) or value of object->table_element (new usage)
	 * @param  string			$keyprefix      		Prefix string to add into name and id of field (can be used to avoid duplicate names)
	 * @param  string			$keysuffix      		Suffix string to add into name and id of field (can be used to avoid duplicate names)
	 * @return array|int								array_options set or 0 if no value
	 */
	public function getOptionalsFromPost($extrafieldsobjectkey, $keyprefix = '', $keysuffix = '')
	{
		global $_POST;

		if (is_string($extrafieldsobjectkey) && !empty($this->attributes[$extrafieldsobjectkey]['label']) && is_array($this->attributes[$extrafieldsobjectkey]['label'])) {
			$extralabels = $this->attributes[$extrafieldsobjectkey]['label'];
		} else {
			$extralabels = $extrafieldsobjectkey;
		}

		if (is_array($extralabels)) {
			$array_options = array();

			// Get extra fields
			foreach ($extralabels as $key => $value) {
				$key_type = '';
				if (is_string($extrafieldsobjectkey)) {
					$key_type = $this->attributes[$extrafieldsobjectkey]['type'][$key];
				}

				if (in_array($key_type, array('date'))) {
					$dateparamname_start = $keysuffix . 'options_' . $key . $keyprefix . '_start';
					$dateparamname_end   = $keysuffix . 'options_' . $key . $keyprefix . '_end';
					if (GETPOST($dateparamname_start . 'year') || GETPOST($dateparamname_end . 'year')) {
						$value_key = array();
						// values provided as a component year, month, day, etc.
						if (GETPOST($dateparamname_start . 'year')) {
							$value_key['start'] = dol_mktime(0, 0, 0, GETPOST($dateparamname_start . 'month', 'int'), GETPOST($dateparamname_start . 'day', 'int'), GETPOST($dateparamname_start . 'year', 'int'));
						}
						if (GETPOST($dateparamname_start . 'year')) {
							$value_key['end'] = dol_mktime(23, 59, 59, GETPOST($dateparamname_end . 'month', 'int'), GETPOST($dateparamname_end . 'day', 'int'), GETPOST($dateparamname_end . 'year', 'int'));
						}
					} elseif (GETPOST($keysuffix."options_".$key.$keyprefix."year")) {
						// Clean parameters
						$value_key = dol_mktime(12, 0, 0, GETPOST($keysuffix."options_".$key.$keyprefix."month", 'int'), GETPOST($keysuffix."options_".$key.$keyprefix."day", 'int'), GETPOST($keysuffix."options_".$key.$keyprefix."year", 'int'));
					} else {
						continue; // Value was not provided, we should not set it.
					}
				} elseif (in_array($key_type, array('datetime', 'datetimegmt'))) {
					$dateparamname_start = $keysuffix . 'options_' . $key . $keyprefix . '_start';
					$dateparamname_end   = $keysuffix . 'options_' . $key . $keyprefix . '_end';
					if (GETPOST($dateparamname_start . 'year') && GETPOST($dateparamname_end . 'year')) {
						// values provided as a date pair (start date + end date), each date being broken down as year, month, day, etc.
						$dateparamname_end_hour = GETPOST($dateparamname_end . 'hour', 'int') != '-1' ? GETPOST($dateparamname_end . 'hour', 'int') : '23';
						$dateparamname_end_min = GETPOST($dateparamname_end . 'min', 'int') != '-1' ? GETPOST($dateparamname_end . 'min', 'int') : '59';
						$dateparamname_end_sec = GETPOST($dateparamname_end . 'sec', 'int') != '-1' ? GETPOST($dateparamname_end . 'sec', 'int') : '59';
						if ($key_type == 'datetimegmt') {
							$value_key = array(
								'start' => dol_mktime(GETPOST($dateparamname_start . 'hour', 'int'), GETPOST($dateparamname_start . 'min', 'int'), GETPOST($dateparamname_start . 'sec', 'int'), GETPOST($dateparamname_start . 'month', 'int'), GETPOST($dateparamname_start . 'day', 'int'), GETPOST($dateparamname_start . 'year', 'int'), 'gmt'),
								'end' => dol_mktime($dateparamname_end_hour, $dateparamname_end_min, $dateparamname_end_sec, GETPOST($dateparamname_end . 'month', 'int'), GETPOST($dateparamname_end . 'day', 'int'), GETPOST($dateparamname_end . 'year', 'int'), 'gmt')
							);
						} else {
							$value_key = array(
								'start' => dol_mktime(GETPOST($dateparamname_start . 'hour', 'int'), GETPOST($dateparamname_start . 'min', 'int'), GETPOST($dateparamname_start . 'sec', 'int'), GETPOST($dateparamname_start . 'month', 'int'), GETPOST($dateparamname_start . 'day', 'int'), GETPOST($dateparamname_start . 'year', 'int'), 'tzuserrel'),
								'end' => dol_mktime($dateparamname_end_hour, $dateparamname_end_min, $dateparamname_end_sec, GETPOST($dateparamname_end . 'month', 'int'), GETPOST($dateparamname_end . 'day', 'int'), GETPOST($dateparamname_end . 'year', 'int'), 'tzuserrel')
							);
						}
					} elseif (GETPOST($keysuffix."options_".$key.$keyprefix."year")) {
						// Clean parameters
						if ($key_type == 'datetimegmt') {
							$value_key = dol_mktime(GETPOST($keysuffix."options_".$key.$keyprefix."hour", 'int'), GETPOST($keysuffix."options_".$key.$keyprefix."min", 'int'), GETPOST($keysuffix."options_".$key.$keyprefix."sec", 'int'), GETPOST($keysuffix."options_".$key.$keyprefix."month", 'int'), GETPOST($keysuffix."options_".$key.$keyprefix."day", 'int'), GETPOST($keysuffix."options_".$key.$keyprefix."year", 'int'), 'gmt');
						} else {
							$value_key = dol_mktime(GETPOST($keysuffix."options_".$key.$keyprefix."hour", 'int'), GETPOST($keysuffix."options_".$key.$keyprefix."min", 'int'), GETPOST($keysuffix."options_".$key.$keyprefix."sec", 'int'), GETPOST($keysuffix."options_".$key.$keyprefix."month", 'int'), GETPOST($keysuffix."options_".$key.$keyprefix."day", 'int'), GETPOST($keysuffix."options_".$key.$keyprefix."year", 'int'), 'tzuserrel');
						}
					} else {
						continue; // Value was not provided, we should not set it.
					}
				} elseif ($key_type == 'select') {
					// to detect if we are in search context
					if (GETPOSTISARRAY($keysuffix."options_".$key.$keyprefix)) {
						$value_arr = GETPOST($keysuffix."options_".$key.$keyprefix, 'array:aZ09');
						// Make sure we get an array even if there's only one selected
						$value_arr = (array) $value_arr;
						$value_key = implode(',', $value_arr);
					} else {
						$value_key = GETPOST($keysuffix."options_".$key.$keyprefix);
					}
				} elseif (in_array($key_type, array('checkbox', 'chkbxlst'))) {
					// We test on a hidden field named "..._multiselect" that is always set to 1 if param is in form so
					// when nothing is provided we can make a difference between noparam in the form and param was set to nothing.
					if (!GETPOSTISSET($keysuffix."options_".$key.$keyprefix.'_multiselect')) {
						continue; // Value was not provided, we should not set it.
					}
					$value_arr = GETPOST($keysuffix."options_".$key.$keyprefix);
					// Make sure we get an array even if there's only one checkbox
					$value_arr = (array) $value_arr;
					$value_key = implode(',', $value_arr);
				} elseif (in_array($key_type, array('price', 'double', 'int'))) {
					if (!GETPOSTISSET($keysuffix."options_".$key.$keyprefix)) {
						continue; // Value was not provided, we should not set it.
					}
					$value_arr = GETPOST($keysuffix."options_".$key.$keyprefix);
					if ($keysuffix != 'search_') {    // If value is for a search, we must keep complex string like '>100 <=150'
						$value_key = price2num($value_arr);
					} else {
						$value_key = $value_arr;
					}
				} elseif (in_array($key_type, array('boolean'))) {
					// We test on a hidden field named "..._boolean" that is always set to 1 if param is in form so
					// when nothing is provided we can make a difference between noparam in the form and param was set to nothing.
					if (!GETPOSTISSET($keysuffix."options_".$key.$keyprefix."_boolean")) {
						$value_key = '';
					} else {
						$value_arr = GETPOST($keysuffix."options_".$key.$keyprefix);
						$value_key = $value_arr;
					}
				} elseif (in_array($key_type, array('html'))) {
					if (!GETPOSTISSET($keysuffix."options_".$key.$keyprefix)) {
						continue; // Value was not provided, we should not set it.
					}
					$value_key = dol_htmlcleanlastbr(GETPOST($keysuffix."options_".$key.$keyprefix, 'restricthtml'));
				} else {
					if (!GETPOST($keysuffix."options_".$key.$keyprefix)) {
						continue; // Value was not provided, we should not set it.
					}
					$value_key = GETPOST($keysuffix."options_".$key.$keyprefix);
				}

				$array_options[$keysuffix."options_".$key] = $value_key; // No keyprefix here. keyprefix is used only for read.
			}

			return $array_options;
		}

		return 0;
	}

	/**
	 * Return if a value is "empty" for a mandatory vision.
	 *
	 * @param 	mixed	$v		Value to test
	 * @param 	string 	$type	Type of extrafield 'sellist', 'link', 'select', ...
	 * @return 	boolean			True is value is an empty value, not allowed for a mandatory field.
	 */
	public static function isEmptyValue($v, string $type)
	{
		if ($v === null || $v === '') {
			return true;
		}
		if (is_array($v) || $type == 'select') {
			return empty($v);
		}
		if ($type == 'link') {
			return ($v == '-1');
		}
		if ($type == 'sellist') {
			return ($v == '0');
		}
		return (empty($v) && $v != '0');
	}
}
