dolibarr 23.0.4
bom.class.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2019 Laurent Destailleur <eldy@users.sourceforge.net>
3 * Copyright (C) 2023 Benjamin Falière <benjamin.faliere@altairis.fr>
4 * Copyright (C) 2023 Charlene Benke <charlene@patas-monkey.com>
5 * Copyright (C) 2024-2025 Frédéric France <frederic.france@free.fr>
6 * Copyright (C) 2024-2025 MDW <mdeweerd@users.noreply.github.com>
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see <https://www.gnu.org/licenses/>.
20 */
21
28// Put here all includes required by your class file
29require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
30require_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php';
31require_once DOL_DOCUMENT_ROOT.'/bom/class/bomline.class.php';
32
33if (isModEnabled('workstation')) {
34 require_once DOL_DOCUMENT_ROOT.'/workstation/class/workstation.class.php';
35}
36
37
41class BOM extends CommonObject
42{
46 public $module = 'bom';
47
51 public $element = 'bom';
52
57 public $TRIGGER_PREFIX = 'BOM';
58
62 public $table_element = 'bom_bom';
63
67 public $picto = 'bom';
68
72 public $product;
73
74
75 const STATUS_DRAFT = 0;
76 const STATUS_VALIDATED = 1;
77 const STATUS_CANCELED = 9;
78
79
106 // BEGIN MODULEBUILDER PROPERTIES
110 public $fields = array(
111 'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'enabled' => 1, 'visible' => -2, 'position' => 1, 'notnull' => 1, 'index' => 1, 'comment' => "Id",),
112 'entity' => array('type' => 'integer', 'label' => 'Entity', 'enabled' => 1, 'visible' => 0, 'notnull' => 1, 'default' => '1', 'index' => 1, 'position' => 5),
113 '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, 'csslist' => 'nowraponall'),
114 'label' => array('type' => 'varchar(255)', 'label' => 'Label', 'enabled' => 1, 'visible' => 1, 'position' => 30, 'notnull' => 1, 'searchall' => 1, 'showoncombobox' => 2, 'autofocusoncreate' => 1, 'css' => 'minwidth300 maxwidth400', 'csslist' => 'tdoverflowmax200'),
115 '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'),
116 //'bomtype' => array('type'=>'integer', 'label'=>'Type', 'enabled'=>1, 'visible'=>-1, 'position'=>32, 'notnull'=>1, 'default'=>'0', 'arrayofkeyval'=>array(0=>'Manufacturing')),
117 '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'),
118 'description' => array('type' => 'text', 'label' => 'Description', 'enabled' => 1, 'visible' => -1, 'position' => 60, 'notnull' => -1,),
119 'qty' => array('type' => 'real', 'label' => 'Quantity', 'enabled' => 1, 'visible' => 1, 'default' => '1', 'position' => 55, 'notnull' => 1, 'isameasure' => 1, 'css' => 'maxwidth50imp right'),
120 //'efficiency' => array('type'=>'real', 'label'=>'ManufacturingEfficiency', 'enabled'=>1, 'visible'=>-1, 'default'=>'1', 'position'=>100, 'notnull'=>0, 'css'=>'maxwidth50imp', 'help'=>'ValueOfMeansLossForProductProduced'),
121 'duration' => array('type' => 'duration', 'label' => 'EstimatedDuration', 'enabled' => 1, 'visible' => -1, 'position' => 101, 'notnull' => -1, 'css' => 'maxwidth50imp', 'help' => 'EstimatedDurationDesc'),
122 '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'),
123 'note_public' => array('type' => 'html', 'label' => 'NotePublic', 'enabled' => 1, 'visible' => -2, 'position' => 161, 'notnull' => -1,),
124 'note_private' => array('type' => 'html', 'label' => 'NotePrivate', 'enabled' => 1, 'visible' => -2, 'position' => 162, 'notnull' => -1,),
125 'date_creation' => array('type' => 'datetime', 'label' => 'DateCreation', 'enabled' => 1, 'visible' => -2, 'position' => 300, 'notnull' => 1,),
126 'tms' => array('type' => 'timestamp', 'label' => 'DateModification', 'enabled' => 1, 'visible' => -2, 'position' => 501, 'notnull' => 1,),
127 'date_valid' => array('type' => 'datetime', 'label' => 'DateValidation', 'enabled' => 1, 'visible' => -2, 'position' => 502, 'notnull' => 0,),
128 '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'),
129 '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'),
130 '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'),
131 'import_key' => array('type' => 'varchar(14)', 'label' => 'ImportId', 'enabled' => 1, 'visible' => -2, 'position' => 1000, 'notnull' => -1,),
132 'model_pdf' => array('type' => 'varchar(255)', 'label' => 'Model pdf', 'enabled' => 1, 'visible' => 0, 'position' => 1010),
133 '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')),
134 );
135
139 public $rowid;
140
144 public $ref;
145
149 public $label;
150
154 public $bomtype;
155
159 public $description;
160
164 public $date_valid;
165
169 public $fk_user_creat;
170
174 public $fk_user_modif;
175
179 public $fk_user_valid;
180
184 public $fk_warehouse;
185
189 public $import_key;
190
194 public $status;
195
199 public $fk_product;
200
204 public $qty;
205
209 public $duration;
210
214 public $efficiency;
215 // END MODULEBUILDER PROPERTIES
216
217
218 // If this object has a subtable with lines
219
223 public $table_element_line = 'bom_bomline';
224
228 public $fk_element = 'fk_bom';
229
233 public $class_element_line = 'BOMLine';
234
235 // /**
236 // * @var array List of child tables. To test if we can delete object.
237 // */
238 // protected $childtables=array();
239
243 protected $childtablesoncascade = array('bom_bomline');
244
248 public $lines = array();
249
253 public $total_cost = 0;
254
258 public $unit_cost = 0;
259
260
266 public function __construct(DoliDB $db)
267 {
268 global $langs;
269
270 $this->db = $db;
271
272 $this->ismultientitymanaged = 1;
273 $this->isextrafieldmanaged = 1;
274
275 if (!getDolGlobalString('MAIN_SHOW_TECHNICAL_ID') && isset($this->fields['rowid'])) {
276 $this->fields['rowid']['visible'] = 0;
277 }
278 if (!isModEnabled('multicompany') && isset($this->fields['entity'])) {
279 $this->fields['entity']['enabled'] = 0;
280 }
281
282 // Unset fields that are disabled
283 foreach ($this->fields as $key => $val) {
284 if (isset($val['enabled']) && empty($val['enabled'])) {
285 unset($this->fields[$key]);
286 }
287 }
288
289 // Translate some data of arrayofkeyval
290 foreach ($this->fields as $key => $val) {
291 if (!empty($val['arrayofkeyval']) && is_array($val['arrayofkeyval'])) {
292 foreach ($val['arrayofkeyval'] as $key2 => $val2) {
293 $this->fields[$key]['arrayofkeyval'][$key2] = $langs->trans($val2);
294 }
295 }
296 }
297 }
298
306 public function create(User $user, $notrigger = 0)
307 {
308 if ($this->efficiency <= 0 || $this->efficiency > 1) {
309 $this->efficiency = 1;
310 }
311
312 return $this->createCommon($user, $notrigger);
313 }
314
322 public function createFromClone(User $user, $fromid)
323 {
324 global $langs, $extrafields;
325 $error = 0;
326
327 dol_syslog(__METHOD__, LOG_DEBUG);
328
329 $object = new self($this->db);
330
331 $this->db->begin();
332
333 // Load source object
334 $result = $object->fetchCommon($fromid);
335 if ($result > 0 && !empty($object->table_element_line)) {
336 $object->fetchLines();
337 }
338
339 // Get lines so they will be clone
340 //foreach ($object->lines as $line)
341 // $line->fetch_optionals();
342
343 // Reset some properties
344 unset($object->id);
345 unset($object->fk_user_creat);
346 unset($object->import_key);
347 unset($object->date_creation);
348 // Clear fields
349 $default_ref = $this->fields['ref']['default'] ?? null;
350 $object->ref = empty($default_ref) ? $langs->trans("copy_of_").$object->ref : $default_ref;
351 // @phan-suppress-next-line PhanTypeInvalidDimOffset
352 $object->label = empty($this->fields['label']['default']) ? $langs->trans("CopyOf")." ".$object->label : $this->fields['label']['default'];
353 $object->status = self::STATUS_DRAFT;
354 // ...
355 // Clear extrafields that are unique
356 if (is_array($object->array_options) && count($object->array_options) > 0) {
357 $extrafields->fetch_name_optionals_label($object->table_element);
358 foreach ($object->array_options as $key => $option) {
359 $shortkey = preg_replace('/options_/', '', $key);
360 if (!empty($extrafields->attributes[$this->element]['unique'][$shortkey])) {
361 //var_dump($key); var_dump($clonedObj->array_options[$key]); exit;
362 unset($object->array_options[$key]);
363 }
364 }
365 }
366
367 // Create clone
368 $object->context['createfromclone'] = 'createfromclone';
369 $result = $object->createCommon($user);
370 if ($result < 0) {
371 $error++;
373 }
374
375 if (!$error) {
376 // copy internal contacts
377 if ($this->copy_linked_contact($object, 'internal') < 0) {
378 $error++;
379 }
380 }
381
382 if (!$error) {
383 // copy external contacts if same company
384 // @phan-suppress-next-line PhanUndeclaredProperty
385 if (property_exists($this, 'socid') && $this->socid == $object->socid) {
386 if ($this->copy_linked_contact($object, 'external') < 0) {
387 $error++;
388 }
389 }
390 }
391
392 // If there is lines, create lines too from ->lines
393 // TODO
394
395 unset($object->context['createfromclone']);
396
397 // End
398 if (!$error) {
399 $this->db->commit();
400 return $object;
401 } else {
402 $this->db->rollback();
403 return -1;
404 }
405 }
406
414 public function fetch($id, $ref = null)
415 {
416 $result = $this->fetchCommon($id, $ref);
417
418 if ($result > 0 && !empty($this->table_element_line)) {
419 $this->fetchLines();
420 }
421 //$this->calculateCosts(); // This consume a high number of subrequests. Do not call it into fetch but when you need it.
422
423 return $result;
424 }
425
431 public function fetchLines()
432 {
433 $this->lines = array();
434
435 $result = $this->fetchLinesCommon();
436 return $result;
437 }
438
445 public function fetchLinesbytypeproduct($typeproduct = 0)
446 {
447 $this->lines = array();
448
449 $objectlineclassname = $this->class_element_line;
450 if (!class_exists($objectlineclassname)) {
451 $this->error = 'Error, class BOMLine not found during call of fetchLinesCommon';
452 return -1;
453 }
454
455 $objectline = new BOMLine($this->db);
456
457 '@phan-var-force BOMLine $objectline';
458
459 $sql = "SELECT ".$objectline->getFieldList('l');
460 $sql .= " FROM ".$this->db->prefix().$objectline->table_element." as l";
461 $sql .= " LEFT JOIN ".$this->db->prefix()."product as p ON p.rowid = l.fk_product";
462 $sql .= " WHERE l.fk_".$this->db->escape($this->element)." = ".((int) $this->id);
463 $sql .= " AND p.fk_product_type = ". ((int) $typeproduct);
464 if (isset($objectline->fields['position'])) {
465 $sql .= $this->db->order('position', 'ASC');
466 }
467
468 $resql = $this->db->query($sql);
469 if ($resql) {
470 $num_rows = $this->db->num_rows($resql);
471 $i = 0;
472 while ($i < $num_rows) {
473 $obj = $this->db->fetch_object($resql);
474 if ($obj) {
475 $newline = new BOMLine($this->db);
476 '@phan-var-force BOMLine $newline';
477 $newline->setVarsFromFetchObj($obj);
478
479 // Load also extrafields for the line
480 //if (empty($noextrafields)) {
481 $newline->fetch_optionals();
482 //}
483
484 $this->lines[] = $newline;
485 }
486 $i++;
487 }
488
489 return $num_rows;
490 } else {
491 $this->error = $this->db->lasterror();
492 $this->errors[] = $this->error;
493 return -1;
494 }
495 }
496
497
509 public function fetchAll($sortorder = '', $sortfield = '', $limit = 0, $offset = 0, $filter = '', $filtermode = 'AND')
510 {
511 dol_syslog(__METHOD__, LOG_DEBUG);
512
513 $records = array();
514
515 $sql = 'SELECT ';
516 $sql .= $this->getFieldList();
517 $sql .= ' FROM '.MAIN_DB_PREFIX.$this->table_element.' as t';
518 if ($this->ismultientitymanaged) {
519 $sql .= ' WHERE t.entity IN ('.getEntity($this->element).')';
520 } else {
521 $sql .= ' WHERE 1 = 1';
522 }
523
524 // Manage filter
525 $errormessage = '';
526 $sql .= forgeSQLFromUniversalSearchCriteria($filter, $errormessage);
527 if ($errormessage) {
528 $this->errors[] = $errormessage;
529 dol_syslog(__METHOD__.' '.implode(',', $this->errors), LOG_ERR);
530 return -1;
531 }
532
533 if (!empty($sortfield)) {
534 $sql .= $this->db->order($sortfield, $sortorder);
535 }
536 if (!empty($limit)) {
537 $sql .= $this->db->plimit($limit, $offset);
538 }
539
540 $resql = $this->db->query($sql);
541 if ($resql) {
542 $num = $this->db->num_rows($resql);
543
544 while ($obj = $this->db->fetch_object($resql)) {
545 $record = new self($this->db);
546 $record->setVarsFromFetchObj($obj);
547
548 $records[$record->id] = $record;
549 }
550 $this->db->free($resql);
551
552 return $records;
553 } else {
554 $this->errors[] = 'Error '.$this->db->lasterror();
555 dol_syslog(__METHOD__.' '.implode(',', $this->errors), LOG_ERR);
556
557 return -1;
558 }
559 }
560
568 public function update(User $user, $notrigger = 0)
569 {
570 if ($this->efficiency <= 0 || $this->efficiency > 1) {
571 $this->efficiency = 1;
572 }
573
574 return $this->updateCommon($user, $notrigger);
575 }
576
584 public function delete(User $user, $notrigger = 0)
585 {
586 return $this->deleteCommon($user, $notrigger);
587 //return $this->deleteCommon($user, $notrigger, 1);
588 }
589
606 public function addLine($fk_product, $qty, $qty_frozen = 0, $disable_stock_change = 0, $efficiency = 1.0, $position = -1, $fk_bom_child = null, $import_key = null, $fk_unit = 0, $array_options = array(), $fk_default_workstation = null)
607 {
608 global $user;
609
610 $logtext = "::addLine bomid=$this->id, qty=$qty, fk_product=$fk_product, qty_frozen=$qty_frozen, disable_stock_change=$disable_stock_change, efficiency=$efficiency";
611 $logtext .= ", fk_bom_child=$fk_bom_child, import_key=$import_key";
612 dol_syslog(get_class($this).$logtext, LOG_DEBUG);
613
614 if ($this->status == self::STATUS_DRAFT) {
615 include_once DOL_DOCUMENT_ROOT.'/core/lib/price.lib.php';
616
617 // Clean parameters
618 if (empty($qty)) {
619 $qty = 0;
620 }
621 if (empty($qty_frozen)) {
622 $qty_frozen = 0;
623 }
624 if (empty($disable_stock_change)) {
625 $disable_stock_change = 0;
626 }
627 if (empty($efficiency)) {
628 $efficiency = 1.0;
629 }
630 if (empty($fk_bom_child)) {
631 $fk_bom_child = null;
632 }
633 if (empty($import_key)) {
634 $import_key = '';
635 }
636 if (empty($position)) {
637 $position = -1;
638 }
639
640 $qty = (float) price2num($qty);
641 $efficiency = (float) price2num($efficiency);
642 $position = (float) price2num($position);
643
644 $this->db->begin();
645
646 // Rank to use
647 $rangMax = $this->line_max();
648 $rankToUse = $position;
649 if ($rankToUse <= 0 or $rankToUse > $rangMax) { // New line after existing lines
650 $rankToUse = $rangMax + 1;
651 } else { // New line between the existing lines
652 foreach ($this->lines as $bl) {
653 if ($bl->position >= $rankToUse) {
654 $bl->position++;
655 $bl->update($user);
656 }
657 }
658 }
659
660 // Insert line
661 $line = new BOMLine($this->db);
662
663 $line->context = $this->context;
664
665 $line->fk_bom = $this->id;
666 $line->fk_product = $fk_product;
667 $line->qty = $qty;
668 $line->qty_frozen = $qty_frozen;
669 $line->disable_stock_change = $disable_stock_change;
670 $line->efficiency = $efficiency;
671 $line->fk_bom_child = $fk_bom_child;
672 $line->import_key = $import_key;
673 $line->position = $rankToUse;
674 $line->fk_unit = $fk_unit;
675 $line->fk_default_workstation = $fk_default_workstation;
676
677 if (is_array($array_options) && count($array_options) > 0) {
678 $line->array_options = $array_options;
679 }
680
681 $result = $line->create($user);
682
683 if ($result > 0) {
684 $this->calculateCosts();
685 $this->db->commit();
686 return $result;
687 } else {
688 $this->setErrorsFromObject($line);
689 dol_syslog(get_class($this)."::addLine error=".$this->error, LOG_ERR);
690 $this->db->rollback();
691 return -2;
692 }
693 } else {
694 dol_syslog(get_class($this)."::addLine status of BOM must be Draft to allow use of ->addLine()", LOG_ERR);
695 return -3;
696 }
697 }
698
714 public function updateLine($rowid, $qty, $qty_frozen = 0, $disable_stock_change = 0, $efficiency = 1.0, $position = -1, $import_key = null, $fk_unit = 0, $array_options = array(), $fk_default_workstation = null)
715 {
716 global $user;
717
718 if (!$this->isLineOfObject($rowid)) {
719 $this->error = 'ErrorLineIDDoesNotMatchWithObjectID';
720 return -1;
721 }
722
723 $logtext = "::updateLine bomid=$this->id, qty=$qty, qty_frozen=$qty_frozen, disable_stock_change=$disable_stock_change, efficiency=$efficiency";
724 $logtext .= ", import_key=$import_key";
725 dol_syslog(get_class($this).$logtext, LOG_DEBUG);
726
727 if ($this->status == self::STATUS_DRAFT) {
728 include_once DOL_DOCUMENT_ROOT.'/core/lib/price.lib.php';
729
730 // Clean parameters
731 if (empty($qty)) {
732 $qty = 0;
733 }
734 if (empty($qty_frozen)) {
735 $qty_frozen = 0;
736 }
737 if (empty($disable_stock_change)) {
738 $disable_stock_change = 0;
739 }
740 if (empty($efficiency)) {
741 $efficiency = 1.0;
742 }
743 if (empty($import_key)) {
744 $import_key = '';
745 }
746 if (empty($position)) {
747 $position = -1;
748 }
749
750 $qty = (float) price2num($qty);
751 $efficiency = (float) price2num($efficiency);
752 $position = (float) price2num($position);
753
754 $this->db->begin();
755
756 // Fetch current line from the database and then clone the object and set it in $oldline property
757 $line = new BOMLine($this->db);
758 $line->fetch($rowid);
759 $line->fetch_optionals();
760
761 $staticLine = clone $line;
762 $line->oldcopy = $staticLine;
763 $line->context = $this->context;
764
765 // Rank to use
766 $rankToUse = (int) $position;
767 if ($rankToUse != $line->oldcopy->position) { // check if position have a new value
768 foreach ($this->lines as $bl) {
769 if ($bl->position >= $rankToUse and $bl->position < ($line->oldcopy->position + 1)) { // move rank up
770 $bl->position++;
771 $bl->update($user);
772 }
773 if ($bl->position <= $rankToUse and $bl->position > ($line->oldcopy->position)) { // move rank down
774 $bl->position--;
775 $bl->update($user);
776 }
777 }
778 }
779
780
781 $line->fk_bom = $this->id;
782 $line->qty = $qty;
783 $line->qty_frozen = $qty_frozen;
784 $line->disable_stock_change = $disable_stock_change;
785 $line->efficiency = $efficiency;
786 $line->import_key = $import_key;
787 $line->position = $rankToUse;
788
789
790 if (!empty($fk_unit)) {
791 $line->fk_unit = $fk_unit;
792 }
793
794
795 if (is_array($array_options) && count($array_options) > 0) {
796 // We replace values in this->line->array_options only for entries defined into $array_options
797 foreach ($array_options as $key => $value) {
798 $line->array_options[$key] = $array_options[$key];
799 }
800 }
801 if ($line->fk_default_workstation != $fk_default_workstation) {
802 $line->fk_default_workstation = ($fk_default_workstation > 0 ? $fk_default_workstation : 0);
803 }
804
805 $result = $line->update($user);
806
807 if ($result > 0) {
808 $this->calculateCosts();
809 $this->db->commit();
810 return $result;
811 } else {
812 $this->setErrorsFromObject($line);
813 dol_syslog(get_class($this)."::addLine error=".$this->error, LOG_ERR);
814 $this->db->rollback();
815 return -2;
816 }
817 } else {
818 dol_syslog(get_class($this)."::addLine status of BOM must be Draft to allow use of ->addLine()", LOG_ERR);
819 return -3;
820 }
821 }
822
831 public function deleteLine(User $user, $idline, $notrigger = 0)
832 {
833 if ($this->status < 0) {
834 $this->error = 'ErrorDeleteLineNotAllowedByObjectStatus';
835 return -2;
836 }
837
838 // Fetch current line from the database and then clone the object and set it in $oldline property
839 $line = new BOMLine($this->db);
840 $line->fetch($idline);
841 if ($this->id > 0 && (int) $line->fk_bom !== (int) $this->id) {
842 $this->error = 'ErrorLineIDDoesNotMatchWithObjectID';
843 return -1;
844 }
845
846 $this->db->begin();
847 $line->fetch_optionals();
848
849 $staticLine = clone $line;
850 $line->oldcopy = $staticLine;
851 $line->context = $this->context;
852
853 $result = $line->delete($user, $notrigger);
854
855 //Positions (rank) reordering
856 foreach ($this->lines as $bl) {
857 if ($bl->position > ($line->oldcopy->position)) { // move rank down
858 $bl->position--;
859 $bl->update($user);
860 }
861 }
862
863 if ($result > 0) {
864 $this->calculateCosts();
865 $this->db->commit();
866 return $result;
867 } else {
868 $this->setErrorsFromObject($line);
869 dol_syslog(get_class($this)."::addLine error=".$this->error, LOG_ERR);
870 $this->db->rollback();
871 return -2;
872 }
873 }
874
882 public function getNextNumRef($prod)
883 {
884 global $langs, $conf;
885 $langs->load("mrp");
886
887 if (getDolGlobalString('BOM_ADDON')) {
888 $mybool = false;
889
890 $file = getDolGlobalString('BOM_ADDON') . ".php";
891 $classname = getDolGlobalString('BOM_ADDON');
892
893 // Include file with class
894 $dirmodels = array_merge(array('/'), (array) $conf->modules_parts['models']);
895 foreach ($dirmodels as $reldir) {
896 $dir = dol_buildpath($reldir."core/modules/bom/");
897
898 // Load file with numbering class (if found)
899 $mybool = ((bool) @include_once $dir.$file) || $mybool;
900 }
901
902 if (!$mybool) {
903 dol_print_error(null, "Failed to include file ".$file);
904 return '';
905 }
906
907 $obj = new $classname();
908 '@phan-var-force ModeleNumRefBoms $obj';
910 $numref = $obj->getNextValue($prod, $this);
911
912 if ($numref != "") {
913 return $numref;
914 } else {
915 $this->error = $obj->error;
916 //dol_print_error($this->db,get_class($this)."::getNextNumRef ".$obj->error);
917 return "";
918 }
919 } else {
920 print $langs->trans("Error")." ".$langs->trans("Error_BOM_ADDON_NotDefined");
921 return "";
922 }
923 }
924
932 public function validate($user, $notrigger = 0)
933 {
934 global $conf;
935
936 require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
937
938 $error = 0;
939
940 // Protection
941 if ($this->status == self::STATUS_VALIDATED) {
942 dol_syslog(get_class($this)."::validate action abandoned: already validated", LOG_WARNING);
943 return 0;
944 }
945
946 $now = dol_now();
947
948 $this->db->begin();
949
950 // Define new ref
951 if (preg_match('/^[\‍(]?PROV/i', $this->ref) || empty($this->ref)) { // empty should not happened, but when it occurs, the test save life
952 $res = $this->fetch_product();
953 if ($res < 0 || !is_object($this->product)) {
954 $this->db->rollback();
955 return -1;
956 }
957 $num = $this->getNextNumRef($this->product);
958 } else {
959 $num = (string) $this->ref;
960 }
961 $this->newref = dol_sanitizeFileName($num);
962
963 // Validate
964 $sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element;
965 $sql .= " SET ref = '".$this->db->escape($num)."',";
966 $sql .= " status = ".self::STATUS_VALIDATED.",";
967 $sql .= " date_valid='".$this->db->idate($now)."',";
968 $sql .= " fk_user_valid = ".((int) $user->id);
969 $sql .= " WHERE rowid = ".((int) $this->id);
970
971 dol_syslog(get_class($this)."::validate()", LOG_DEBUG);
972 $resql = $this->db->query($sql);
973 if (!$resql) {
974 dol_print_error($this->db);
975 $this->error = $this->db->lasterror();
976 $error++;
977 }
978
979 if (!$error && !$notrigger) {
980 // Call trigger
981 $result = $this->call_trigger('BOM_VALIDATE', $user);
982 if ($result < 0) {
983 $error++;
984 }
985 // End call triggers
986 }
987
988 if (!$error) {
989 $this->oldref = $this->ref;
990
991 // Rename directory if dir was a temporary ref
992 if (preg_match('/^[\‍(]?PROV/i', $this->ref)) {
993 // Now we rename also files into index
994 $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)."'";
995 $sql .= " WHERE filename LIKE '".$this->db->escape($this->ref)."%' AND filepath = 'bom/".$this->db->escape($this->ref)."' and entity = ".$conf->entity;
996 $resql = $this->db->query($sql);
997 if (!$resql) {
998 $error++;
999 $this->error = $this->db->lasterror();
1000 }
1001 $sql = 'UPDATE '.MAIN_DB_PREFIX."ecm_files set filepath = 'bom/".$this->db->escape($this->newref)."'";
1002 $sql .= " WHERE filepath = 'bom/".$this->db->escape($this->ref)."' and entity = ".$conf->entity;
1003 $resql = $this->db->query($sql);
1004 if (!$resql) {
1005 $error++;
1006 $this->error = $this->db->lasterror();
1007 }
1008
1009 // We rename directory ($this->ref = old ref, $num = new ref) in order not to lose the attachments
1010 $oldref = dol_sanitizeFileName($this->ref);
1011 $newref = dol_sanitizeFileName($num);
1012 $dirsource = getMultidirOutput($this) . '/'.$oldref;
1013 $dirdest = getMultidirOutput($this) . '/'.$newref;
1014 if (!$error && file_exists($dirsource)) {
1015 dol_syslog(get_class($this)."::validate() rename dir ".$dirsource." into ".$dirdest);
1016
1017 if (@rename($dirsource, $dirdest)) {
1018 dol_syslog("Rename ok");
1019 // Rename docs starting with $oldref with $newref
1020 $listoffiles = dol_dir_list(getMultidirOutput($this) . '/'.$newref, 'files', 1, '^'.preg_quote($oldref, '/'));
1021 foreach ($listoffiles as $fileentry) {
1022 $dirsource = $fileentry['name'];
1023 $dirdest = preg_replace('/^'.preg_quote($oldref, '/').'/', $newref, $dirsource);
1024 $dirsource = $fileentry['path'].'/'.$dirsource;
1025 $dirdest = $fileentry['path'].'/'.$dirdest;
1026 @rename($dirsource, $dirdest);
1027 }
1028 }
1029 }
1030 }
1031 }
1032
1033 // Set new ref and current status
1034 if (!$error) {
1035 $this->ref = $num;
1036 $this->status = self::STATUS_VALIDATED;
1037 }
1038
1039 if (!$error) {
1040 $this->db->commit();
1041 return 1;
1042 } else {
1043 $this->db->rollback();
1044 return -1;
1045 }
1046 }
1047
1055 public function setDraft($user, $notrigger = 0)
1056 {
1057 // Protection
1058 if ($this->status <= self::STATUS_DRAFT) {
1059 return 0;
1060 }
1061
1062 return $this->setStatusCommon($user, self::STATUS_DRAFT, $notrigger, 'BOM_UNVALIDATE');
1063 }
1064
1072 public function cancel($user, $notrigger = 0)
1073 {
1074 // Protection
1075 if ($this->status != self::STATUS_VALIDATED) {
1076 return 0;
1077 }
1078
1079 return $this->setStatusCommon($user, self::STATUS_CANCELED, $notrigger, 'BOM_CLOSE');
1080 }
1081
1089 public function reopen($user, $notrigger = 0)
1090 {
1091 // Protection
1092 if ($this->status != self::STATUS_CANCELED) {
1093 return 0;
1094 }
1095
1096 return $this->setStatusCommon($user, self::STATUS_VALIDATED, $notrigger, 'BOM_REOPEN');
1097 }
1098
1105 public function getTooltipContentArray($params)
1106 {
1107 global $langs;
1108
1109 $langs->loadLangs(['product', 'mrp']);
1110
1111 $datas = [];
1112
1113 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
1114 return ['optimize' => $langs->trans("ShowBillOfMaterials")];
1115 }
1116 $picto = img_picto('', $this->picto).' <u class="paddingrightonly">'.$langs->trans("BillOfMaterials").'</u>';
1117 if (isset($this->status)) {
1118 $picto .= ' '.$this->getLibStatut(5);
1119 }
1120 $datas['picto'] = $picto;
1121 $datas['ref'] = '<br><b>'.$langs->trans('Ref').':</b> '.$this->ref;
1122 if (isset($this->label)) {
1123 $datas['label'] = '<br><b>'.$langs->trans('Label').':</b> '.$this->label;
1124 }
1125 if (!empty($this->fk_product) && $this->fk_product > 0) {
1126 include_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
1127 $product = new Product($this->db);
1128 $resultFetch = $product->fetch($this->fk_product);
1129 if ($resultFetch > 0) {
1130 $datas['product'] = "<br><b>".$langs->trans("Product").'</b>: '.$product->ref.' - '.$product->label;
1131 }
1132 }
1133
1134 return $datas;
1135 }
1136
1147 public function getNomUrl($withpicto = 0, $option = '', $notooltip = 0, $morecss = '', $save_lastsearch_value = -1)
1148 {
1149 global $conf, $langs, $hookmanager;
1150
1151 if (!empty($conf->dol_no_mouse_hover)) {
1152 $notooltip = 1; // Force disable tooltips
1153 }
1154
1155 $result = '';
1156 $params = [
1157 'id' => $this->id,
1158 'objecttype' => $this->element,
1159 'option' => $option,
1160 ];
1161 $classfortooltip = 'classfortooltip';
1162 $dataparams = '';
1163 if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
1164 $classfortooltip = 'classforajaxtooltip';
1165 $dataparams = ' data-params="'.dol_escape_htmltag(json_encode($params)).'"';
1166 $label = '';
1167 } else {
1168 $label = implode($this->getTooltipContentArray($params));
1169 }
1170
1171 $url = DOL_URL_ROOT.'/bom/bom_card.php?id='.$this->id;
1172
1173 if ($option != 'nolink') {
1174 // Add param to save lastsearch_values or not
1175 $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
1176 if ($save_lastsearch_value == -1 && isset($_SERVER["PHP_SELF"]) && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
1177 $add_save_lastsearch_values = 1;
1178 }
1179 if ($add_save_lastsearch_values) {
1180 $url .= '&save_lastsearch_values=1';
1181 }
1182 }
1183
1184 $linkclose = '';
1185 if (empty($notooltip)) {
1186 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
1187 $label = $langs->trans("ShowBillOfMaterials");
1188 $linkclose .= ' alt="'.dolPrintHTMLForAttribute($label).'"';
1189 }
1190 $linkclose .= ($label ? ' title="'.dolPrintHTMLForAttribute($label).'"' : ' title="tocomplete"');
1191 $linkclose .= $dataparams.' class="'.$classfortooltip.($morecss ? ' '.$morecss : '').'"';
1192 } else {
1193 $linkclose = ($morecss ? ' class="'.$morecss.'"' : '');
1194 }
1195
1196 $linkstart = '<a href="'.$url.'"';
1197 $linkstart .= $linkclose.'>';
1198 $linkend = '</a>';
1199
1200 $result .= $linkstart;
1201 if ($withpicto) {
1202 $result .= img_object(($notooltip ? '' : $label), ($this->picto ? $this->picto : 'generic'), (($withpicto != 2) ? 'class="paddingright"' : ''), 0, 0, $notooltip ? 0 : 1);
1203 }
1204 if ($withpicto != 2) {
1205 $result .= $this->ref;
1206 }
1207 $result .= $linkend;
1208 //if ($withpicto != 2) $result.=(($addlabel && $this->label) ? $sep . dol_trunc($this->label, ($addlabel > 1 ? $addlabel : 0)) : '');
1209
1210 global $action, $hookmanager;
1211 $hookmanager->initHooks(array('bomdao'));
1212 $parameters = array('id' => $this->id, 'getnomurl' => &$result);
1213 $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
1214 if ($reshook > 0) {
1215 $result = $hookmanager->resPrint;
1216 } else {
1217 $result .= $hookmanager->resPrint;
1218 }
1219
1220 return $result;
1221 }
1222
1229 public function getLibStatut($mode = 0)
1230 {
1231 return $this->LibStatut($this->status, $mode);
1232 }
1233
1234 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1242 public function LibStatut($status, $mode = 0)
1243 {
1244 // phpcs:enable
1245 if (empty($this->labelStatus)) {
1246 global $langs;
1247 //$langs->load("mrp");
1248 $this->labelStatus[self::STATUS_DRAFT] = $langs->transnoentitiesnoconv('Draft');
1249 $this->labelStatus[self::STATUS_VALIDATED] = $langs->transnoentitiesnoconv('Enabled');
1250 $this->labelStatus[self::STATUS_CANCELED] = $langs->transnoentitiesnoconv('Disabled');
1251 }
1252
1253 $statusType = 'status'.$status;
1254 if ($status == self::STATUS_VALIDATED) {
1255 $statusType = 'status4';
1256 }
1257 if ($status == self::STATUS_CANCELED) {
1258 $statusType = 'status6';
1259 }
1260
1261 return dolGetStatus($this->labelStatus[$status], $this->labelStatus[$status], '', $statusType, $mode);
1262 }
1263
1270 public function info($id)
1271 {
1272 $sql = 'SELECT rowid, date_creation as datec, tms as datem,';
1273 $sql .= ' fk_user_creat, fk_user_modif';
1274 $sql .= ' FROM '.MAIN_DB_PREFIX.$this->table_element.' as t';
1275 $sql .= ' WHERE t.rowid = '.((int) $id);
1276 $result = $this->db->query($sql);
1277 if ($result) {
1278 if ($this->db->num_rows($result)) {
1279 $obj = $this->db->fetch_object($result);
1280
1281 $this->id = $obj->rowid;
1282
1283 $this->user_creation_id = $obj->fk_user_creat;
1284 $this->user_modification_id = $obj->fk_user_modif;
1285 $this->date_creation = $this->db->jdate($obj->datec);
1286 $this->date_modification = empty($obj->datem) ? '' : $this->db->jdate($obj->datem);
1287 }
1288
1289 $this->db->free($result);
1290 } else {
1291 dol_print_error($this->db);
1292 }
1293 }
1294
1300 public function getLinesArray()
1301 {
1302 $this->lines = array();
1303
1304 $objectline = new BOMLine($this->db);
1305 $result = $objectline->fetchAll('ASC', 'position', 0, 0, '(fk_bom:=:'.((int) $this->id).')');
1306
1307 if (is_numeric($result)) {
1308 $this->error = $objectline->error;
1309 $this->errors = $objectline->errors;
1310 return $result;
1311 } else {
1312 $this->lines = $result;
1313 return $this->lines;
1314 }
1315 }
1316
1328 public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0, $moreparams = null)
1329 {
1330 global $langs;
1331
1332 $langs->load("mrp");
1333 $outputlangs->load("products");
1334
1335 if (!dol_strlen($modele)) {
1336 $modele = '';
1337
1338 if ($this->model_pdf) {
1339 $modele = $this->model_pdf;
1340 } elseif (getDolGlobalString('BOM_ADDON_PDF')) {
1341 $modele = getDolGlobalString('BOM_ADDON_PDF');
1342 }
1343 }
1344
1345 $modelpath = "core/modules/bom/doc/";
1346 if (!empty($modele)) {
1347 return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams);
1348 } else {
1349 return 0;
1350 }
1351 }
1352
1353 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1360 public function is_photo_available($sdir)
1361 {
1362 // phpcs:enable
1363 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1364 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
1365
1366 $sdir .= '/'.get_exdir(0, 0, 0, 0, $this, 'bom');
1367
1368 $dir_osencoded = dol_osencode($sdir);
1369 if (file_exists($dir_osencoded)) {
1370 $handle = opendir($dir_osencoded);
1371 if (is_resource($handle)) {
1372 while (($file = readdir($handle)) !== false) {
1373 if (!utf8_check($file)) {
1374 $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // To be sure data is stored in UTF8 in memory
1375 }
1376 if (dol_is_file($sdir.$file) && image_format_supported($file) >= 0) {
1377 return true;
1378 }
1379 }
1380 }
1381 }
1382 return false;
1383 }
1384
1391 public function initAsSpecimen()
1392 {
1393 $this->initAsSpecimenCommon();
1394 $this->ref = 'BOM-123';
1395 $this->date_creation = dol_now() - 20000;
1396
1397 return 1;
1398 }
1399
1400
1407 public function calculateCosts()
1408 {
1409 global $hookmanager;
1410
1411 include_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
1412 $this->unit_cost = 0;
1413 $this->total_cost = 0;
1414
1415 $parameters = array();
1416 $reshook = $hookmanager->executeHooks('calculateCostsBom', $parameters, $this); // Note that $action and $object may have been modified by hook
1417
1418 if ($reshook > 0) {
1419 $hookmanager->executeHooks('calculateCostsBomAfter', $parameters, $this); // Note that $action and $object may have been modified by hook
1420 return $hookmanager->resPrint;
1421 }
1422
1423 if (is_array($this->lines) && count($this->lines)) {
1424 require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
1425 $productFournisseur = new ProductFournisseur($this->db);
1426 $tmpproduct = new Product($this->db);
1427
1428 foreach ($this->lines as &$line) {
1429 $tmpproduct->cost_price = 0;
1430 $tmpproduct->pmp = 0;
1431 $result = $tmpproduct->fetch($line->fk_product, '', '', '', 0, 1, 1); // We discard selling price and language loading
1432
1433 $unit_cost = (float) (is_null($tmpproduct->cost_price) ? $tmpproduct->pmp : $tmpproduct->cost_price);
1434 if (empty($unit_cost)) { // @phpstan-ignore-line phpstan thinks this is always false. No,if unit_cost is 0, it is not.
1435 if ($productFournisseur->find_min_price_product_fournisseur($line->fk_product) > 0) {
1436 if ($productFournisseur->fourn_remise_percent != "0") {
1437 $line->unit_cost = $productFournisseur->fourn_unitprice_with_discount;
1438 } else {
1439 $line->unit_cost = $productFournisseur->fourn_unitprice;
1440 }
1441 } else {
1442 $line->unit_cost = 0;
1443 }
1444 } else {
1445 $line->unit_cost = (float) price2num($unit_cost);
1446 }
1447
1448 if ($tmpproduct->type == $tmpproduct::TYPE_PRODUCT) {
1449 if (empty($line->fk_bom_child)) {
1450 if ($result < 0) {
1451 $this->error = $tmpproduct->error;
1452 return -1;
1453 }
1454
1455 $line->total_cost = (float) $line->unit_cost * $line->qty / $line->efficiency;
1456
1457 $this->total_cost += $line->total_cost;
1458 } else {
1459 $bom_child = new BOM($this->db);
1460 $res = $bom_child->fetch((int) $line->fk_bom_child);
1461 if ($res > 0) {
1462 $bom_child->calculateCosts();
1463 $line->childBom[] = $bom_child;
1464 $line->total_cost = (float) $bom_child->unit_cost * $line->qty / $line->efficiency;
1465 $this->total_cost += $line->total_cost;
1466 } else {
1467 $this->error = $bom_child->error;
1468 return -2;
1469 }
1470 }
1471 } else {
1472 // Convert qty of line into hours
1473 require_once DOL_DOCUMENT_ROOT.'/core/class/cunits.class.php';
1474 $measuringUnits = new CUnits($this->db);
1475 $measuringUnits->fetch($line->fk_unit);
1476
1477 // The unit is a unit for time, so the $measuringUnits->scale is not a power of 10, but directly the factor to change unit into seconds
1478 $qtyhourforline = $line->qty * (int) $measuringUnits->scale / 3600;
1479
1480 if (isModEnabled('workstation') && !empty($line->fk_default_workstation)) {
1481 $workstation = new Workstation($this->db);
1482 $res = $workstation->fetch($line->fk_default_workstation);
1483
1484 if ($res > 0) {
1485 $line->total_cost = (float) $qtyhourforline * ($workstation->thm_operator_estimated + $workstation->thm_machine_estimated);
1486 } else {
1487 $this->error = $workstation->error;
1488 return -3;
1489 }
1490 } else {
1491 $defaultdurationofservice = $tmpproduct->duration;
1492 $reg = array();
1493 $qtyhourservice = 0;
1494 if (preg_match('/^(\d+)([a-z]+)$/', $defaultdurationofservice, $reg)) {
1495 $qtyhourservice = convertDurationtoHour((float) $reg[1], $reg[2]);
1496 }
1497
1498 if ($qtyhourservice) {
1499 $line->total_cost = (float) $qtyhourforline / $qtyhourservice * $line->unit_cost;
1500 } else {
1501 $line->total_cost = (float) $line->qty * $line->unit_cost;
1502 }
1503 }
1504
1505 $this->total_cost += $line->total_cost;
1506 }
1507 }
1508
1509 $this->total_cost = (float) price2num($this->total_cost, 'MT');
1510
1511 if ($this->qty > 0) {
1512 $this->unit_cost = (float) price2num($this->total_cost / $this->qty, 'MU');
1513 } elseif ($this->qty < 0) {
1514 $this->unit_cost = (float) price2num($this->total_cost * $this->qty, 'MU');
1515 }
1516 }
1517 $hookmanager->executeHooks('calculateCostsBomAfter', $parameters, $this);
1518
1519 return 1;
1520 }
1521
1530 public static function replaceProduct(DoliDB $db, $origin_id, $dest_id)
1531 {
1532 $tables = array(
1533 'bom_bomline'
1534 );
1535
1536 return CommonObject::commonReplaceProduct($db, $origin_id, $dest_id, $tables);
1537 }
1538
1546 public function getNetNeeds(&$TNetNeeds = array(), $qty = 0)
1547 {
1548 if (!empty($this->lines)) {
1549 foreach ($this->lines as $line) {
1550 if (!empty($line->childBom)) {
1551 foreach ($line->childBom as $childBom) {
1552 $childBom->getNetNeeds($TNetNeeds, $line->qty * $qty / $childBom->qty);
1553 }
1554 } else {
1555 if (empty($TNetNeeds[$line->fk_product]['qty'])) {
1556 $TNetNeeds[$line->fk_product]['qty'] = 0.0;
1557 }
1558 // When using nested level (or not), the qty for needs must always use the same unit to be able to be cumulated.
1559 // So if unit in bom is not the same than default, we must recalculate qty after units comparisons.
1560 $TNetNeeds[$line->fk_product]['fk_unit'] = $line->fk_unit;
1561 $TNetNeeds[$line->fk_product]['qty'] += $line->qty * $qty;
1562 }
1563 }
1564 }
1565 }
1566
1575 public function getNetNeedsTree(&$TNetNeeds = array(), $qty = 0, $level = 0)
1576 {
1577 if (!empty($this->lines)) {
1578 foreach ($this->lines as $line) {
1579 if (!empty($line->childBom)) {
1580 foreach ($line->childBom as $childBom) {
1581 $TNetNeeds[$childBom->id]['bom'] = $childBom;
1582 $TNetNeeds[$childBom->id]['parentid'] = $this->id;
1583 // When using nested level (or not), the qty for needs must always use the same unit to be able to be cumulated.
1584 // So if unit in bom is not the same than default, we must recalculate qty after units comparisons.
1585 //$TNetNeeds[$childBom->id]['fk_unit'] = $line->fk_unit;
1586 $TNetNeeds[$childBom->id]['qty'] = $line->qty * $qty;
1587 $TNetNeeds[$childBom->id]['level'] = $level;
1588 $childBom->getNetNeedsTree($TNetNeeds, $line->qty * $qty / $childBom->qty, $level + 1);
1589 }
1590 } else {
1591 // When using nested level (or not), the qty for needs must always use the same unit to be able to be cumulated.
1592 // So if unit in bom is not the same than default, we must recalculate qty after units comparisons.
1593 if (!isset($TNetNeeds[$this->id]['product'])) {
1594 $TNetNeeds[$this->id]['product'] = array();
1595 }
1596 if (!isset($TNetNeeds[$this->id]['product'][$line->fk_product])) {
1597 $TNetNeeds[$this->id]['product'][$line->fk_product] = array();
1598 }
1599 $TNetNeeds[$this->id]['product'][$line->fk_product]['fk_unit'] = $line->fk_unit;
1600 if (!isset($TNetNeeds[$this->id]['product'][$line->fk_product]['qty'])) {
1601 $TNetNeeds[$this->id]['product'][$line->fk_product]['qty'] = 0.0;
1602 }
1603 $TNetNeeds[$this->id]['product'][$line->fk_product]['qty'] += $line->qty * $qty;
1604 $TNetNeeds[$this->id]['product'][$line->fk_product]['level'] = $level;
1605 }
1606 }
1607 }
1608 }
1609
1618 public function getParentBomTreeRecursive(&$TParentBom, $bom_id = 0, $level = 1)
1619 {
1620 // Protection against infinite loop
1621 if ($level > 1000) {
1622 return;
1623 }
1624
1625 if (empty($bom_id)) {
1626 $bom_id = $this->id;
1627 }
1628
1629 $sql = "SELECT l.fk_bom, b.label FROM ".MAIN_DB_PREFIX."bom_bomline as l INNER JOIN ".MAIN_DB_PREFIX.$this->table_element." b ON b.rowid = l.fk_bom";
1630 $sql .= " WHERE fk_bom_child = ".((int) $bom_id);
1631
1632 $resql = $this->db->query($sql);
1633 if (!empty($resql)) {
1634 while ($res = $this->db->fetch_object($resql)) {
1635 $TParentBom[(int) $res->fk_bom] = (int) $res->fk_bom;
1636 $this->getParentBomTreeRecursive($TParentBom, $res->fk_bom, $level + 1);
1637 }
1638 }
1639 }
1640
1648 public function getKanbanView($option = '', $arraydata = null)
1649 {
1650 global $langs;
1651
1652 $selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);
1653
1654 $return = '<div class="box-flex-item box-flex-grow-zero">';
1655 $return .= '<div class="info-box info-box-sm">';
1656 $return .= '<span class="info-box-icon bg-infobox-action">';
1657 $return .= img_picto('', $this->picto);
1658 $return .= '</span>';
1659 $return .= '<div class="info-box-content">';
1660 $return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">' . $this->getNomUrl() . '</span>';
1661 if ($selected >= 0) {
1662 $return .= '<input id="cb'.$this->id.'" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="'.$this->id.'"'.($selected ? ' checked="checked"' : '').'>';
1663 }
1664 $arrayofkeyval = $this->fields['bomtype']['arrayofkeyval'] ?? null;
1665 if (!empty($arrayofkeyval)) {
1666 $return .= '<br><span class="info-box-label opacitymedium">'.$langs->trans("Type").' : </span>';
1667 if ($this->bomtype == 0) {
1668 $return .= '<span class="info-box-label">'.$arrayofkeyval[0].'</span>';
1669 } else {
1670 $return .= '<span class="info-box-label">'.$arrayofkeyval[1].'</span>';
1671 }
1672 }
1673 if (!empty($arraydata['prod'])) {
1674 $prod = $arraydata['prod'];
1675 $return .= '<br><span class="info-box-label">'.$prod->getNomUrl(1).'</span>';
1676 }
1677 $return .= '<br><div class="info-box-status">'.$this->getLibStatut(3).'</div>';
1678
1679 $return .= '</div>';
1680 $return .= '</div>';
1681 $return .= '</div>';
1682
1683 return $return;
1684 }
1685}
if(! $sortfield) if(! $sortorder) $object
Definition account.php:100
$object ref
Definition info.php:90
Class for BOM.
Definition bom.class.php:42
static replaceProduct(DoliDB $db, $origin_id, $dest_id)
Function used to replace a product id with another one.
fetchLines()
Load object lines in memory from the database.
is_photo_available($sdir)
Return if at least one photo is available.
__construct(DoliDB $db)
Constructor.
calculateCosts()
BOM costs calculation based on cost_price or pmp of each BOM line.
info($id)
Load the info information in the object.
getLibStatut($mode=0)
Return label of the status.
initAsSpecimen()
Initialise object with example values Id must be 0 if object instance is a specimen.
create(User $user, $notrigger=0)
Create object into database.
validate($user, $notrigger=0)
Validate bom.
reopen($user, $notrigger=0)
Reopen if canceled.
cancel($user, $notrigger=0)
Set cancel status.
LibStatut($status, $mode=0)
Return the status.
getParentBomTreeRecursive(&$TParentBom, $bom_id=0, $level=1)
Recursively retrieves all parent bom in the tree that leads to the $bom_id bom.
getTooltipContentArray($params)
getTooltipContentArray
fetchLinesbytypeproduct($typeproduct=0)
Load object lines in memory from the database by type of product.
deleteLine(User $user, $idline, $notrigger=0)
Delete a line of object in database.
createFromClone(User $user, $fromid)
Clone an object into another one.
fetch($id, $ref=null)
Load object in memory from the database.
getNomUrl($withpicto=0, $option='', $notooltip=0, $morecss='', $save_lastsearch_value=-1)
Return a link to the object card (with optionally the picto)
updateLine($rowid, $qty, $qty_frozen=0, $disable_stock_change=0, $efficiency=1.0, $position=-1, $import_key=null, $fk_unit=0, $array_options=array(), $fk_default_workstation=null)
Update an BOM line into database.
getNetNeeds(&$TNetNeeds=array(), $qty=0)
Get Net needs by product.
update(User $user, $notrigger=0)
Update object into database.
generateDocument($modele, $outputlangs, $hidedetails=0, $hidedesc=0, $hideref=0, $moreparams=null)
Create a document onto disk according to template module.
getLinesArray()
Create an array of lines.
setDraft($user, $notrigger=0)
Set draft status.
getKanbanView($option='', $arraydata=null)
Return clickable link of object (with eventually picto)
fetchAll($sortorder='', $sortfield='', $limit=0, $offset=0, $filter='', $filtermode='AND')
Load list of objects in memory from the database.
addLine($fk_product, $qty, $qty_frozen=0, $disable_stock_change=0, $efficiency=1.0, $position=-1, $fk_bom_child=null, $import_key=null, $fk_unit=0, $array_options=array(), $fk_default_workstation=null)
Add an BOM line into database (linked to BOM)
getNetNeedsTree(&$TNetNeeds=array(), $qty=0, $level=0)
Get/add Net needs Tree by product or bom.
Class for BOMLine.
Class of dictionary type of thirdparty (used by imports)
Parent class of all other business classes (invoices, contracts, proposals, orders,...
commonGenerateDocument($modelspath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams=null)
Common function for all objects extending CommonObject for generating documents.
setErrorsFromObject($object)
setErrorsFromObject
isLineOfObject($lineid)
Check that a line belongs to this object, using $this->table_element_line and $this->fk_element.
createCommon(User $user, $notrigger=0)
Create object in the database.
getFieldList($alias='', $excludefields=array())
Function to concat keys of fields.
updateCommon(User $user, $notrigger=0)
Update object into database.
setStatusCommon($user, $status, $notrigger=0, $triggercode='')
Set to a status.
initAsSpecimenCommon()
Initialise object with example values Id must be 0 if object instance is a specimen.
copy_linked_contact($objFrom, $source='internal')
Copy contact from one element to current.
fetch_product()
Load the product with id $this->fk_product into this->product.
fetchLinesCommon($morewhere='', $noextrafields=0)
Load object in memory from the database.
static commonReplaceProduct(DoliDB $dbs, $origin_id, $dest_id, array $tables, $ignoreerrors=0)
Function used to replace a product id with another one.
line_max($fk_parent_line=0)
Get max value used for position of line (rang)
fetchCommon($id, $ref=null, $morewhere='', $noextrafields=0)
Load object in memory from the database.
deleteCommon(User $user, $notrigger=0, $forcechilddeletion=0)
Delete object in database.
Class to manage Dolibarr database access.
Class to manage predefined suppliers products.
Class to manage products or services.
Class to manage Dolibarr users.
Class for Workstation.
print $langs trans("Ref").' m titre as m m statut as status
Or an array listing all the potential status of the object: array: int of the status => translated la...
Definition index.php:171
convertDurationtoHour($duration_value, $duration_unit)
Convert duration to hour.
Definition date.lib.php:356
dol_is_file($pathoffile)
Return if path is a file.
dol_dir_list($utf8_path, $types="all", $recursive=0, $filter="", $excludefilter=null, $sortcriteria="name", $sortorder=SORT_ASC, $mode=0, $nohook=0, $relativename="", $donotfollowsymlinks=0, $nbsecondsold=0)
Scan a directory and return a list of files/directories.
Definition files.lib.php:64
dol_now($mode='gmt')
Return date for now.
img_picto($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0, $alt='', $morecss='', $marginleftonlyshort=2, $allowothertags=array())
Show picto whatever it's its name (generic function)
dol_osencode($str)
Return a string encoded into OS filesystem encoding.
price2num($amount, $rounding='', $option=0)
Function that return a number with universal decimal format (decimal separator is '.
img_object($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0, $allowothertags=array())
Show a picto called object_picto (generic function)
dol_sanitizeFileName($str, $newstr='_', $unaccent=1, $includequotes=0, $allowdash=0)
Clean a string to use it as a file name.
dol_strlen($string, $stringencoding='UTF-8')
Make a strlen call.
forgeSQLFromUniversalSearchCriteria($filter, &$errorstr='', $noand=0, $nopar=0, $noerror=0)
forgeSQLFromUniversalSearchCriteria
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
dolGetStatus($statusLabel='', $statusLabelShort='', $html='', $statusType='status0', $displayMode=0, $url='', $params=array())
Output the badge of a status.
if(!function_exists( 'utf8_encode')) if(!function_exists('utf8_decode')) if(!function_exists( 'str_starts_with')) if(!function_exists('str_ends_with')) if(!function_exists( 'str_contains')) getMultidirOutput($object, $module='', $forobject=0, $mode='output')
Return the full path of the directory where a module (or an object of a module) stores its files.
dol_buildpath($path, $type=0, $returnemptyifnotfound=0)
Return path of url or filesystem.
dol_print_error($db=null, $error='', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
isModEnabled($module)
Is Dolibarr module enabled.
utf8_check($str)
Check if a string is in UTF8.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
image_format_supported($file, $acceptsvg=0)
Return if a filename is file name of a supported image format.