1<?php
2/* Copyright (C) 2019  Laurent Destailleur <eldy@users.sourceforge.net>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17
18/**
19 * \file        bom/class/bom.class.php
20 * \ingroup     bom
21 * \brief       This file is a CRUD class file for BOM (Create/Read/Update/Delete)
22 */
23
24// Put here all includes required by your class file
25require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
26//require_once DOL_DOCUMENT_ROOT . '/societe/class/societe.class.php';
27//require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php';
28
29
30/**
31 * Class for BOM
32 */
33class BOM extends CommonObject
34{
35	/**
36	 * @var string ID to identify managed object
37	 */
38	public $element = 'bom';
39
40	/**
41	 * @var string Name of table without prefix where object is stored
42	 */
43	public $table_element = 'bom_bom';
44
45	/**
46	 * @var int  Does bom support multicompany module ? 0=No test on entity, 1=Test with field entity, 2=Test with link by societe
47	 */
48	public $ismultientitymanaged = 1;
49
50	/**
51	 * @var int  Does object support extrafields ? 0=No, 1=Yes
52	 */
53	public $isextrafieldmanaged = 1;
54
55	/**
56	 * @var string String with name of icon for bom. Must be the part after the 'object_' into object_bom.png
57	 */
58	public $picto = 'bom';
59
60
61	const STATUS_DRAFT = 0;
62	const STATUS_VALIDATED = 1;
63	const STATUS_CANCELED = 9;
64
65
66	/**
67	 *  'type' field format ('integer', 'integer:ObjectClass:PathToClass[:AddCreateButtonOrNot[:Filter]]', 'sellist:TableName:LabelFieldName[:KeyFieldName[:KeyFieldParent[:Filter]]]', 'varchar(x)', 'double(24,8)', 'real', 'price', 'text', 'text:none', 'html', 'date', 'datetime', 'timestamp', 'duration', 'mail', 'phone', 'url', 'password')
68	 *         Note: Filter can be a string like "(t.ref:like:'SO-%') or (t.date_creation:<:'20160101') or (t.nature:is:NULL)"
69	 *  'label' the translation key.
70	 *  'picto' is code of a picto to show before value in forms
71	 *  'enabled' is a condition when the field must be managed (Example: 1 or '$conf->global->MY_SETUP_PARAM)
72	 *  'position' is the sort order of field.
73	 *  'notnull' is set to 1 if not null in database. Set to -1 if we must set data to null if empty ('' or 0).
74	 *  'visible' says if field is visible in list (Examples: 0=Not visible, 1=Visible on list and create/update/view forms, 2=Visible on list only, 3=Visible on create/update/view form only (not list), 4=Visible on list and update/view form only (not create). 5=Visible on list and view only (not create/not update). Using a negative value means field is not shown by default on list but can be selected for viewing)
75	 *  'noteditable' says if field is not editable (1 or 0)
76	 *  'default' is a default value for creation (can still be overwrote by the Setup of Default Values if field is editable in creation form). Note: If default is set to '(PROV)' and field is 'ref', the default value will be set to '(PROVid)' where id is rowid when a new record is created.
77	 *  'index' if we want an index in database.
78	 *  'foreignkey'=>'tablename.field' if the field is a foreign key (it is recommanded to name the field fk_...).
79	 *  'searchall' is 1 if we want to search in this field when making a search from the quick search button.
80	 *  'isameasure' must be set to 1 if you want to have a total on list for this field. Field type must be summable like integer or double(24,8).
81	 *  'css' and 'cssview' and 'csslist' is the CSS style to use on field. 'css' is used in creation and update. 'cssview' is used in view mode. 'csslist' is used for columns in lists. For example: 'maxwidth200', 'wordbreak', 'tdoverflowmax200'
82	 *  'help' is a 'TranslationString' to use to show a tooltip on field. You can also use 'TranslationString:keyfortooltiponlick' for a tooltip on click.
83	 *  'showoncombobox' if value of the field must be visible into the label of the combobox that list record
84	 *  'disabled' is 1 if we want to have the field locked by a 'disabled' attribute. In most cases, this is never set into the definition of $fields into class, but is set dynamically by some part of code.
85	 *  'arrayofkeyval' to set list of value if type is a list of predefined values. For example: array("0"=>"Draft","1"=>"Active","-1"=>"Cancel")
86	 *  'autofocusoncreate' to have field having the focus on a create form. Only 1 field should have this property set to 1.
87	 *  'comment' is not used. You can store here any text of your choice. It is not used by application.
88	 *
89	 *  Note: To have value dynamic, you can set value to 0 in definition and edit the value on the fly into the constructor.
90	 */
91
92	// BEGIN MODULEBUILDER PROPERTIES
93	/**
94	 * @var array  Array with all fields and their property. Do not use it as a static var. It may be modified by constructor.
95	 */
96	public $fields = array(
97		'rowid' => array('type'=>'integer', 'label'=>'TechnicalID', 'enabled'=>1, 'visible'=>-1, 'position'=>1, 'notnull'=>1, 'index'=>1, 'comment'=>"Id",),
98		'entity' => array('type'=>'integer', 'label'=>'Entity', 'enabled'=>1, 'visible'=>0, 'notnull'=> 1, 'default'=>1, 'index'=>1, 'position'=>5),
99		'ref' => array('type'=>'varchar(128)', 'label'=>'Ref', 'enabled'=>1, 'noteditable'=>1, 'visible'=>4, 'position'=>10, 'notnull'=>1, 'default'=>'(PROV)', 'index'=>1, 'searchall'=>1, 'comment'=>"Reference of BOM", 'showoncombobox'=>'1',),
100		'label' => array('type'=>'varchar(255)', 'label'=>'Label', 'enabled'=>1, 'visible'=>1, 'position'=>30, 'notnull'=>1, 'searchall'=>1, 'showoncombobox'=>'2', 'autofocusoncreate'=>1, 'css'=>'maxwidth300', 'csslist'=>'tdoverflowmax200'),
101		'bomtype' => array('type'=>'integer', 'label'=>'Type', 'enabled'=>1, 'visible'=>1, 'position'=>33, 'notnull'=>1, 'default'=>'0', 'arrayofkeyval'=>array(0=>'Manufacturing', 1=>'Disassemble'), 'css'=>'minwidth175', 'csslist'=>'minwidth175 center'),
102		//'bomtype' => array('type'=>'integer', 'label'=>'Type', 'enabled'=>1, 'visible'=>-1, 'position'=>32, 'notnull'=>1, 'default'=>'0', 'arrayofkeyval'=>array(0=>'Manufacturing')),
103		'fk_product' => array('type'=>'integer:Product:product/class/product.class.php:1:(finished IS NULL or finished <> 0)', 'label'=>'Product', 'picto'=>'product', 'enabled'=>1, 'visible'=>1, 'position'=>35, 'notnull'=>1, 'index'=>1, 'help'=>'ProductBOMHelp', 'css'=>'maxwidth500', 'csslist'=>'tdoverflowmax100'),
104		'description' => array('type'=>'text', 'label'=>'Description', 'enabled'=>1, 'visible'=>-1, 'position'=>60, 'notnull'=>-1,),
105		'qty' => array('type'=>'real', 'label'=>'Quantity', 'enabled'=>1, 'visible'=>1, 'default'=>1, 'position'=>55, 'notnull'=>1, 'isameasure'=>'1', 'css'=>'maxwidth75imp'),
106		//'efficiency' => array('type'=>'real', 'label'=>'ManufacturingEfficiency', 'enabled'=>1, 'visible'=>-1, 'default'=>1, 'position'=>100, 'notnull'=>0, 'css'=>'maxwidth50imp', 'help'=>'ValueOfMeansLossForProductProduced'),
107		'duration' => array('type'=>'duration', 'label'=>'EstimatedDuration', 'enabled'=>1, 'visible'=>-1, 'position'=>101, 'notnull'=>-1, 'css'=>'maxwidth50imp', 'help'=>'EstimatedDurationDesc'),
108		'fk_warehouse' => array('type'=>'integer:Entrepot:product/stock/class/entrepot.class.php:0', 'label'=>'WarehouseForProduction', 'picto'=>'stock', 'enabled'=>1, 'visible'=>-1, 'position'=>102, 'css'=>'maxwidth500', 'csslist'=>'tdoverflowmax100'),
109		'note_public' => array('type'=>'html', 'label'=>'NotePublic', 'enabled'=>1, 'visible'=>-2, 'position'=>161, 'notnull'=>-1,),
110		'note_private' => array('type'=>'html', 'label'=>'NotePrivate', 'enabled'=>1, 'visible'=>-2, 'position'=>162, 'notnull'=>-1,),
111		'date_creation' => array('type'=>'datetime', 'label'=>'DateCreation', 'enabled'=>1, 'visible'=>-2, 'position'=>300, 'notnull'=>1,),
112		'tms' => array('type'=>'timestamp', 'label'=>'DateModification', 'enabled'=>1, 'visible'=>-2, 'position'=>501, 'notnull'=>1,),
113		'date_valid' => array('type'=>'datetime', 'label'=>'DateValidation', 'enabled'=>1, 'visible'=>-2, 'position'=>502, 'notnull'=>0,),
114		'fk_user_creat' => array('type'=>'integer:User:user/class/user.class.php', 'label'=>'UserCreation', 'picto'=>'user', 'enabled'=>1, 'visible'=>-2, 'position'=>510, 'notnull'=>1, 'foreignkey'=>'user.rowid', 'csslist'=>'tdoverflowmax100'),
115		'fk_user_modif' => array('type'=>'integer:User:user/class/user.class.php', 'label'=>'UserModif', 'picto'=>'user', 'enabled'=>1, 'visible'=>-2, 'position'=>511, 'notnull'=>-1, 'csslist'=>'tdoverflowmax100'),
116		'fk_user_valid' => array('type'=>'integer:User:user/class/user.class.php', 'label'=>'UserValidation', 'picto'=>'user', 'enabled'=>1, 'visible'=>-2, 'position'=>512, 'notnull'=>0, 'csslist'=>'tdoverflowmax100'),
117		'import_key' => array('type'=>'varchar(14)', 'label'=>'ImportId', 'enabled'=>1, 'visible'=>-2, 'position'=>1000, 'notnull'=>-1,),
118		'model_pdf' =>array('type'=>'varchar(255)', 'label'=>'Model pdf', 'enabled'=>1, 'visible'=>0, 'position'=>1010),
119		'status' => array('type'=>'integer', 'label'=>'Status', 'enabled'=>1, 'visible'=>2, 'position'=>1000, 'notnull'=>1, 'default'=>0, 'index'=>1, 'arrayofkeyval'=>array(0=>'Draft', 1=>'Enabled', 9=>'Disabled')),
120	);
121
122	/**
123	 * @var int rowid
124	 */
125	public $rowid;
126
127	/**
128	 * @var string ref
129	 */
130	public $ref;
131
132	/**
133	 * @var string label
134	 */
135	public $label;
136
137	/**
138	 * @var int bomtype
139	 */
140	public $bomtype;
141
142	/**
143	 * @var string description
144	 */
145	public $description;
146
147	/**
148	 * @var integer|string date_creation
149	 */
150	public $date_creation;
151
152
153	public $tms;
154
155	/**
156	 * @var int Id User creator
157	 */
158	public $fk_user_creat;
159
160	/**
161	 * @var int Id User modifying
162	 */
163	public $fk_user_modif;
164
165	/**
166	 * @var string import key
167	 */
168	public $import_key;
169
170	/**
171	 * @var int status
172	 */
173	public $status;
174
175	/**
176	 * @var int product Id
177	 */
178	public $fk_product;
179	public $qty;
180	public $efficiency;
181	// END MODULEBUILDER PROPERTIES
182
183
184	// If this object has a subtable with lines
185
186	/**
187	 * @var int    Name of subtable line
188	 */
189	public $table_element_line = 'bom_bomline';
190
191	/**
192	 * @var string    Fieldname with ID of parent key if this field has a parent
193	 */
194	public $fk_element = 'fk_bom';
195
196	/**
197	 * @var string    Name of subtable class that manage subtable lines
198	 */
199	public $class_element_line = 'BOMLine';
200
201	// /**
202	//  * @var array	List of child tables. To test if we can delete object.
203	//  */
204	// protected $childtables=array();
205
206	/**
207	 * @var array	List of child tables. To know object to delete on cascade.
208	 */
209	protected $childtablesoncascade = array('bom_bomline');
210
211	/**
212	 * @var BOMLine[]     Array of subtable lines
213	 */
214	public $lines = array();
215
216	/**
217	 * @var int		Calculated cost for the BOM
218	 */
219	public $total_cost = 0;
220
221	/**
222	 * @var int		Calculated cost for 1 unit of the product in BOM
223	 */
224	public $unit_cost = 0;
225
226
227
228	/**
229	 * Constructor
230	 *
231	 * @param DoliDb $db Database handler
232	 */
233	public function __construct(DoliDB $db)
234	{
235		global $conf, $langs;
236
237		$this->db = $db;
238
239		if (empty($conf->global->MAIN_SHOW_TECHNICAL_ID) && isset($this->fields['rowid'])) {
240			$this->fields['rowid']['visible'] = 0;
241		}
242		if (empty($conf->multicompany->enabled) && isset($this->fields['entity'])) {
243			$this->fields['entity']['enabled'] = 0;
244		}
245
246		// Unset fields that are disabled
247		foreach ($this->fields as $key => $val) {
248			if (isset($val['enabled']) && empty($val['enabled'])) {
249				unset($this->fields[$key]);
250			}
251		}
252
253		// Translate some data of arrayofkeyval
254		foreach ($this->fields as $key => $val) {
255			if (!empty($val['arrayofkeyval']) && is_array($val['arrayofkeyval'])) {
256				foreach ($val['arrayofkeyval'] as $key2 => $val2) {
257					$this->fields[$key]['arrayofkeyval'][$key2] = $langs->trans($val2);
258				}
259			}
260		}
261	}
262
263	/**
264	 * Create object into database
265	 *
266	 * @param  User $user      User that creates
267	 * @param  bool $notrigger false=launch triggers after, true=disable triggers
268	 * @return int             <0 if KO, Id of created object if OK
269	 */
270	public function create(User $user, $notrigger = false)
271	{
272		if ($this->efficiency <= 0 || $this->efficiency > 1) {
273			$this->efficiency = 1;
274		}
275
276		return $this->createCommon($user, $notrigger);
277	}
278
279	/**
280	 * Clone an object into another one
281	 *
282	 * @param  	User 	$user      	User that creates
283	 * @param  	int 	$fromid     Id of object to clone
284	 * @return 	mixed 				New object created, <0 if KO
285	 */
286	public function createFromClone(User $user, $fromid)
287	{
288		global $langs, $hookmanager, $extrafields;
289		$error = 0;
290
291		dol_syslog(__METHOD__, LOG_DEBUG);
292
293		$object = new self($this->db);
294
295		$this->db->begin();
296
297		// Load source object
298		$result = $object->fetchCommon($fromid);
299		if ($result > 0 && !empty($object->table_element_line)) {
300			$object->fetchLines();
301		}
302
303		// Get lines so they will be clone
304		//foreach ($object->lines as $line)
305		//	$line->fetch_optionals();
306
307		// Reset some properties
308		unset($object->id);
309		unset($object->fk_user_creat);
310		unset($object->import_key);
311
312		// Clear fields
313		$object->ref = empty($this->fields['ref']['default']) ? $langs->trans("copy_of_").$object->ref : $this->fields['ref']['default'];
314		$object->label = empty($this->fields['label']['default']) ? $langs->trans("CopyOf")." ".$object->label : $this->fields['label']['default'];
315		$object->status = self::STATUS_DRAFT;
316		// ...
317		// Clear extrafields that are unique
318		if (is_array($object->array_options) && count($object->array_options) > 0) {
319			$extrafields->fetch_name_optionals_label($object->table_element);
320			foreach ($object->array_options as $key => $option) {
321				$shortkey = preg_replace('/options_/', '', $key);
322				if (!empty($extrafields->attributes[$this->element]['unique'][$shortkey])) {
323					//var_dump($key); var_dump($clonedObj->array_options[$key]); exit;
324					unset($object->array_options[$key]);
325				}
326			}
327		}
328
329		// Create clone
330		$object->context['createfromclone'] = 'createfromclone';
331		$result = $object->createCommon($user);
332		if ($result < 0) {
333			$error++;
334			$this->error = $object->error;
335			$this->errors = $object->errors;
336		}
337
338		if (!$error) {
339			// copy internal contacts
340			if ($this->copy_linked_contact($object, 'internal') < 0) {
341				$error++;
342			}
343		}
344
345		if (!$error) {
346			// copy external contacts if same company
347			if (property_exists($this, 'socid') && $this->socid == $object->socid) {
348				if ($this->copy_linked_contact($object, 'external') < 0) {
349					$error++;
350				}
351			}
352		}
353
354		// If there is lines, create lines too
355
356
357
358		unset($object->context['createfromclone']);
359
360		// End
361		if (!$error) {
362			$this->db->commit();
363			return $object;
364		} else {
365			$this->db->rollback();
366			return -1;
367		}
368	}
369
370	/**
371	 * Load object in memory from the database
372	 *
373	 * @param int    $id   Id object
374	 * @param string $ref  Ref
375	 * @return int         <0 if KO, 0 if not found, >0 if OK
376	 */
377	public function fetch($id, $ref = null)
378	{
379		$result = $this->fetchCommon($id, $ref);
380
381		if ($result > 0 && !empty($this->table_element_line)) {
382			$this->fetchLines();
383		}
384		//$this->calculateCosts();		// This consume a high number of subrequests. Do not call it into fetch but when you need it.
385
386		return $result;
387	}
388
389	/**
390	 * Load object lines in memory from the database
391	 *
392	 * @return int         <0 if KO, 0 if not found, >0 if OK
393	 */
394	public function fetchLines()
395	{
396		$this->lines = array();
397
398		$result = $this->fetchLinesCommon();
399		return $result;
400	}
401
402	/**
403	 * Load list of objects in memory from the database.
404	 *
405	 * @param  string      $sortorder    Sort Order
406	 * @param  string      $sortfield    Sort field
407	 * @param  int         $limit        limit
408	 * @param  int         $offset       Offset
409	 * @param  array       $filter       Filter array. Example array('field'=>'valueforlike', 'customurl'=>...)
410	 * @param  string      $filtermode   Filter mode (AND or OR)
411	 * @return array|int                 int <0 if KO, array of pages if OK
412	 */
413	public function fetchAll($sortorder = '', $sortfield = '', $limit = 0, $offset = 0, array $filter = array(), $filtermode = 'AND')
414	{
415		global $conf;
416
417		dol_syslog(__METHOD__, LOG_DEBUG);
418
419		$records = array();
420
421		$sql = 'SELECT ';
422		$sql .= $this->getFieldList();
423		$sql .= ' FROM '.MAIN_DB_PREFIX.$this->table_element.' as t';
424		if ($this->ismultientitymanaged) {
425			$sql .= ' WHERE t.entity IN ('.getEntity($this->table_element).')';
426		} else {
427			$sql .= ' WHERE 1 = 1';
428		}
429		// Manage filter
430		$sqlwhere = array();
431		if (count($filter) > 0) {
432			foreach ($filter as $key => $value) {
433				if ($key == 't.rowid') {
434					$sqlwhere[] = $key.' = '.((int) $value);
435				} elseif (strpos($key, 'date') !== false) {
436					$sqlwhere[] = $key." = '".$this->db->idate($value)."'";
437				} elseif ($key == 'customsql') {
438					$sqlwhere[] = $value;
439				} else {
440					$sqlwhere[] = $key." LIKE '%".$this->db->escape($value)."%'";
441				}
442			}
443		}
444		if (count($sqlwhere) > 0) {
445			$sql .= ' AND ('.implode(' '.$filtermode.' ', $sqlwhere).')';
446		}
447
448		if (!empty($sortfield)) {
449			$sql .= $this->db->order($sortfield, $sortorder);
450		}
451		if (!empty($limit)) {
452			$sql .= ' '.$this->db->plimit($limit, $offset);
453		}
454
455		$resql = $this->db->query($sql);
456		if ($resql) {
457			$num = $this->db->num_rows($resql);
458
459			while ($obj = $this->db->fetch_object($resql)) {
460				$record = new self($this->db);
461				$record->setVarsFromFetchObj($obj);
462
463				$records[$record->id] = $record;
464			}
465			$this->db->free($resql);
466
467			return $records;
468		} else {
469			$this->errors[] = 'Error '.$this->db->lasterror();
470			dol_syslog(__METHOD__.' '.join(',', $this->errors), LOG_ERR);
471
472			return -1;
473		}
474	}
475
476	/**
477	 * Update object into database
478	 *
479	 * @param  User $user      User that modifies
480	 * @param  bool $notrigger false=launch triggers after, true=disable triggers
481	 * @return int             <0 if KO, >0 if OK
482	 */
483	public function update(User $user, $notrigger = false)
484	{
485		if ($this->efficiency <= 0 || $this->efficiency > 1) {
486			$this->efficiency = 1;
487		}
488
489		return $this->updateCommon($user, $notrigger);
490	}
491
492	/**
493	 * Delete object in database
494	 *
495	 * @param User $user       User that deletes
496	 * @param bool $notrigger  false=launch triggers after, true=disable triggers
497	 * @return int             <0 if KO, >0 if OK
498	 */
499	public function delete(User $user, $notrigger = false)
500	{
501		return $this->deleteCommon($user, $notrigger);
502		//return $this->deleteCommon($user, $notrigger, 1);
503	}
504
505	/**
506	 *  Delete a line of object in database
507	 *
508	 *	@param  User	$user       User that delete
509	 *  @param	int		$idline		Id of line to delete
510	 *  @param 	bool 	$notrigger  false=launch triggers after, true=disable triggers
511	 *  @return int         		>0 if OK, <0 if KO
512	 */
513	public function deleteLine(User $user, $idline, $notrigger = false)
514	{
515		if ($this->status < 0) {
516			$this->error = 'ErrorDeleteLineNotAllowedByObjectStatus';
517			return -2;
518		}
519
520		return $this->deleteLineCommon($user, $idline, $notrigger);
521	}
522
523	/**
524	 *  Returns the reference to the following non used BOM depending on the active numbering module
525	 *  defined into BOM_ADDON
526	 *
527	 *  @param	Product		$prod 	Object product
528	 *  @return string      		BOM free reference
529	 */
530	public function getNextNumRef($prod)
531	{
532		global $langs, $conf;
533		$langs->load("mrp");
534
535		if (!empty($conf->global->BOM_ADDON)) {
536			$mybool = false;
537
538			$file = $conf->global->BOM_ADDON.".php";
539			$classname = $conf->global->BOM_ADDON;
540
541			// Include file with class
542			$dirmodels = array_merge(array('/'), (array) $conf->modules_parts['models']);
543			foreach ($dirmodels as $reldir) {
544				$dir = dol_buildpath($reldir."core/modules/bom/");
545
546				// Load file with numbering class (if found)
547				$mybool |= @include_once $dir.$file;
548			}
549
550			if ($mybool === false) {
551				dol_print_error('', "Failed to include file ".$file);
552				return '';
553			}
554
555			$obj = new $classname();
556			$numref = $obj->getNextValue($prod, $this);
557
558			if ($numref != "") {
559				return $numref;
560			} else {
561				$this->error = $obj->error;
562				//dol_print_error($this->db,get_class($this)."::getNextNumRef ".$obj->error);
563				return "";
564			}
565		} else {
566			print $langs->trans("Error")." ".$langs->trans("Error_BOM_ADDON_NotDefined");
567			return "";
568		}
569	}
570
571	/**
572	 *	Validate bom
573	 *
574	 *	@param		User	$user     		User making status change
575	 *  @param		int		$notrigger		1=Does not execute triggers, 0= execute triggers
576	 *	@return  	int						<=0 if OK, 0=Nothing done, >0 if KO
577	 */
578	public function validate($user, $notrigger = 0)
579	{
580		global $conf, $langs;
581
582		require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
583
584		$error = 0;
585
586		// Protection
587		if ($this->status == self::STATUS_VALIDATED) {
588			dol_syslog(get_class($this)."::validate action abandonned: already validated", LOG_WARNING);
589			return 0;
590		}
591
592		/*if (! ((empty($conf->global->MAIN_USE_ADVANCED_PERMS) && ! empty($user->rights->bom->create))
593			|| (! empty($conf->global->MAIN_USE_ADVANCED_PERMS) && ! empty($user->rights->bom->bom_advance->validate))))
594		{
595			$this->error='NotEnoughPermissions';
596			dol_syslog(get_class($this)."::valid ".$this->error, LOG_ERR);
597			return -1;
598		}*/
599
600		$now = dol_now();
601
602		$this->db->begin();
603
604		// Define new ref
605		if (!$error && (preg_match('/^[\(]?PROV/i', $this->ref) || empty($this->ref))) { // empty should not happened, but when it occurs, the test save life
606			$this->fetch_product();
607			$num = $this->getNextNumRef($this->product);
608		} else {
609			$num = $this->ref;
610		}
611		$this->newref = dol_sanitizeFileName($num);
612
613		// Validate
614		$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element;
615		$sql .= " SET ref = '".$this->db->escape($num)."',";
616		$sql .= " status = ".self::STATUS_VALIDATED.",";
617		$sql .= " date_valid='".$this->db->idate($now)."',";
618		$sql .= " fk_user_valid = ".((int) $user->id);
619		$sql .= " WHERE rowid = ".((int) $this->id);
620
621		dol_syslog(get_class($this)."::validate()", LOG_DEBUG);
622		$resql = $this->db->query($sql);
623		if (!$resql) {
624			dol_print_error($this->db);
625			$this->error = $this->db->lasterror();
626			$error++;
627		}
628
629		if (!$error && !$notrigger) {
630			// Call trigger
631			$result = $this->call_trigger('BOM_VALIDATE', $user);
632			if ($result < 0) {
633				$error++;
634			}
635			// End call triggers
636		}
637
638		if (!$error) {
639			$this->oldref = $this->ref;
640
641			// Rename directory if dir was a temporary ref
642			if (preg_match('/^[\(]?PROV/i', $this->ref)) {
643				// Now we rename also files into index
644				$sql = 'UPDATE '.MAIN_DB_PREFIX."ecm_files set filename = CONCAT('".$this->db->escape($this->newref)."', SUBSTR(filename, ".(strlen($this->ref) + 1).")), filepath = 'bom/".$this->db->escape($this->newref)."'";
645				$sql .= " WHERE filename LIKE '".$this->db->escape($this->ref)."%' AND filepath = 'bom/".$this->db->escape($this->ref)."' and entity = ".$conf->entity;
646				$resql = $this->db->query($sql);
647				if (!$resql) {
648					$error++; $this->error = $this->db->lasterror();
649				}
650
651				// We rename directory ($this->ref = old ref, $num = new ref) in order not to lose the attachments
652				$oldref = dol_sanitizeFileName($this->ref);
653				$newref = dol_sanitizeFileName($num);
654				$dirsource = $conf->bom->dir_output.'/'.$oldref;
655				$dirdest = $conf->bom->dir_output.'/'.$newref;
656				if (!$error && file_exists($dirsource)) {
657					dol_syslog(get_class($this)."::validate() rename dir ".$dirsource." into ".$dirdest);
658
659					if (@rename($dirsource, $dirdest)) {
660						dol_syslog("Rename ok");
661						// Rename docs starting with $oldref with $newref
662						$listoffiles = dol_dir_list($conf->bom->dir_output.'/'.$newref, 'files', 1, '^'.preg_quote($oldref, '/'));
663						foreach ($listoffiles as $fileentry) {
664							$dirsource = $fileentry['name'];
665							$dirdest = preg_replace('/^'.preg_quote($oldref, '/').'/', $newref, $dirsource);
666							$dirsource = $fileentry['path'].'/'.$dirsource;
667							$dirdest = $fileentry['path'].'/'.$dirdest;
668							@rename($dirsource, $dirdest);
669						}
670					}
671				}
672			}
673		}
674
675		// Set new ref and current status
676		if (!$error) {
677			$this->ref = $num;
678			$this->status = self::STATUS_VALIDATED;
679		}
680
681		if (!$error) {
682			$this->db->commit();
683			return 1;
684		} else {
685			$this->db->rollback();
686			return -1;
687		}
688	}
689
690	/**
691	 *	Set draft status
692	 *
693	 *	@param	User	$user			Object user that modify
694	 *  @param	int		$notrigger		1=Does not execute triggers, 0=Execute triggers
695	 *	@return	int						<0 if KO, >0 if OK
696	 */
697	public function setDraft($user, $notrigger = 0)
698	{
699		// Protection
700		if ($this->status <= self::STATUS_DRAFT) {
701			return 0;
702		}
703
704		/*if (! ((empty($conf->global->MAIN_USE_ADVANCED_PERMS) && ! empty($user->rights->bom->write))
705		 || (! empty($conf->global->MAIN_USE_ADVANCED_PERMS) && ! empty($user->rights->bom->bom_advance->validate))))
706		 {
707		 $this->error='Permission denied';
708		 return -1;
709		 }*/
710
711		return $this->setStatusCommon($user, self::STATUS_DRAFT, $notrigger, 'BOM_UNVALIDATE');
712	}
713
714	/**
715	 *	Set cancel status
716	 *
717	 *	@param	User	$user			Object user that modify
718	 *  @param	int		$notrigger		1=Does not execute triggers, 0=Execute triggers
719	 *	@return	int						<0 if KO, 0=Nothing done, >0 if OK
720	 */
721	public function cancel($user, $notrigger = 0)
722	{
723		// Protection
724		if ($this->status != self::STATUS_VALIDATED) {
725			return 0;
726		}
727
728		/*if (! ((empty($conf->global->MAIN_USE_ADVANCED_PERMS) && ! empty($user->rights->bom->write))
729		 || (! empty($conf->global->MAIN_USE_ADVANCED_PERMS) && ! empty($user->rights->bom->bom_advance->validate))))
730		 {
731		 $this->error='Permission denied';
732		 return -1;
733		 }*/
734
735		return $this->setStatusCommon($user, self::STATUS_CANCELED, $notrigger, 'BOM_CLOSE');
736	}
737
738	/**
739	 *	Set cancel status
740	 *
741	 *	@param	User	$user			Object user that modify
742	 *  @param	int		$notrigger		1=Does not execute triggers, 0=Execute triggers
743	 *	@return	int						<0 if KO, 0=Nothing done, >0 if OK
744	 */
745	public function reopen($user, $notrigger = 0)
746	{
747		// Protection
748		if ($this->status != self::STATUS_CANCELED) {
749			return 0;
750		}
751
752		/*if (! ((empty($conf->global->MAIN_USE_ADVANCED_PERMS) && ! empty($user->rights->bom->write))
753		 || (! empty($conf->global->MAIN_USE_ADVANCED_PERMS) && ! empty($user->rights->bom->bom_advance->validate))))
754		 {
755		 $this->error='Permission denied';
756		 return -1;
757		 }*/
758
759		return $this->setStatusCommon($user, self::STATUS_VALIDATED, $notrigger, 'BOM_REOPEN');
760	}
761
762
763	/**
764	 *  Return a link to the object card (with optionaly the picto)
765	 *
766	 *	@param	int		$withpicto					Include picto in link (0=No picto, 1=Include picto into link, 2=Only picto)
767	 *	@param	string	$option						On what the link point to ('nolink', ...)
768	 *  @param	int  	$notooltip					1=Disable tooltip
769	 *  @param  string  $morecss            		Add more css on link
770	 *  @param  int     $save_lastsearch_value    	-1=Auto, 0=No save of lastsearch_values when clicking, 1=Save lastsearch_values whenclicking
771	 *	@return	string								String with URL
772	 */
773	public function getNomUrl($withpicto = 0, $option = '', $notooltip = 0, $morecss = '', $save_lastsearch_value = -1)
774	{
775		global $db, $conf, $langs, $hookmanager;
776
777		if (!empty($conf->dol_no_mouse_hover)) {
778			$notooltip = 1; // Force disable tooltips
779		}
780
781		$result = '';
782
783		$label = img_picto('', $this->picto).' <u class="paddingrightonly">'.$langs->trans("BillOfMaterials").'</u>';
784		if (isset($this->status)) {
785			$label .= ' '.$this->getLibStatut(5);
786		}
787		$label .= '<br>';
788		$label .= '<b>'.$langs->trans('Ref').':</b> '.$this->ref;
789		if (isset($this->label)) {
790			$label .= '<br><b>'.$langs->trans('Label').':</b> '.$this->label;
791		}
792
793		$url = DOL_URL_ROOT.'/bom/bom_card.php?id='.$this->id;
794
795		if ($option != 'nolink') {
796			// Add param to save lastsearch_values or not
797			$add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
798			if ($save_lastsearch_value == -1 && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
799				$add_save_lastsearch_values = 1;
800			}
801			if ($add_save_lastsearch_values) {
802				$url .= '&save_lastsearch_values=1';
803			}
804		}
805
806		$linkclose = '';
807		if (empty($notooltip)) {
808			if (!empty($conf->global->MAIN_OPTIMIZEFORTEXTBROWSER)) {
809				$label = $langs->trans("ShowBillOfMaterials");
810				$linkclose .= ' alt="'.dol_escape_htmltag($label, 1).'"';
811			}
812			$linkclose .= ' title="'.dol_escape_htmltag($label, 1).'"';
813			$linkclose .= ' class="classfortooltip'.($morecss ? ' '.$morecss : '').'"';
814
815			/*
816			 $hookmanager->initHooks(array('bomdao'));
817			 $parameters=array('id'=>$this->id);
818			 $reshook=$hookmanager->executeHooks('getnomurltooltip',$parameters,$this,$action);    // Note that $action and $object may have been modified by some hooks
819			 if ($reshook > 0) $linkclose = $hookmanager->resPrint;
820			 */
821		} else {
822			$linkclose = ($morecss ? ' class="'.$morecss.'"' : '');
823		}
824
825		$linkstart = '<a href="'.$url.'"';
826		$linkstart .= $linkclose.'>';
827		$linkend = '</a>';
828
829		$result .= $linkstart;
830		if ($withpicto) {
831			$result .= img_object(($notooltip ? '' : $label), ($this->picto ? $this->picto : 'generic'), ($notooltip ? (($withpicto != 2) ? 'class="paddingright"' : '') : 'class="'.(($withpicto != 2) ? 'paddingright ' : '').'classfortooltip"'), 0, 0, $notooltip ? 0 : 1);
832		}
833		if ($withpicto != 2) {
834			$result .= $this->ref;
835		}
836		$result .= $linkend;
837		//if ($withpicto != 2) $result.=(($addlabel && $this->label) ? $sep . dol_trunc($this->label, ($addlabel > 1 ? $addlabel : 0)) : '');
838
839		global $action, $hookmanager;
840		$hookmanager->initHooks(array('bomdao'));
841		$parameters = array('id'=>$this->id, 'getnomurl'=>$result);
842		$reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
843		if ($reshook > 0) {
844			$result = $hookmanager->resPrint;
845		} else {
846			$result .= $hookmanager->resPrint;
847		}
848
849		return $result;
850	}
851
852	/**
853	 *  Return label of the status
854	 *
855	 *  @param  int		$mode          0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=Short label + Picto, 6=Long label + Picto
856	 *  @return	string 			       Label of status
857	 */
858	public function getLibStatut($mode = 0)
859	{
860		return $this->LibStatut($this->status, $mode);
861	}
862
863	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
864	/**
865	 *  Return the status
866	 *
867	 *  @param	int		$status        Id status
868	 *  @param  int		$mode          0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=Short label + Picto, 6=Long label + Picto
869	 *  @return string 			       Label of status
870	 */
871	public function LibStatut($status, $mode = 0)
872	{
873		// phpcs:enable
874		if (empty($this->labelStatus)) {
875			global $langs;
876			//$langs->load("mrp");
877			$this->labelStatus[self::STATUS_DRAFT] = $langs->transnoentitiesnoconv('Draft');
878			$this->labelStatus[self::STATUS_VALIDATED] = $langs->transnoentitiesnoconv('Enabled');
879			$this->labelStatus[self::STATUS_CANCELED] = $langs->transnoentitiesnoconv('Disabled');
880		}
881
882		$statusType = 'status'.$status;
883		if ($status == self::STATUS_VALIDATED) {
884			$statusType = 'status4';
885		}
886		if ($status == self::STATUS_CANCELED) {
887			$statusType = 'status6';
888		}
889
890		return dolGetStatus($this->labelStatus[$status], $this->labelStatus[$status], '', $statusType, $mode);
891	}
892
893	/**
894	 *	Load the info information in the object
895	 *
896	 *	@param  int		$id       Id of object
897	 *	@return	void
898	 */
899	public function info($id)
900	{
901		$sql = 'SELECT rowid, date_creation as datec, tms as datem,';
902		$sql .= ' fk_user_creat, fk_user_modif';
903		$sql .= ' FROM '.MAIN_DB_PREFIX.$this->table_element.' as t';
904		$sql .= ' WHERE t.rowid = '.((int) $id);
905		$result = $this->db->query($sql);
906		if ($result) {
907			if ($this->db->num_rows($result)) {
908				$obj = $this->db->fetch_object($result);
909				$this->id = $obj->rowid;
910				if ($obj->fk_user_author) {
911					$cuser = new User($this->db);
912					$cuser->fetch($obj->fk_user_author);
913					$this->user_creation = $cuser;
914				}
915
916				if ($obj->fk_user_valid) {
917					$vuser = new User($this->db);
918					$vuser->fetch($obj->fk_user_valid);
919					$this->user_validation = $vuser;
920				}
921
922				if ($obj->fk_user_cloture) {
923					$cluser = new User($this->db);
924					$cluser->fetch($obj->fk_user_cloture);
925					$this->user_cloture = $cluser;
926				}
927
928				$this->date_creation     = $this->db->jdate($obj->datec);
929				$this->date_modification = $this->db->jdate($obj->datem);
930				$this->date_validation   = $this->db->jdate($obj->datev);
931			}
932
933			$this->db->free($result);
934		} else {
935			dol_print_error($this->db);
936		}
937	}
938
939	/**
940	 * 	Create an array of lines
941	 *
942	 * 	@return array|int		array of lines if OK, <0 if KO
943	 */
944	public function getLinesArray()
945	{
946		$this->lines = array();
947
948		$objectline = new BOMLine($this->db);
949		$result = $objectline->fetchAll('ASC', 'position', 0, 0, array('customsql'=>'fk_bom = '.((int) $this->id)));
950
951		if (is_numeric($result)) {
952			$this->error = $this->error;
953			$this->errors = $this->errors;
954			return $result;
955		} else {
956			$this->lines = $result;
957			return $this->lines;
958		}
959	}
960
961	/**
962	 *  Create a document onto disk according to template module.
963	 *
964	 *  @param	    string		$modele			Force template to use ('' to not force)
965	 *  @param		Translate	$outputlangs	objet lang a utiliser pour traduction
966	 *  @param      int			$hidedetails    Hide details of lines
967	 *  @param      int			$hidedesc       Hide description
968	 *  @param      int			$hideref        Hide ref
969	 *  @param      null|array  $moreparams     Array to provide more information
970	 *  @return     int         				0 if KO, 1 if OK
971	 */
972	public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0, $moreparams = null)
973	{
974		global $conf, $langs;
975
976		$langs->load("mrp");
977		$outputlangs->load("products");
978
979		if (!dol_strlen($modele)) {
980			$modele = 'standard';
981
982			if ($this->model_pdf) {
983				$modele = $this->model_pdf;
984			} elseif (!empty($conf->global->BOM_ADDON_PDF)) {
985				$modele = $conf->global->BOM_ADDON_PDF;
986			}
987		}
988
989		$modelpath = "core/modules/bom/doc/";
990
991		return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams);
992	}
993
994	/**
995	 * Initialise object with example values
996	 * Id must be 0 if object instance is a specimen
997	 *
998	 * @return void
999	 */
1000	public function initAsSpecimen()
1001	{
1002		$this->initAsSpecimenCommon();
1003		$this->ref = 'BOM-123';
1004		$this->date = $this->date_creation;
1005	}
1006
1007
1008	/**
1009	 * Action executed by scheduler
1010	 * CAN BE A CRON TASK. In such a case, parameters come from the schedule job setup field 'Parameters'
1011	 *
1012	 * @return	int			0 if OK, <>0 if KO (this function is used also by cron so only 0 is OK)
1013	 */
1014	public function doScheduledJob()
1015	{
1016		global $conf, $langs;
1017
1018		//$conf->global->SYSLOG_FILE = 'DOL_DATA_ROOT/dolibarr_mydedicatedlofile.log';
1019
1020		$error = 0;
1021		$this->output = '';
1022		$this->error = '';
1023
1024		dol_syslog(__METHOD__, LOG_DEBUG);
1025
1026		$now = dol_now();
1027
1028		$this->db->begin();
1029
1030		// ...
1031
1032		$this->db->commit();
1033
1034		return $error;
1035	}
1036
1037	/**
1038	 * BOM costs calculation based on cost_price or pmp of each BOM line.
1039	 * Set the property ->total_cost and ->unit_cost of BOM.
1040	 *
1041	 * @return void
1042	 */
1043	public function calculateCosts()
1044	{
1045		include_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
1046		$this->unit_cost = 0;
1047		$this->total_cost = 0;
1048
1049		if (is_array($this->lines) && count($this->lines)) {
1050			require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
1051			$productFournisseur = new ProductFournisseur($this->db);
1052			$tmpproduct = new Product($this->db);
1053
1054			foreach ($this->lines as &$line) {
1055				$tmpproduct->cost_price = 0;
1056				$tmpproduct->pmp = 0;
1057
1058				$result = $tmpproduct->fetch($line->fk_product, '', '', '', 0, 1, 1);	// We discard selling price and language loading
1059				if ($result < 0) {
1060					$this->error = $tmpproduct->error;
1061					return -1;
1062				}
1063				$line->unit_cost = price2num((!empty($tmpproduct->cost_price)) ? $tmpproduct->cost_price : $tmpproduct->pmp);
1064				if (empty($line->unit_cost)) {
1065					if ($productFournisseur->find_min_price_product_fournisseur($line->fk_product) > 0) {
1066						$line->unit_cost = $productFournisseur->fourn_unitprice;
1067					}
1068				}
1069
1070				$line->total_cost = price2num($line->qty * $line->unit_cost, 'MT');
1071
1072				$this->total_cost += $line->total_cost;
1073			}
1074
1075			$this->total_cost = price2num($this->total_cost, 'MT');
1076			if ($this->qty) {
1077				$this->unit_cost = price2num($this->total_cost / $this->qty, 'MU');
1078			}
1079		}
1080	}
1081}
1082
1083
1084/**
1085 * Class for BOMLine
1086 */
1087class BOMLine extends CommonObjectLine
1088{
1089	/**
1090	 * @var string ID to identify managed object
1091	 */
1092	public $element = 'bomline';
1093
1094	/**
1095	 * @var string Name of table without prefix where object is stored
1096	 */
1097	public $table_element = 'bom_bomline';
1098
1099	/**
1100	 * @var int  Does bomline support multicompany module ? 0=No test on entity, 1=Test with field entity, 2=Test with link by societe
1101	 */
1102	public $ismultientitymanaged = 0;
1103
1104	/**
1105	 * @var int  Does bomline support extrafields ? 0=No, 1=Yes
1106	 */
1107	public $isextrafieldmanaged = 1;
1108
1109	/**
1110	 * @var string String with name of icon for bomline. Must be the part after the 'object_' into object_bomline.png
1111	 */
1112	public $picto = 'bomline';
1113
1114
1115	/**
1116	 *  'type' if the field format.
1117	 *  'label' the translation key.
1118	 *  'enabled' is a condition when the field must be managed.
1119	 *  'visible' says if field is visible in list (Examples: 0=Not visible, 1=Visible on list and create/update/view forms, 2=Visible on list only. Using a negative value means field is not shown by default on list but can be selected for viewing)
1120	 *  'notnull' is set to 1 if not null in database. Set to -1 if we must set data to null if empty ('' or 0).
1121	 *  'default' is a default value for creation (can still be replaced by the global setup of default values)
1122	 *  'index' if we want an index in database.
1123	 *  'foreignkey'=>'tablename.field' if the field is a foreign key (it is recommanded to name the field fk_...).
1124	 *  'position' is the sort order of field.
1125	 *  'searchall' is 1 if we want to search in this field when making a search from the quick search button.
1126	 *  'isameasure' must be set to 1 if you want to have a total on list for this field. Field type must be summable like integer or double(24,8).
1127	 *  'css' is the CSS style to use on field. For example: 'maxwidth200'
1128	 *  'help' is a string visible as a tooltip on field
1129	 *  'comment' is not used. You can store here any text of your choice. It is not used by application.
1130	 *  'showoncombobox' if value of the field must be visible into the label of the combobox that list record
1131	 *  'arrayofkeyval' to set list of value if type is a list of predefined values. For example: array("0"=>"Draft","1"=>"Active","-1"=>"Cancel")
1132	 */
1133
1134	// BEGIN MODULEBUILDER PROPERTIES
1135	/**
1136	 * @var array  Array with all fields and their property. Do not use it as a static var. It may be modified by constructor.
1137	 */
1138	public $fields = array(
1139		'rowid' => array('type'=>'integer', 'label'=>'LineID', 'enabled'=>1, 'visible'=>-1, 'position'=>1, 'notnull'=>1, 'index'=>1, 'comment'=>"Id",),
1140		'fk_bom' => array('type'=>'integer:BillOfMaterials:societe/class/bom.class.php', 'label'=>'BillOfMaterials', 'enabled'=>1, 'visible'=>1, 'position'=>10, 'notnull'=>1, 'index'=>1,),
1141		'fk_product' => array('type'=>'integer:Product:product/class/product.class.php', 'label'=>'Product', 'enabled'=>1, 'visible'=>1, 'position'=>20, 'notnull'=>1, 'index'=>1,),
1142		'description' => array('type'=>'text', 'label'=>'Description', 'enabled'=>1, 'visible'=>-1, 'position'=>60, 'notnull'=>-1,),
1143		'qty' => array('type'=>'double(24,8)', 'label'=>'Quantity', 'enabled'=>1, 'visible'=>1, 'position'=>100, 'notnull'=>1, 'isameasure'=>'1',),
1144		'qty_frozen' => array('type'=>'smallint', 'label'=>'QuantityFrozen', 'enabled'=>1, 'visible'=>1, 'default'=>0, 'position'=>105, 'css'=>'maxwidth50imp', 'help'=>'QuantityConsumedInvariable'),
1145		'disable_stock_change' => array('type'=>'smallint', 'label'=>'DisableStockChange', 'enabled'=>1, 'visible'=>1, 'default'=>0, 'position'=>108, 'css'=>'maxwidth50imp', 'help'=>'DisableStockChangeHelp'),
1146		'efficiency' => array('type'=>'double(24,8)', 'label'=>'ManufacturingEfficiency', 'enabled'=>1, 'visible'=>0, 'default'=>1, 'position'=>110, 'notnull'=>1, 'css'=>'maxwidth50imp', 'help'=>'ValueOfEfficiencyConsumedMeans'),
1147		'position' => array('type'=>'integer', 'label'=>'Rank', 'enabled'=>1, 'visible'=>0, 'default'=>0, 'position'=>200, 'notnull'=>1,),
1148		'import_key' => array('type'=>'varchar(14)', 'label'=>'ImportId', 'enabled'=>1, 'visible'=>-2, 'position'=>1000, 'notnull'=>-1,),
1149	);
1150
1151	/**
1152	 * @var int rowid
1153	 */
1154	public $rowid;
1155
1156	/**
1157	 * @var int fk_bom
1158	 */
1159	public $fk_bom;
1160
1161	/**
1162	 * @var int Id of product
1163	 */
1164	public $fk_product;
1165
1166	/**
1167	 * @var string description
1168	 */
1169	public $description;
1170	public $qty;
1171
1172	/**
1173	 * @var int qty frozen
1174	 */
1175	public $qty_frozen;
1176	public $disable_stock_change;
1177	public $efficiency;
1178
1179	/**
1180	 * @var int position of line
1181	 */
1182	public $position;
1183
1184	/**
1185	 * @var string import key
1186	 */
1187	public $import_key;
1188	// END MODULEBUILDER PROPERTIES
1189
1190	/**
1191	 * @var int		Calculated cost for the BOM line
1192	 */
1193	public $total_cost = 0;
1194
1195	/**
1196	 * @var int		Line unit cost based on product cost price or pmp
1197	 */
1198	public $unit_cost = 0;
1199
1200
1201	/**
1202	 * Constructor
1203	 *
1204	 * @param DoliDb $db Database handler
1205	 */
1206	public function __construct(DoliDB $db)
1207	{
1208		global $conf, $langs;
1209
1210		$this->db = $db;
1211
1212		if (empty($conf->global->MAIN_SHOW_TECHNICAL_ID) && isset($this->fields['rowid'])) {
1213			$this->fields['rowid']['visible'] = 0;
1214		}
1215		if (empty($conf->multicompany->enabled) && isset($this->fields['entity'])) {
1216			$this->fields['entity']['enabled'] = 0;
1217		}
1218
1219		// Unset fields that are disabled
1220		foreach ($this->fields as $key => $val) {
1221			if (isset($val['enabled']) && empty($val['enabled'])) {
1222				unset($this->fields[$key]);
1223			}
1224		}
1225
1226		// Translate some data of arrayofkeyval
1227		foreach ($this->fields as $key => $val) {
1228			if (!empty($val['arrayofkeyval']) && is_array($val['arrayofkeyval'])) {
1229				foreach ($val['arrayofkeyval'] as $key2 => $val2) {
1230					$this->fields[$key]['arrayofkeyval'][$key2] = $langs->trans($val2);
1231				}
1232			}
1233		}
1234	}
1235
1236	/**
1237	 * Create object into database
1238	 *
1239	 * @param  User $user      User that creates
1240	 * @param  bool $notrigger false=launch triggers after, true=disable triggers
1241	 * @return int             <0 if KO, Id of created object if OK
1242	 */
1243	public function create(User $user, $notrigger = false)
1244	{
1245		if ($this->efficiency < 0 || $this->efficiency > 1) {
1246			$this->efficiency = 1;
1247		}
1248
1249		return $this->createCommon($user, $notrigger);
1250	}
1251
1252	/**
1253	 * Load object in memory from the database
1254	 *
1255	 * @param int    $id   Id object
1256	 * @param string $ref  Ref
1257	 * @return int         <0 if KO, 0 if not found, >0 if OK
1258	 */
1259	public function fetch($id, $ref = null)
1260	{
1261		$result = $this->fetchCommon($id, $ref);
1262		//if ($result > 0 && ! empty($this->table_element_line)) $this->fetchLines();
1263		return $result;
1264	}
1265
1266	/**
1267	 * Load list of objects in memory from the database.
1268	 *
1269	 * @param  string      $sortorder    Sort Order
1270	 * @param  string      $sortfield    Sort field
1271	 * @param  int         $limit        limit
1272	 * @param  int         $offset       Offset
1273	 * @param  array       $filter       Filter array. Example array('field'=>'valueforlike', 'customurl'=>...)
1274	 * @param  string      $filtermode   Filter mode (AND or OR)
1275	 * @return array|int                 int <0 if KO, array of pages if OK
1276	 */
1277	public function fetchAll($sortorder = '', $sortfield = '', $limit = 0, $offset = 0, array $filter = array(), $filtermode = 'AND')
1278	{
1279		global $conf;
1280
1281		dol_syslog(__METHOD__, LOG_DEBUG);
1282
1283		$records = array();
1284
1285		$sql = 'SELECT ';
1286		$sql .= $this->getFieldList();
1287		$sql .= ' FROM '.MAIN_DB_PREFIX.$this->table_element.' as t';
1288		if ($this->ismultientitymanaged) {
1289			$sql .= ' WHERE t.entity IN ('.getEntity($this->table_element).')';
1290		} else {
1291			$sql .= ' WHERE 1 = 1';
1292		}
1293		// Manage filter
1294		$sqlwhere = array();
1295		if (count($filter) > 0) {
1296			foreach ($filter as $key => $value) {
1297				if ($key == 't.rowid') {
1298					$sqlwhere[] = $key.'='.$value;
1299				} elseif (strpos($key, 'date') !== false) {
1300					$sqlwhere[] = $key.' = \''.$this->db->idate($value).'\'';
1301				} elseif ($key == 'customsql') {
1302					$sqlwhere[] = $value;
1303				} else {
1304					$sqlwhere[] = $key.' LIKE \'%'.$this->db->escape($value).'%\'';
1305				}
1306			}
1307		}
1308		if (count($sqlwhere) > 0) {
1309			$sql .= ' AND ('.implode(' '.$filtermode.' ', $sqlwhere).')';
1310		}
1311
1312		if (!empty($sortfield)) {
1313			$sql .= $this->db->order($sortfield, $sortorder);
1314		}
1315		if (!empty($limit)) {
1316			$sql .= ' '.$this->db->plimit($limit, $offset);
1317		}
1318
1319		$resql = $this->db->query($sql);
1320		if ($resql) {
1321			$num = $this->db->num_rows($resql);
1322
1323			while ($obj = $this->db->fetch_object($resql)) {
1324				$record = new self($this->db);
1325				$record->setVarsFromFetchObj($obj);
1326
1327				$records[$record->id] = $record;
1328			}
1329			$this->db->free($resql);
1330
1331			return $records;
1332		} else {
1333			$this->errors[] = 'Error '.$this->db->lasterror();
1334			dol_syslog(__METHOD__.' '.join(',', $this->errors), LOG_ERR);
1335
1336			return -1;
1337		}
1338	}
1339
1340	/**
1341	 * Update object into database
1342	 *
1343	 * @param  User $user      User that modifies
1344	 * @param  bool $notrigger false=launch triggers after, true=disable triggers
1345	 * @return int             <0 if KO, >0 if OK
1346	 */
1347	public function update(User $user, $notrigger = false)
1348	{
1349		if ($this->efficiency < 0 || $this->efficiency > 1) {
1350			$this->efficiency = 1;
1351		}
1352
1353		return $this->updateCommon($user, $notrigger);
1354	}
1355
1356	/**
1357	 * Delete object in database
1358	 *
1359	 * @param User $user       User that deletes
1360	 * @param bool $notrigger  false=launch triggers after, true=disable triggers
1361	 * @return int             <0 if KO, >0 if OK
1362	 */
1363	public function delete(User $user, $notrigger = false)
1364	{
1365		return $this->deleteCommon($user, $notrigger);
1366		//return $this->deleteCommon($user, $notrigger, 1);
1367	}
1368
1369	/**
1370	 *  Return a link to the object card (with optionaly the picto)
1371	 *
1372	 *	@param	int		$withpicto					Include picto in link (0=No picto, 1=Include picto into link, 2=Only picto)
1373	 *	@param	string	$option						On what the link point to ('nolink', ...)
1374	 *  @param	int  	$notooltip					1=Disable tooltip
1375	 *  @param  string  $morecss            		Add more css on link
1376	 *  @param  int     $save_lastsearch_value    	-1=Auto, 0=No save of lastsearch_values when clicking, 1=Save lastsearch_values whenclicking
1377	 *  @return	string								String with URL
1378	 */
1379	public function getNomUrl($withpicto = 0, $option = '', $notooltip = 0, $morecss = '', $save_lastsearch_value = -1)
1380	{
1381		global $db, $conf, $langs, $hookmanager;
1382
1383		if (!empty($conf->dol_no_mouse_hover)) {
1384			$notooltip = 1; // Force disable tooltips
1385		}
1386
1387		$result = '';
1388
1389		$label = '<u>'.$langs->trans("BillOfMaterialsLine").'</u>';
1390		$label .= '<br>';
1391		$label .= '<b>'.$langs->trans('Ref').':</b> '.$this->ref;
1392
1393		$url = dol_buildpath('/bom/bomline_card.php', 1).'?id='.$this->id;
1394
1395		if ($option != 'nolink') {
1396			// Add param to save lastsearch_values or not
1397			$add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
1398			if ($save_lastsearch_value == -1 && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
1399				$add_save_lastsearch_values = 1;
1400			}
1401			if ($add_save_lastsearch_values) {
1402				$url .= '&save_lastsearch_values=1';
1403			}
1404		}
1405
1406		$linkclose = '';
1407		if (empty($notooltip)) {
1408			if (!empty($conf->global->MAIN_OPTIMIZEFORTEXTBROWSER)) {
1409				$label = $langs->trans("ShowBillOfMaterialsLine");
1410				$linkclose .= ' alt="'.dol_escape_htmltag($label, 1).'"';
1411			}
1412			$linkclose .= ' title="'.dol_escape_htmltag($label, 1).'"';
1413			$linkclose .= ' class="classfortooltip'.($morecss ? ' '.$morecss : '').'"';
1414
1415			/*
1416			 $hookmanager->initHooks(array('bomlinedao'));
1417			 $parameters=array('id'=>$this->id);
1418			 $reshook=$hookmanager->executeHooks('getnomurltooltip',$parameters,$this,$action);    // Note that $action and $object may have been modified by some hooks
1419			 if ($reshook > 0) $linkclose = $hookmanager->resPrint;
1420			 */
1421		} else {
1422			$linkclose = ($morecss ? ' class="'.$morecss.'"' : '');
1423		}
1424
1425		$linkstart = '<a href="'.$url.'"';
1426		$linkstart .= $linkclose.'>';
1427		$linkend = '</a>';
1428
1429		$result .= $linkstart;
1430		if ($withpicto) {
1431			$result .= img_object(($notooltip ? '' : $label), ($this->picto ? $this->picto : 'generic'), ($notooltip ? (($withpicto != 2) ? 'class="paddingright"' : '') : 'class="'.(($withpicto != 2) ? 'paddingright ' : '').'classfortooltip"'), 0, 0, $notooltip ? 0 : 1);
1432		}
1433		if ($withpicto != 2) {
1434			$result .= $this->ref;
1435		}
1436		$result .= $linkend;
1437		//if ($withpicto != 2) $result.=(($addlabel && $this->label) ? $sep . dol_trunc($this->label, ($addlabel > 1 ? $addlabel : 0)) : '');
1438
1439		global $action, $hookmanager;
1440		$hookmanager->initHooks(array('bomlinedao'));
1441		$parameters = array('id'=>$this->id, 'getnomurl'=>$result);
1442		$reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
1443		if ($reshook > 0) {
1444			$result = $hookmanager->resPrint;
1445		} else {
1446			$result .= $hookmanager->resPrint;
1447		}
1448
1449		return $result;
1450	}
1451
1452	/**
1453	 *  Return label of the status
1454	 *
1455	 *  @param  int		$mode          0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=Short label + Picto, 6=Long label + Picto
1456	 *  @return	string 			       Label of status
1457	 */
1458	public function getLibStatut($mode = 0)
1459	{
1460		return $this->LibStatut($this->status, $mode);
1461	}
1462
1463	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1464	/**
1465	 *  Return the status
1466	 *
1467	 *  @param	int		$status        Id status
1468	 *  @param  int		$mode          0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=Short label + Picto, 6=Long label + Picto
1469	 *  @return string 			       Label of status
1470	 */
1471	public function LibStatut($status, $mode = 0)
1472	{
1473		// phpcs:enable
1474		return '';
1475	}
1476
1477	/**
1478	 *	Load the info information in the object
1479	 *
1480	 *	@param  int		$id       Id of object
1481	 *	@return	void
1482	 */
1483	public function info($id)
1484	{
1485		$sql = 'SELECT rowid, date_creation as datec, tms as datem,';
1486		$sql .= ' fk_user_creat, fk_user_modif';
1487		$sql .= ' FROM '.MAIN_DB_PREFIX.$this->table_element.' as t';
1488		$sql .= ' WHERE t.rowid = '.((int) $id);
1489		$result = $this->db->query($sql);
1490		if ($result) {
1491			if ($this->db->num_rows($result)) {
1492				$obj = $this->db->fetch_object($result);
1493				$this->id = $obj->rowid;
1494				if ($obj->fk_user_author) {
1495					$cuser = new User($this->db);
1496					$cuser->fetch($obj->fk_user_author);
1497					$this->user_creation = $cuser;
1498				}
1499
1500				if ($obj->fk_user_valid) {
1501					$vuser = new User($this->db);
1502					$vuser->fetch($obj->fk_user_valid);
1503					$this->user_validation = $vuser;
1504				}
1505
1506				if ($obj->fk_user_cloture) {
1507					$cluser = new User($this->db);
1508					$cluser->fetch($obj->fk_user_cloture);
1509					$this->user_cloture = $cluser;
1510				}
1511
1512				$this->date_creation     = $this->db->jdate($obj->datec);
1513				$this->date_modification = $this->db->jdate($obj->datem);
1514				$this->date_validation   = $this->db->jdate($obj->datev);
1515			}
1516
1517			$this->db->free($result);
1518		} else {
1519			dol_print_error($this->db);
1520		}
1521	}
1522
1523	/**
1524	 * Initialise object with example values
1525	 * Id must be 0 if object instance is a specimen
1526	 *
1527	 * @return void
1528	 */
1529	public function initAsSpecimen()
1530	{
1531		$this->initAsSpecimenCommon();
1532	}
1533}
1534