dolibarr 25.0.0-alpha
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-2026 Frédéric France <frederic.france@free.fr>
6 * Copyright (C) 2024-2026 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->sanitize($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 = ".((int) $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 = ".((int) $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 $baseurl = DOL_URL_ROOT.'/bom/bom_card.php';
1172
1173 $query = ['id' => $this->id];
1174
1175 if ($option != 'nolink') {
1176 // Add param to save lastsearch_values or not
1177 $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
1178 if ($save_lastsearch_value == -1 && isset($_SERVER["PHP_SELF"]) && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
1179 $add_save_lastsearch_values = 1;
1180 }
1181 if ($add_save_lastsearch_values) {
1182 $query = array_merge($query, ['save_lastsearch_values' => 1]);
1183 }
1184 }
1185 $url = dolBuildUrl($baseurl, $query);
1186
1187 $linkclose = '';
1188 if (empty($notooltip)) {
1189 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
1190 $label = $langs->trans("ShowBillOfMaterials");
1191 $linkclose .= ' alt="'.dolPrintHTMLForAttribute($label).'"';
1192 }
1193 $linkclose .= ($label ? ' title="'.dolPrintHTMLForAttribute($label).'"' : ' title="tocomplete"');
1194 $linkclose .= $dataparams.' class="'.$classfortooltip.($morecss ? ' '.$morecss : '').'"';
1195 } else {
1196 $linkclose = ($morecss ? ' class="'.$morecss.'"' : '');
1197 }
1198
1199 $linkstart = '<a href="'.$url.'"';
1200 $linkstart .= $linkclose.'>';
1201 $linkend = '</a>';
1202
1203 $result .= $linkstart;
1204 if ($withpicto) {
1205 $result .= img_object(($notooltip ? '' : $label), ($this->picto ? $this->picto : 'generic'), (($withpicto != 2) ? 'class="paddingright"' : ''), 0, 0, $notooltip ? 0 : 1);
1206 }
1207 if ($withpicto != 2) {
1208 $result .= $this->ref;
1209 }
1210 $result .= $linkend;
1211 //if ($withpicto != 2) $result.=(($addlabel && $this->label) ? $sep . dol_trunc($this->label, ($addlabel > 1 ? $addlabel : 0)) : '');
1212
1213 global $action, $hookmanager;
1214 $hookmanager->initHooks(array('bomdao'));
1215 $parameters = array('id' => $this->id, 'getnomurl' => &$result);
1216 $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
1217 if ($reshook > 0) {
1218 $result = $hookmanager->resPrint;
1219 } else {
1220 $result .= $hookmanager->resPrint;
1221 }
1222
1223 return $result;
1224 }
1225
1232 public function getLibStatut($mode = 0)
1233 {
1234 return $this->LibStatut($this->status, $mode);
1235 }
1236
1237 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1245 public function LibStatut($status, $mode = 0)
1246 {
1247 // phpcs:enable
1248 if (empty($this->labelStatus)) {
1249 global $langs;
1250 //$langs->load("mrp");
1251 $this->labelStatus[self::STATUS_DRAFT] = $langs->transnoentitiesnoconv('Draft');
1252 $this->labelStatus[self::STATUS_VALIDATED] = $langs->transnoentitiesnoconv('Enabled');
1253 $this->labelStatus[self::STATUS_CANCELED] = $langs->transnoentitiesnoconv('Disabled');
1254 }
1255
1256 $statusType = 'status'.$status;
1257 if ($status == self::STATUS_VALIDATED) {
1258 $statusType = 'status4';
1259 }
1260 if ($status == self::STATUS_CANCELED) {
1261 $statusType = 'status6';
1262 }
1263
1264 return dolGetStatus($this->labelStatus[$status], $this->labelStatus[$status], '', $statusType, $mode);
1265 }
1266
1273 public function info($id)
1274 {
1275 $sql = 'SELECT rowid, date_creation as datec, tms as datem,';
1276 $sql .= ' fk_user_creat, fk_user_modif';
1277 $sql .= ' FROM '.MAIN_DB_PREFIX.$this->table_element.' as t';
1278 $sql .= ' WHERE t.rowid = '.((int) $id);
1279 $result = $this->db->query($sql);
1280 if ($result) {
1281 if ($this->db->num_rows($result)) {
1282 $obj = $this->db->fetch_object($result);
1283
1284 $this->id = $obj->rowid;
1285
1286 $this->user_creation_id = $obj->fk_user_creat;
1287 $this->user_modification_id = $obj->fk_user_modif;
1288 $this->date_creation = $this->db->jdate($obj->datec);
1289 $this->date_modification = empty($obj->datem) ? '' : $this->db->jdate($obj->datem);
1290 }
1291
1292 $this->db->free($result);
1293 } else {
1294 dol_print_error($this->db);
1295 }
1296 }
1297
1303 public function getLinesArray()
1304 {
1305 $this->lines = array();
1306
1307 $objectline = new BOMLine($this->db);
1308 $result = $objectline->fetchAll('ASC', 'position', 0, 0, '(fk_bom:=:'.((int) $this->id).')');
1309
1310 if (is_numeric($result)) {
1311 $this->error = $objectline->error;
1312 $this->errors = $objectline->errors;
1313 return $result;
1314 } else {
1315 $this->lines = $result;
1316 return $this->lines;
1317 }
1318 }
1319
1331 public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0, $moreparams = null)
1332 {
1333 global $langs;
1334
1335 $langs->load("mrp");
1336 $outputlangs->load("products");
1337
1338 if (!dol_strlen($modele)) {
1339 $modele = '';
1340
1341 if ($this->model_pdf) {
1342 $modele = $this->model_pdf;
1343 } elseif (getDolGlobalString('BOM_ADDON_PDF')) {
1344 $modele = getDolGlobalString('BOM_ADDON_PDF');
1345 }
1346 }
1347
1348 $modelpath = "core/modules/bom/doc/";
1349 if (!empty($modele)) {
1350 return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams);
1351 } else {
1352 return 0;
1353 }
1354 }
1355
1356 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1363 public function is_photo_available($sdir)
1364 {
1365 // phpcs:enable
1366 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1367 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
1368
1369 $sdir .= '/'.get_exdir(0, 0, 0, 0, $this, 'bom');
1370
1371 $dir_osencoded = dol_osencode($sdir);
1372 if (file_exists($dir_osencoded)) {
1373 $handle = opendir($dir_osencoded);
1374 if (is_resource($handle)) {
1375 while (($file = readdir($handle)) !== false) {
1376 if (!utf8_check($file)) {
1377 $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // To be sure data is stored in UTF8 in memory
1378 }
1379 if (dol_is_file($sdir.$file) && image_format_supported($file) >= 0) {
1380 return true;
1381 }
1382 }
1383 }
1384 }
1385 return false;
1386 }
1387
1394 public function initAsSpecimen()
1395 {
1396 $this->initAsSpecimenCommon();
1397 $this->ref = 'BOM-123';
1398 $this->date_creation = dol_now() - 20000;
1399
1400 return 1;
1401 }
1402
1403
1410 public function calculateCosts()
1411 {
1412 global $hookmanager;
1413
1414 include_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
1415 $this->unit_cost = 0;
1416 $this->total_cost = 0;
1417
1418 $parameters = array();
1419 $reshook = $hookmanager->executeHooks('calculateCostsBom', $parameters, $this); // Note that $action and $object may have been modified by hook
1420
1421 if ($reshook > 0) {
1422 $hookmanager->executeHooks('calculateCostsBomAfter', $parameters, $this); // Note that $action and $object may have been modified by hook
1423 return $hookmanager->resPrint;
1424 }
1425
1426 if (is_array($this->lines) && count($this->lines)) {
1427 require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
1428 $productFournisseur = new ProductFournisseur($this->db);
1429 $tmpproduct = new Product($this->db);
1430
1431 foreach ($this->lines as &$line) {
1432 $tmpproduct->cost_price = 0;
1433 $tmpproduct->pmp = 0;
1434 $result = $tmpproduct->fetch($line->fk_product, '', '', '', 0, 1, 1); // We discard selling price and language loading
1435
1436 $unit_cost = (float) (is_null($tmpproduct->cost_price) ? $tmpproduct->pmp : $tmpproduct->cost_price);
1437 if (empty($unit_cost)) { // @phpstan-ignore-line phpstan thinks this is always false. No,if unit_cost is 0, it is not.
1438 if ($productFournisseur->find_min_price_product_fournisseur($line->fk_product) > 0) {
1439 if ($productFournisseur->fourn_remise_percent != "0") {
1440 $line->unit_cost = $productFournisseur->fourn_unitprice_with_discount;
1441 } else {
1442 $line->unit_cost = $productFournisseur->fourn_unitprice;
1443 }
1444 } else {
1445 $line->unit_cost = 0;
1446 }
1447 } else {
1448 $line->unit_cost = (float) price2num($unit_cost);
1449 }
1450
1451 if ($tmpproduct->type == $tmpproduct::TYPE_PRODUCT) {
1452 if (empty($line->fk_bom_child)) {
1453 if ($result < 0) {
1454 $this->error = $tmpproduct->error;
1455 return -1;
1456 }
1457
1458 $line->total_cost = (float) $line->unit_cost * $line->qty / $line->efficiency;
1459
1460 $this->total_cost += $line->total_cost;
1461 } else {
1462 $bom_child = new BOM($this->db);
1463 $res = $bom_child->fetch((int) $line->fk_bom_child);
1464 if ($res > 0) {
1465 $bom_child->calculateCosts();
1466 $line->childBom[] = $bom_child;
1467 $line->total_cost = (float) $bom_child->unit_cost * $line->qty / $line->efficiency;
1468 $this->total_cost += $line->total_cost;
1469 } else {
1470 $this->error = $bom_child->error;
1471 return -2;
1472 }
1473 }
1474 } else {
1475 // Convert qty of line into hours
1476 require_once DOL_DOCUMENT_ROOT.'/core/class/cunits.class.php';
1477 $measuringUnits = new CUnits($this->db);
1478 $measuringUnits->fetch($line->fk_unit);
1479
1480 // 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
1481 $qtyhourforline = $line->qty * (int) $measuringUnits->scale / 3600;
1482
1483 if (isModEnabled('workstation') && !empty($line->fk_default_workstation)) {
1484 $workstation = new Workstation($this->db);
1485 $res = $workstation->fetch($line->fk_default_workstation);
1486
1487 if ($res > 0) {
1488 $line->total_cost = (float) $qtyhourforline * ($workstation->thm_operator_estimated + $workstation->thm_machine_estimated);
1489 } else {
1490 $this->error = $workstation->error;
1491 return -3;
1492 }
1493 } else {
1494 $defaultdurationofservice = $tmpproduct->duration;
1495 $reg = array();
1496 $qtyhourservice = 0;
1497 if (preg_match('/^(\d+)([a-z]+)$/', $defaultdurationofservice, $reg)) {
1498 $qtyhourservice = convertDurationtoHour((float) $reg[1], $reg[2]);
1499 }
1500
1501 if ($qtyhourservice) {
1502 $line->total_cost = (float) $qtyhourforline / $qtyhourservice * $line->unit_cost;
1503 } else {
1504 $line->total_cost = (float) $line->qty * $line->unit_cost;
1505 }
1506 }
1507
1508 $this->total_cost += $line->total_cost;
1509 }
1510 }
1511
1512 $this->total_cost = (float) price2num($this->total_cost, 'MT');
1513
1514 if ($this->qty > 0) {
1515 $this->unit_cost = (float) price2num($this->total_cost / $this->qty, 'MU');
1516 } elseif ($this->qty < 0) {
1517 $this->unit_cost = (float) price2num($this->total_cost * $this->qty, 'MU');
1518 }
1519 }
1520 $hookmanager->executeHooks('calculateCostsBomAfter', $parameters, $this);
1521
1522 return 1;
1523 }
1524
1533 public static function replaceProduct(DoliDB $db, $origin_id, $dest_id)
1534 {
1535 $tables = array(
1536 'bom_bomline'
1537 );
1538
1539 return CommonObject::commonReplaceProduct($db, $origin_id, $dest_id, $tables);
1540 }
1541
1549 public function getNetNeeds(&$TNetNeeds = array(), $qty = 0)
1550 {
1551 if (!empty($this->lines)) {
1552 foreach ($this->lines as $line) {
1553 if (!empty($line->childBom)) {
1554 foreach ($line->childBom as $childBom) {
1555 $childBom->getNetNeeds($TNetNeeds, $line->qty * $qty / $childBom->qty);
1556 }
1557 } else {
1558 if (empty($TNetNeeds[$line->fk_product]['qty'])) {
1559 $TNetNeeds[$line->fk_product]['qty'] = 0.0;
1560 }
1561 // When using nested level (or not), the qty for needs must always use the same unit to be able to be cumulated.
1562 // So if unit in bom is not the same than default, we must recalculate qty after units comparisons.
1563 $TNetNeeds[$line->fk_product]['fk_unit'] = $line->fk_unit;
1564 $TNetNeeds[$line->fk_product]['qty'] += $line->qty * $qty;
1565 }
1566 }
1567 }
1568 }
1569
1578 public function getNetNeedsTree(&$TNetNeeds = array(), $qty = 0, $level = 0)
1579 {
1580 if (!empty($this->lines)) {
1581 foreach ($this->lines as $line) {
1582 if (!empty($line->childBom)) {
1583 foreach ($line->childBom as $childBom) {
1584 $TNetNeeds[$childBom->id]['bom'] = $childBom;
1585 $TNetNeeds[$childBom->id]['parentid'] = $this->id;
1586 // When using nested level (or not), the qty for needs must always use the same unit to be able to be cumulated.
1587 // So if unit in bom is not the same than default, we must recalculate qty after units comparisons.
1588 //$TNetNeeds[$childBom->id]['fk_unit'] = $line->fk_unit;
1589 $TNetNeeds[$childBom->id]['qty'] = $line->qty * $qty;
1590 $TNetNeeds[$childBom->id]['level'] = $level;
1591 $TNetNeeds[$childBom->id]['position'] = $line->position;
1592 $childBom->getNetNeedsTree($TNetNeeds, $line->qty * $qty / $childBom->qty, $level + 1);
1593 }
1594 } else {
1595 // When using nested level (or not), the qty for needs must always use the same unit to be able to be cumulated.
1596 // So if unit in bom is not the same than default, we must recalculate qty after units comparisons.
1597 if (!isset($TNetNeeds[$this->id]['product'])) {
1598 $TNetNeeds[$this->id]['product'] = array();
1599 }
1600 if (!isset($TNetNeeds[$this->id]['product'][$line->fk_product])) {
1601 $TNetNeeds[$this->id]['product'][$line->fk_product] = array();
1602 }
1603 $TNetNeeds[$this->id]['product'][$line->fk_product]['fk_unit'] = $line->fk_unit;
1604 if (!isset($TNetNeeds[$this->id]['product'][$line->fk_product]['qty'])) {
1605 $TNetNeeds[$this->id]['product'][$line->fk_product]['qty'] = 0.0;
1606 }
1607 $TNetNeeds[$this->id]['product'][$line->fk_product]['qty'] += $line->qty * $qty;
1608 $TNetNeeds[$this->id]['product'][$line->fk_product]['level'] = $level;
1609 $TNetNeeds[$this->id]['product'][$line->fk_product]['position'] = $line->position;
1610 }
1611 }
1612 }
1613 }
1614
1623 public function getParentBomTreeRecursive(&$TParentBom, $bom_id = 0, $level = 1)
1624 {
1625 // Protection against infinite loop
1626 if ($level > 1000) {
1627 return;
1628 }
1629
1630 if (empty($bom_id)) {
1631 $bom_id = $this->id;
1632 }
1633
1634 $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";
1635 $sql .= " WHERE fk_bom_child = ".((int) $bom_id);
1636
1637 $resql = $this->db->query($sql);
1638 if (!empty($resql)) {
1639 while ($res = $this->db->fetch_object($resql)) {
1640 $TParentBom[(int) $res->fk_bom] = (int) $res->fk_bom;
1641 $this->getParentBomTreeRecursive($TParentBom, $res->fk_bom, $level + 1);
1642 }
1643 }
1644 }
1645
1653 public function getKanbanView($option = '', $arraydata = null)
1654 {
1655 global $langs;
1656
1657 $selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);
1658
1659 $return = '<div class="box-flex-item box-flex-grow-zero">';
1660 $return .= '<div class="info-box info-box-sm">';
1661 $return .= '<span class="info-box-icon bg-infobox-action">';
1662 $return .= img_picto('', $this->picto);
1663 $return .= '</span>';
1664 $return .= '<div class="info-box-content">';
1665 $return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">' . $this->getNomUrl() . '</span>';
1666 if ($selected >= 0) {
1667 $return .= '<input id="cb'.$this->id.'" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="'.$this->id.'"'.($selected ? ' checked="checked"' : '').'>';
1668 }
1669 $arrayofkeyval = $this->fields['bomtype']['arrayofkeyval'] ?? null;
1670 if (!empty($arrayofkeyval)) {
1671 $return .= '<br><span class="info-box-label opacitymedium">'.$langs->trans("Type").' : </span>';
1672 if ($this->bomtype == 0) {
1673 $return .= '<span class="info-box-label">'.$arrayofkeyval[0].'</span>';
1674 } else {
1675 $return .= '<span class="info-box-label">'.$arrayofkeyval[1].'</span>';
1676 }
1677 }
1678 if (!empty($arraydata['prod'])) {
1679 $prod = $arraydata['prod'];
1680 $return .= '<br><span class="info-box-label">'.$prod->getNomUrl(1).'</span>';
1681 }
1682 $return .= '<br><div class="info-box-status">'.$this->getLibStatut(3).'</div>';
1683
1684 $return .= '</div>';
1685 $return .= '</div>';
1686 $return .= '</div>';
1687
1688 return $return;
1689 }
1690}
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)
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:169
convertDurationtoHour($duration_value, $duration_unit)
Convert duration to hour.
Definition date.lib.php:347
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $conf
The main.inc.php has been included so the following variable are now defined:
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:65
dol_now($mode='gmt')
Return date for now.
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 '.
dolBuildUrl($url, $params=[], $addtoken=false, $anchor='')
Return path of url.
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.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
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.
forgeSQLFromUniversalSearchCriteria($filter, &$errorstr='', $noand=0, $nopar=0, $noerror=0, $forbiddenfields=array())
forgeSQLFromUniversalSearchCriteria
dol_buildpath($path, $type=0, $returnemptyifnotfound=0)
Return path of url or filesystem.
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.