Passed
Pull Request — dev (#8)
by Rafael
58:47
created

BOM::__construct()   C

Complexity

Conditions 12
Paths 48

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 15
nc 48
nop 1
dl 0
loc 28
rs 6.9666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/* Copyright (C) 2019       Laurent Destailleur         <[email protected]>
4
 * Copyright (C) 2023	    Benjamin Falière	        <[email protected]>
5
 * Copyright (C) 2023	    Charlene Benke		        <[email protected]>
6
 * Copyright (C) 2024       Frédéric France             <[email protected]>
7
 * Copyright (C) 2024		MDW							<[email protected]>
8
 * Copyright (C) 2024       Rafael San José             <[email protected]>
9
 *
10
 * This program is free software; you can redistribute it and/or modify
11
 * it under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation; either version 3 of the License, or
13
 * (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 * GNU General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU General Public License
21
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22
 */
23
24
namespace Dolibarr\Code\Bom\Classes;
25
26
use Dolibarr\Code\Product\Classes\Product;
27
use Dolibarr\Code\User\Classes\User;
28
use Dolibarr\Core\Base\CommonObject;
29
use DoliDB;
30
31
/**
32
 * \file        htdocs/bom/class/bom.class.php
33
 * \ingroup     bom
34
 * \brief       This file is a CRUD class file for BOM (Create/Read/Update/Delete)
35
 */
36
37
// Put here all includes required by your class file
38
require_once constant('DOL_DOCUMENT_ROOT') . '/core/lib/date.lib.php';
39
40
/**
41
 * Class for BOM
42
 */
43
class BOM extends CommonObject
44
{
45
    /**
46
     * @var string ID of module.
47
     */
48
    public $module = 'bom';
49
50
    /**
51
     * @var string ID to identify managed object
52
     */
53
    public $element = 'bom';
54
55
    /**
56
     * @var string Name of table without prefix where object is stored
57
     */
58
    public $table_element = 'bom_bom';
59
60
    /**
61
     * @var string String with name of icon for bom. Must be the part after the 'object_' into object_bom.png
62
     */
63
    public $picto = 'bom';
64
65
    /**
66
     * @var Product Object product of the BOM
67
     */
68
    public $product;
69
70
    const STATUS_DRAFT = 0;
71
    const STATUS_VALIDATED = 1;
72
    const STATUS_CANCELED = 9;
73
74
75
    /**
76
     *  'type' field format ('integer', 'integer:ObjectClass:PathToClass[:AddCreateButtonOrNot[:Filter]]', 'sellist:TableName:LabelFieldName[:KeyFieldName[:KeyFieldParent[:Filter]]]', 'varchar(x)', 'double(24,8)', 'real', 'price', 'text', 'text:none', 'html', 'date', 'datetime', 'timestamp', 'duration', 'mail', 'phone', 'url', 'password')
77
     *         Note: Filter can be a string like "(t.ref:like:'SO-%') or (t.date_creation:<:'20160101') or (t.nature:is:NULL)"
78
     *  'label' the translation key.
79
     *  'picto' is code of a picto to show before value in forms
80
     *  'enabled' is a condition when the field must be managed (Example: 1 or 'getDolGlobalString("MY_SETUP_PARAM")'
81
     *  'position' is the sort order of field.
82
     *  'notnull' is set to 1 if not null in database. Set to -1 if we must set data to null if empty ('' or 0).
83
     *  'visible' says if field is visible in list (Examples: 0=Not visible, 1=Visible on list and create/update/view forms, 2=Visible on list only, 3=Visible on create/update/view form only (not list), 4=Visible on list and update/view form only (not create). 5=Visible on list and view only (not create/not update). Using a negative value means field is not shown by default on list but can be selected for viewing)
84
     *  'noteditable' says if field is not editable (1 or 0)
85
     *  'default' is a default value for creation (can still be overwrote by the Setup of Default Values if field is editable in creation form). Note: If default is set to '(PROV)' and field is 'ref', the default value will be set to '(PROVid)' where id is rowid when a new record is created.
86
     *  'index' if we want an index in database.
87
     *  'foreignkey'=>'tablename.field' if the field is a foreign key (it is recommended to name the field fk_...).
88
     *  'searchall' is 1 if we want to search in this field when making a search from the quick search button.
89
     *  'isameasure' must be set to 1 if you want to have a total on list for this field. Field type must be summable like integer or double(24,8).
90
     *  'css' and 'cssview' and 'csslist' is the CSS style to use on field. 'css' is used in creation and update. 'cssview' is used in view mode. 'csslist' is used for columns in lists. For example: 'css'=>'minwidth300 maxwidth500 widthcentpercentminusx', 'cssview'=>'wordbreak', 'csslist'=>'tdoverflowmax200'
91
     *  'help' is a 'TranslationString' to use to show a tooltip on field. You can also use 'TranslationString:keyfortooltiponlick' for a tooltip on click.
92
     *  'showoncombobox' if value of the field must be visible into the label of the combobox that list record
93
     *  'disabled' is 1 if we want to have the field locked by a 'disabled' attribute. In most cases, this is never set into the definition of $fields into class, but is set dynamically by some part of code.
94
     *  'arrayofkeyval' to set list of value if type is a list of predefined values. For example: array("0"=>"Draft","1"=>"Active","-1"=>"Cancel")
95
     *  'autofocusoncreate' to have field having the focus on a create form. Only 1 field should have this property set to 1.
96
     *  'comment' is not used. You can store here any text of your choice. It is not used by application.
97
     *
98
     *  Note: To have value dynamic, you can set value to 0 in definition and edit the value on the fly into the constructor.
99
     */
100
101
    // BEGIN MODULEBUILDER PROPERTIES
102
    /**
103
     * @var array<string,array{type:string,label:string,enabled:int<0,2>|string,position:int,notnull?:int,visible:int,noteditable?:int,default?:string,index?:int,foreignkey?:string,searchall?:int,isameasure?:int,css?:string,csslist?:string,help?:string,showoncombobox?:int,disabled?:int,arrayofkeyval?:array<int,string>,comment?:string}>  Array with all fields and their property. Do not use it as a static var. It may be modified by constructor.
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string,array{type:...ring>,comment?:string}> at position 16 could not be parsed: Expected '}' at position 16, but found 'int'.
Loading history...
104
     */
105
    public $fields = array(
106
        'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'enabled' => 1, 'visible' => -2, 'position' => 1, 'notnull' => 1, 'index' => 1, 'comment' => "Id",),
107
        'entity' => array('type' => 'integer', 'label' => 'Entity', 'enabled' => 1, 'visible' => 0, 'notnull' => 1, 'default' => 1, 'index' => 1, 'position' => 5),
108
        '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'),
109
        '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'),
110
        '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'),
111
        //'bomtype' => array('type'=>'integer', 'label'=>'Type', 'enabled'=>1, 'visible'=>-1, 'position'=>32, 'notnull'=>1, 'default'=>'0', 'arrayofkeyval'=>array(0=>'Manufacturing')),
112
        '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'),
113
        'description' => array('type' => 'text', 'label' => 'Description', 'enabled' => 1, 'visible' => -1, 'position' => 60, 'notnull' => -1,),
114
        'qty' => array('type' => 'real', 'label' => 'Quantity', 'enabled' => 1, 'visible' => 1, 'default' => 1, 'position' => 55, 'notnull' => 1, 'isameasure' => 1, 'css' => 'maxwidth50imp right'),
115
        //'efficiency' => array('type'=>'real', 'label'=>'ManufacturingEfficiency', 'enabled'=>1, 'visible'=>-1, 'default'=>1, 'position'=>100, 'notnull'=>0, 'css'=>'maxwidth50imp', 'help'=>'ValueOfMeansLossForProductProduced'),
116
        'duration' => array('type' => 'duration', 'label' => 'EstimatedDuration', 'enabled' => 1, 'visible' => -1, 'position' => 101, 'notnull' => -1, 'css' => 'maxwidth50imp', 'help' => 'EstimatedDurationDesc'),
117
        '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'),
118
        'note_public' => array('type' => 'html', 'label' => 'NotePublic', 'enabled' => 1, 'visible' => -2, 'position' => 161, 'notnull' => -1,),
119
        'note_private' => array('type' => 'html', 'label' => 'NotePrivate', 'enabled' => 1, 'visible' => -2, 'position' => 162, 'notnull' => -1,),
120
        'date_creation' => array('type' => 'datetime', 'label' => 'DateCreation', 'enabled' => 1, 'visible' => -2, 'position' => 300, 'notnull' => 1,),
121
        'tms' => array('type' => 'timestamp', 'label' => 'DateModification', 'enabled' => 1, 'visible' => -2, 'position' => 501, 'notnull' => 1,),
122
        'date_valid' => array('type' => 'datetime', 'label' => 'DateValidation', 'enabled' => 1, 'visible' => -2, 'position' => 502, 'notnull' => 0,),
123
        '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'),
124
        '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'),
125
        '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'),
126
        'import_key' => array('type' => 'varchar(14)', 'label' => 'ImportId', 'enabled' => 1, 'visible' => -2, 'position' => 1000, 'notnull' => -1,),
127
        'model_pdf' => array('type' => 'varchar(255)', 'label' => 'Model pdf', 'enabled' => 1, 'visible' => 0, 'position' => 1010),
128
        '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')),
129
    );
130
131
    /**
132
     * @var int rowid
133
     */
134
    public $rowid;
135
136
    /**
137
     * @var string ref
138
     */
139
    public $ref;
140
141
    /**
142
     * @var string label
143
     */
144
    public $label;
145
146
    /**
147
     * @var int bomtype
148
     */
149
    public $bomtype;
150
151
    /**
152
     * @var string description
153
     */
154
    public $description;
155
156
    /**
157
     * @var integer|string date_creation
158
     */
159
    public $date_creation;
160
161
    /**
162
     * @var integer|string date_valid
163
     */
164
    public $date_valid;
165
166
    /**
167
     * @var int Id User creator
168
     */
169
    public $fk_user_creat;
170
171
    /**
172
     * @var int Id User modifying
173
     */
174
    public $fk_user_modif;
175
176
    /**
177
     * @var int Id User modifying
178
     */
179
    public $fk_user_valid;
180
181
    /**
182
     * @var int Id User modifying
183
     */
184
    public $fk_warehouse;
185
186
    /**
187
     * @var string import key
188
     */
189
    public $import_key;
190
191
    /**
192
     * @var int status
193
     */
194
    public $status;
195
196
    /**
197
     * @var int product Id
198
     */
199
    public $fk_product;
200
    public $qty;
201
    public $duration;
202
    public $efficiency;
203
    // END MODULEBUILDER PROPERTIES
204
205
206
    // If this object has a subtable with lines
207
208
    /**
209
     * @var string    Name of subtable line
210
     */
211
    public $table_element_line = 'bom_bomline';
212
213
    /**
214
     * @var string    Fieldname with ID of parent key if this field has a parent
215
     */
216
    public $fk_element = 'fk_bom';
217
218
    /**
219
     * @var string    Name of subtable class that manage subtable lines
220
     */
221
    public $class_element_line = 'BOMLine';
222
223
    // /**
224
    //  * @var array    List of child tables. To test if we can delete object.
225
    //  */
226
    // protected $childtables=array();
227
228
    /**
229
     * @var string[]    List of child tables. To know object to delete on cascade.
230
     */
231
    protected $childtablesoncascade = array('bom_bomline');
232
233
    /**
234
     * @var BOMLine[]     Array of subtable lines
235
     */
236
    public $lines = array();
237
238
    /**
239
     * @var float       Calculated cost for the BOM
240
     */
241
    public $total_cost = 0;
242
243
    /**
244
     * @var float       Calculated cost for 1 unit of the product in BOM
245
     */
246
    public $unit_cost = 0;
247
248
249
    /**
250
     * Constructor
251
     *
252
     * @param DoliDB $db Database handler
253
     */
254
    public function __construct(DoliDB $db)
255
    {
256
        global $conf, $langs;
257
258
        $this->db = $db;
259
260
        $this->ismultientitymanaged = 1;
261
        $this->isextrafieldmanaged = 1;
262
263
        if (!getDolGlobalString('MAIN_SHOW_TECHNICAL_ID') && isset($this->fields['rowid'])) {
264
            $this->fields['rowid']['visible'] = 0;
265
        }
266
        if (!isModEnabled('multicompany') && isset($this->fields['entity'])) {
267
            $this->fields['entity']['enabled'] = 0;
268
        }
269
270
        // Unset fields that are disabled
271
        foreach ($this->fields as $key => $val) {
272
            if (isset($val['enabled']) && empty($val['enabled'])) {
273
                unset($this->fields[$key]);
274
            }
275
        }
276
277
        // Translate some data of arrayofkeyval
278
        foreach ($this->fields as $key => $val) {
279
            if (!empty($val['arrayofkeyval']) && is_array($val['arrayofkeyval'])) {
280
                foreach ($val['arrayofkeyval'] as $key2 => $val2) {
281
                    $this->fields[$key]['arrayofkeyval'][$key2] = $langs->trans($val2);
282
                }
283
            }
284
        }
285
    }
286
287
    /**
288
     * Create object into database
289
     *
290
     * @param  User $user      User that creates
291
     * @param  int  $notrigger false=launch triggers after, true=disable triggers
292
     * @return int             Return integer <0 if KO, Id of created object if OK
293
     */
294
    public function create(User $user, $notrigger = 1)
295
    {
296
        if ($this->efficiency <= 0 || $this->efficiency > 1) {
297
            $this->efficiency = 1;
298
        }
299
300
        return $this->createCommon($user, $notrigger);
301
    }
302
303
    /**
304
     * Clone an object into another one
305
     *
306
     * @param   User    $user       User that creates
307
     * @param   int     $fromid     Id of object to clone
308
     * @return  mixed               New object created, <0 if KO
309
     */
310
    public function createFromClone(User $user, $fromid)
311
    {
312
        global $langs, $hookmanager, $extrafields;
313
        $error = 0;
314
315
        dol_syslog(__METHOD__, LOG_DEBUG);
316
317
        $object = new self($this->db);
318
319
        $this->db->begin();
320
321
        // Load source object
322
        $result = $object->fetchCommon($fromid);
323
        if ($result > 0 && !empty($object->table_element_line)) {
324
            $object->fetchLines();
325
        }
326
327
        // Get lines so they will be clone
328
        //foreach ($object->lines as $line)
329
        //  $line->fetch_optionals();
330
331
        // Reset some properties
332
        unset($object->id);
333
        unset($object->fk_user_creat);
334
        unset($object->import_key);
335
336
        // Clear fields
337
        $object->ref = empty($this->fields['ref']['default']) ? $langs->trans("copy_of_") . $object->ref : $this->fields['ref']['default'];
338
        $object->label = empty($this->fields['label']['default']) ? $langs->trans("CopyOf") . " " . $object->label : $this->fields['label']['default'];
339
        $object->status = self::STATUS_DRAFT;
340
        // ...
341
        // Clear extrafields that are unique
342
        if (is_array($object->array_options) && count($object->array_options) > 0) {
343
            $extrafields->fetch_name_optionals_label($object->table_element);
344
            foreach ($object->array_options as $key => $option) {
345
                $shortkey = preg_replace('/options_/', '', $key);
346
                if (!empty($extrafields->attributes[$this->element]['unique'][$shortkey])) {
347
                    //var_dump($key); var_dump($clonedObj->array_options[$key]); exit;
348
                    unset($object->array_options[$key]);
349
                }
350
            }
351
        }
352
353
        // Create clone
354
        $object->context['createfromclone'] = 'createfromclone';
355
        $result = $object->createCommon($user);
356
        if ($result < 0) {
357
            $error++;
358
            $this->error = $object->error;
359
            $this->errors = $object->errors;
360
        }
361
362
        if (!$error) {
363
            // copy internal contacts
364
            if ($this->copy_linked_contact($object, 'internal') < 0) {
365
                $error++;
366
            }
367
        }
368
369
        if (!$error) {
370
            // copy external contacts if same company
371
            if (property_exists($this, 'socid') && $this->socid == $object->socid) {
372
                if ($this->copy_linked_contact($object, 'external') < 0) {
373
                    $error++;
374
                }
375
            }
376
        }
377
378
        // If there is lines, create lines too
379
380
381
382
        unset($object->context['createfromclone']);
383
384
        // End
385
        if (!$error) {
386
            $this->db->commit();
387
            return $object;
388
        } else {
389
            $this->db->rollback();
390
            return -1;
391
        }
392
    }
393
394
    /**
395
     * Load object in memory from the database
396
     *
397
     * @param int    $id   Id object
398
     * @param string $ref  Ref
399
     * @return int         Return integer <0 if KO, 0 if not found, >0 if OK
400
     */
401
    public function fetch($id, $ref = null)
402
    {
403
        $result = $this->fetchCommon($id, $ref);
404
405
        if ($result > 0 && !empty($this->table_element_line)) {
406
            $this->fetchLines();
407
        }
408
        //$this->calculateCosts();      // This consume a high number of subrequests. Do not call it into fetch but when you need it.
409
410
        return $result;
411
    }
412
413
    /**
414
     * Load object lines in memory from the database
415
     *
416
     * @return int         Return integer <0 if KO, 0 if not found, >0 if OK
417
     */
418
    public function fetchLines()
419
    {
420
        $this->lines = array();
421
422
        $result = $this->fetchLinesCommon();
423
        return $result;
424
    }
425
426
    /**
427
     * Load object lines in memory from the database by type of product
428
     *
429
     *  @param int    $typeproduct   0 type product, 1 type service
430
431
     * @return int         Return integer <0 if KO, 0 if not found, >0 if OK
432
     */
433
    public function fetchLinesbytypeproduct($typeproduct = 0)
434
    {
435
        $this->lines = array();
436
437
        $objectlineclassname = get_class($this) . 'Line';
438
        if (!class_exists($objectlineclassname)) {
439
            $this->error = 'Error, class ' . $objectlineclassname . ' not found during call of fetchLinesCommon';
440
            return -1;
441
        }
442
443
        $objectline = new $objectlineclassname($this->db);
444
445
        $sql = "SELECT " . $objectline->getFieldList('l');
446
        $sql .= " FROM " . $this->db->prefix() . $objectline->table_element . " as l";
447
        $sql .= " LEFT JOIN " . $this->db->prefix() . "product as p ON p.rowid = l.fk_product";
448
        $sql .= " WHERE l.fk_" . $this->db->escape($this->element) . " = " . ((int) $this->id);
449
        $sql .= " AND p.fk_product_type = " . ((int) $typeproduct);
450
        if (isset($objectline->fields['position'])) {
451
            $sql .= $this->db->order('position', 'ASC');
452
        }
453
454
        $resql = $this->db->query($sql);
455
        if ($resql) {
456
            $num_rows = $this->db->num_rows($resql);
457
            $i = 0;
458
            while ($i < $num_rows) {
459
                $obj = $this->db->fetch_object($resql);
460
                if ($obj) {
461
                    $newline = new $objectlineclassname($this->db);
462
                    $newline->setVarsFromFetchObj($obj);
463
464
                    $this->lines[] = $newline;
465
                }
466
                $i++;
467
            }
468
469
            return $num_rows;
470
        } else {
471
            $this->error = $this->db->lasterror();
472
            $this->errors[] = $this->error;
473
            return -1;
474
        }
475
    }
476
477
478
    /**
479
     * Load list of objects in memory from the database.
480
     *
481
     * @param  string           $sortorder    Sort Order
482
     * @param  string           $sortfield    Sort field
483
     * @param  int              $limit        Limit
484
     * @param  int              $offset       Offset
485
     * @param  string           $filter       Filter USF
486
     * @param  string           $filtermode   Filter mode (AND or OR)
487
     * @return array|int                      int <0 if KO, array of pages if OK
488
     */
489
    public function fetchAll($sortorder = '', $sortfield = '', $limit = 0, $offset = 0, $filter = '', $filtermode = 'AND')
490
    {
491
        dol_syslog(__METHOD__, LOG_DEBUG);
492
493
        $records = array();
494
495
        $sql = 'SELECT ';
496
        $sql .= $this->getFieldList();
497
        $sql .= ' FROM ' . MAIN_DB_PREFIX . $this->table_element . ' as t';
498
        if ($this->ismultientitymanaged) {
499
            $sql .= ' WHERE t.entity IN (' . getEntity($this->element) . ')';
500
        } else {
501
            $sql .= ' WHERE 1 = 1';
502
        }
503
504
        // Manage filter
505
        $errormessage = '';
506
        $sql .= forgeSQLFromUniversalSearchCriteria($filter, $errormessage);
507
        if ($errormessage) {
508
            $this->errors[] = $errormessage;
509
            dol_syslog(__METHOD__ . ' ' . implode(',', $this->errors), LOG_ERR);
510
            return -1;
511
        }
512
513
        if (!empty($sortfield)) {
514
            $sql .= $this->db->order($sortfield, $sortorder);
515
        }
516
        if (!empty($limit)) {
517
            $sql .= $this->db->plimit($limit, $offset);
518
        }
519
520
        $resql = $this->db->query($sql);
521
        if ($resql) {
522
            $num = $this->db->num_rows($resql);
523
524
            while ($obj = $this->db->fetch_object($resql)) {
525
                $record = new self($this->db);
526
                $record->setVarsFromFetchObj($obj);
527
528
                $records[$record->id] = $record;
529
            }
530
            $this->db->free($resql);
531
532
            return $records;
533
        } else {
534
            $this->errors[] = 'Error ' . $this->db->lasterror();
535
            dol_syslog(__METHOD__ . ' ' . implode(',', $this->errors), LOG_ERR);
536
537
            return -1;
538
        }
539
    }
540
541
    /**
542
     * Update object into database
543
     *
544
     * @param  User $user      User that modifies
545
     * @param  int  $notrigger 0=launch triggers after, 1=disable triggers
546
     * @return int             Return integer <0 if KO, >0 if OK
547
     */
548
    public function update(User $user, $notrigger = 1)
549
    {
550
        if ($this->efficiency <= 0 || $this->efficiency > 1) {
551
            $this->efficiency = 1;
552
        }
553
554
        return $this->updateCommon($user, $notrigger);
555
    }
556
557
    /**
558
     * Delete object in database
559
     *
560
     * @param User $user        User that deletes
561
     * @param int   $notrigger  0=launch triggers after, 1=disable triggers
562
     * @return int              Return integer <0 if KO, >0 if OK
563
     */
564
    public function delete(User $user, $notrigger = 1)
565
    {
566
        return $this->deleteCommon($user, $notrigger);
567
        //return $this->deleteCommon($user, $notrigger, 1);
568
    }
569
570
    /**
571
     * Add an BOM line into database (linked to BOM)
572
     *
573
     * @param   int     $fk_product             Id of product
574
     * @param   float   $qty                    Quantity
575
     * @param   int<0,1> $qty_frozen            If the qty is Frozen
576
     * @param   int     $disable_stock_change   Disable stock change on using in MO
577
     * @param   float   $efficiency             Efficiency in MO
578
     * @param   int     $position               Position of BOM-Line in BOM-Lines
579
     * @param   int     $fk_bom_child           Id of BOM Child
580
     * @param   string  $import_key             Import Key
581
     * @param   int     $fk_unit                Unit
582
     * @param   array   $array_options          extrafields array
583
     * @param   int     $fk_default_workstation Default workstation
584
     * @return  int                             Return integer <0 if KO, Id of created object if OK
585
     */
586
    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)
587
    {
588
        global $mysoc, $conf, $langs, $user;
589
590
        $logtext = "::addLine bomid=$this->id, qty=$qty, fk_product=$fk_product, qty_frozen=$qty_frozen, disable_stock_change=$disable_stock_change, efficiency=$efficiency";
591
        $logtext .= ", fk_bom_child=$fk_bom_child, import_key=$import_key";
592
        dol_syslog(get_class($this) . $logtext, LOG_DEBUG);
593
594
        if ($this->statut == self::STATUS_DRAFT) {
0 ignored issues
show
Deprecated Code introduced by
The property Dolibarr\Core\Base\CommonObject::$statut has been deprecated: Use $status instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

594
        if (/** @scrutinizer ignore-deprecated */ $this->statut == self::STATUS_DRAFT) {

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
595
            include_once DOL_DOCUMENT_ROOT . '/core/lib/price.lib.php';
596
597
            // Clean parameters
598
            if (empty($qty)) {
599
                $qty = 0;
600
            }
601
            if (empty($qty_frozen)) {
602
                $qty_frozen = 0;
603
            }
604
            if (empty($disable_stock_change)) {
605
                $disable_stock_change = 0;
606
            }
607
            if (empty($efficiency)) {
608
                $efficiency = 1.0;
609
            }
610
            if (empty($fk_bom_child)) {
611
                $fk_bom_child = null;
612
            }
613
            if (empty($import_key)) {
614
                $import_key = null;
615
            }
616
            if (empty($position)) {
617
                $position = -1;
618
            }
619
620
            $qty = (float) price2num($qty);
621
            $efficiency = (float) price2num($efficiency);
622
            $position = (float) price2num($position);
623
624
            $this->db->begin();
625
626
            // Rank to use
627
            $rangMax = $this->line_max();
628
            $rankToUse = $position;
629
            if ($rankToUse <= 0 or $rankToUse > $rangMax) { // New line after existing lines
630
                $rankToUse = $rangMax + 1;
631
            } else { // New line between the existing lines
632
                foreach ($this->lines as $bl) {
633
                    if ($bl->position >= $rankToUse) {
634
                        $bl->position++;
635
                        $bl->update($user);
636
                    }
637
                }
638
            }
639
640
            // Insert line
641
            $line = new BOMLine($this->db);
642
643
            $line->context = $this->context;
644
645
            $line->fk_bom = $this->id;
646
            $line->fk_product = $fk_product;
647
            $line->qty = $qty;
648
            $line->qty_frozen = $qty_frozen;
649
            $line->disable_stock_change = $disable_stock_change;
650
            $line->efficiency = $efficiency;
651
            $line->fk_bom_child = $fk_bom_child;
652
            $line->import_key = $import_key;
653
            $line->position = $rankToUse;
654
            $line->fk_unit = $fk_unit;
655
            $line->fk_default_workstation = $fk_default_workstation;
656
657
            if (is_array($array_options) && count($array_options) > 0) {
658
                $line->array_options = $array_options;
659
            }
660
661
            $result = $line->create($user);
662
663
            if ($result > 0) {
664
                $this->calculateCosts();
665
                $this->db->commit();
666
                return $result;
667
            } else {
668
                $this->setErrorsFromObject($line);
669
                dol_syslog(get_class($this) . "::addLine error=" . $this->error, LOG_ERR);
670
                $this->db->rollback();
671
                return -2;
672
            }
673
        } else {
674
            dol_syslog(get_class($this) . "::addLine status of BOM must be Draft to allow use of ->addLine()", LOG_ERR);
675
            return -3;
676
        }
677
    }
678
679
    /**
680
     * Update an BOM line into database
681
     *
682
     * @param   int     $rowid                  Id of line to update
683
     * @param   float   $qty                    Quantity
684
     * @param   float   $qty_frozen             Frozen quantity
685
     * @param   int     $disable_stock_change   Disable stock change on using in MO
686
     * @param   float   $efficiency             Efficiency in MO
687
     * @param   int     $position               Position of BOM-Line in BOM-Lines
688
     * @param   string  $import_key             Import Key
689
     * @param   int     $fk_unit                Unit of line
690
     * @param   array   $array_options          extrafields array
691
     * @param   int     $fk_default_workstation Default workstation
692
     * @return  int                             Return integer <0 if KO, Id of updated BOM-Line if OK
693
     */
694
    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)
695
    {
696
        global $user;
697
698
        $logtext = "::updateLine bomid=$this->id, qty=$qty, qty_frozen=$qty_frozen, disable_stock_change=$disable_stock_change, efficiency=$efficiency";
699
        $logtext .= ", import_key=$import_key";
700
        dol_syslog(get_class($this) . $logtext, LOG_DEBUG);
701
702
        if ($this->statut == self::STATUS_DRAFT) {
0 ignored issues
show
Deprecated Code introduced by
The property Dolibarr\Core\Base\CommonObject::$statut has been deprecated: Use $status instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

702
        if (/** @scrutinizer ignore-deprecated */ $this->statut == self::STATUS_DRAFT) {

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
703
            include_once DOL_DOCUMENT_ROOT . '/core/lib/price.lib.php';
704
705
            // Clean parameters
706
            if (empty($qty)) {
707
                $qty = 0;
708
            }
709
            if (empty($qty_frozen)) {
710
                $qty_frozen = 0;
711
            }
712
            if (empty($disable_stock_change)) {
713
                $disable_stock_change = 0;
714
            }
715
            if (empty($efficiency)) {
716
                $efficiency = 1.0;
717
            }
718
            if (empty($import_key)) {
719
                $import_key = null;
720
            }
721
            if (empty($position)) {
722
                $position = -1;
723
            }
724
725
            $qty = (float) price2num($qty);
726
            $efficiency = (float) price2num($efficiency);
727
            $position = (float) price2num($position);
728
729
            $this->db->begin();
730
731
            //Fetch current line from the database and then clone the object and set it in $oldline property
732
            $line = new BOMLine($this->db);
733
            $line->fetch($rowid);
734
            $line->fetch_optionals();
735
736
            $staticLine = clone $line;
737
            $line->oldcopy = $staticLine;
738
            $line->context = $this->context;
739
740
            // Rank to use
741
            $rankToUse = (int) $position;
742
            if ($rankToUse != $line->oldcopy->position) { // check if position have a new value
743
                foreach ($this->lines as $bl) {
744
                    if ($bl->position >= $rankToUse and $bl->position < ($line->oldcopy->position + 1)) { // move rank up
745
                        $bl->position++;
746
                        $bl->update($user);
747
                    }
748
                    if ($bl->position <= $rankToUse and $bl->position > ($line->oldcopy->position)) { // move rank down
749
                        $bl->position--;
750
                        $bl->update($user);
751
                    }
752
                }
753
            }
754
755
756
            $line->fk_bom = $this->id;
757
            $line->qty = $qty;
758
            $line->qty_frozen = $qty_frozen;
759
            $line->disable_stock_change = $disable_stock_change;
760
            $line->efficiency = $efficiency;
761
            $line->import_key = $import_key;
762
            $line->position = $rankToUse;
763
764
765
            if (!empty($fk_unit)) {
766
                $line->fk_unit = $fk_unit;
767
            }
768
769
770
            if (is_array($array_options) && count($array_options) > 0) {
771
                // We replace values in this->line->array_options only for entries defined into $array_options
772
                foreach ($array_options as $key => $value) {
773
                    $line->array_options[$key] = $array_options[$key];
774
                }
775
            }
776
            if ($line->fk_default_workstation != $fk_default_workstation) {
777
                $line->fk_default_workstation = ($fk_default_workstation > 0 ? $fk_default_workstation : 0);
778
            }
779
780
            $result = $line->update($user);
781
782
            if ($result > 0) {
783
                $this->calculateCosts();
784
                $this->db->commit();
785
                return $result;
786
            } else {
787
                $this->setErrorsFromObject($line);
788
                dol_syslog(get_class($this) . "::addLine error=" . $this->error, LOG_ERR);
789
                $this->db->rollback();
790
                return -2;
791
            }
792
        } else {
793
            dol_syslog(get_class($this) . "::addLine status of BOM must be Draft to allow use of ->addLine()", LOG_ERR);
794
            return -3;
795
        }
796
    }
797
798
    /**
799
     *  Delete a line of object in database
800
     *
801
     *  @param  User    $user       User that delete
802
     *  @param  int     $idline     Id of line to delete
803
     *  @param  int     $notrigger  0=launch triggers after, 1=disable triggers
804
     *  @return int                 >0 if OK, <0 if KO
805
     */
806
    public function deleteLine(User $user, $idline, $notrigger = 0)
807
    {
808
        if ($this->status < 0) {
809
            $this->error = 'ErrorDeleteLineNotAllowedByObjectStatus';
810
            return -2;
811
        }
812
813
        $this->db->begin();
814
815
        //Fetch current line from the database and then clone the object and set it in $oldline property
816
        $line = new BOMLine($this->db);
817
        $line->fetch($idline);
818
        $line->fetch_optionals();
819
820
        $staticLine = clone $line;
821
        $line->oldcopy = $staticLine;
822
        $line->context = $this->context;
823
824
        $result = $line->delete($user, $notrigger);
825
826
        //Positions (rank) reordering
827
        foreach ($this->lines as $bl) {
828
            if ($bl->position > ($line->oldcopy->position)) { // move rank down
829
                $bl->position--;
830
                $bl->update($user);
831
            }
832
        }
833
834
        if ($result > 0) {
835
            $this->calculateCosts();
836
            $this->db->commit();
837
            return $result;
838
        } else {
839
            $this->setErrorsFromObject($line);
840
            dol_syslog(get_class($this) . "::addLine error=" . $this->error, LOG_ERR);
841
            $this->db->rollback();
842
            return -2;
843
        }
844
    }
845
846
    /**
847
     *  Returns the reference to the following non used BOM depending on the active numbering module
848
     *  defined into BOM_ADDON
849
     *
850
     *  @param  Product     $prod   Object product
851
     *  @return string              BOM free reference
852
     */
853
    public function getNextNumRef($prod)
854
    {
855
        global $langs, $conf;
856
        $langs->load("mrp");
857
858
        if (getDolGlobalString('BOM_ADDON')) {
859
            $mybool = false;
860
861
            $file = getDolGlobalString('BOM_ADDON') . ".php";
862
            $classname = getDolGlobalString('BOM_ADDON');
863
864
            // Include file with class
865
            $dirmodels = array_merge(array('/'), (array) $conf->modules_parts['models']);
866
            foreach ($dirmodels as $reldir) {
867
                $dir = dol_buildpath($reldir . "core/modules/bom/");
868
869
                // Load file with numbering class (if found)
870
                $mybool = ((bool) @include_once $dir . $file) || $mybool;
871
            }
872
873
            if ($mybool === false) {
874
                dol_print_error(null, "Failed to include file " . $file);
875
                return '';
876
            }
877
878
            $obj = new $classname();
879
            $numref = $obj->getNextValue($prod, $this);
880
881
            if ($numref != "") {
882
                return $numref;
883
            } else {
884
                $this->error = $obj->error;
885
                //dol_print_error($this->db,get_class($this)."::getNextNumRef ".$obj->error);
886
                return "";
887
            }
888
        } else {
889
            print $langs->trans("Error") . " " . $langs->trans("Error_BOM_ADDON_NotDefined");
890
            return "";
891
        }
892
    }
893
894
    /**
895
     *  Validate bom
896
     *
897
     *  @param      User    $user           User making status change
898
     *  @param      int     $notrigger      1=Does not execute triggers, 0= execute triggers
899
     *  @return     int                     Return integer <=0 if OK, 0=Nothing done, >0 if KO
900
     */
901
    public function validate($user, $notrigger = 0)
902
    {
903
        global $conf;
904
905
        require_once constant('DOL_DOCUMENT_ROOT') . '/core/lib/files.lib.php';
906
907
        $error = 0;
908
909
        // Protection
910
        if ($this->status == self::STATUS_VALIDATED) {
911
            dol_syslog(get_class($this) . "::validate action abandoned: already validated", LOG_WARNING);
912
            return 0;
913
        }
914
915
        $now = dol_now();
916
917
        $this->db->begin();
918
919
        // Define new ref
920
        if (!$error && (preg_match('/^[\(]?PROV/i', $this->ref) || empty($this->ref))) { // empty should not happened, but when it occurs, the test save life
921
            $this->fetch_product();
922
            $num = $this->getNextNumRef($this->product);
923
        } else {
924
            $num = $this->ref;
925
        }
926
        $this->newref = dol_sanitizeFileName($num);
927
928
        // Validate
929
        $sql = "UPDATE " . MAIN_DB_PREFIX . $this->table_element;
930
        $sql .= " SET ref = '" . $this->db->escape($num) . "',";
931
        $sql .= " status = " . self::STATUS_VALIDATED . ",";
932
        $sql .= " date_valid='" . $this->db->idate($now) . "',";
933
        $sql .= " fk_user_valid = " . ((int) $user->id);
934
        $sql .= " WHERE rowid = " . ((int) $this->id);
935
936
        dol_syslog(get_class($this) . "::validate()", LOG_DEBUG);
937
        $resql = $this->db->query($sql);
938
        if (!$resql) {
939
            dol_print_error($this->db);
940
            $this->error = $this->db->lasterror();
941
            $error++;
942
        }
943
944
        if (!$error && !$notrigger) {
945
            // Call trigger
946
            $result = $this->call_trigger('BOM_VALIDATE', $user);
947
            if ($result < 0) {
948
                $error++;
949
            }
950
            // End call triggers
951
        }
952
953
        if (!$error) {
954
            $this->oldref = $this->ref;
955
956
            // Rename directory if dir was a temporary ref
957
            if (preg_match('/^[\(]?PROV/i', $this->ref)) {
958
                // Now we rename also files into index
959
                $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) . "'";
960
                $sql .= " WHERE filename LIKE '" . $this->db->escape($this->ref) . "%' AND filepath = 'bom/" . $this->db->escape($this->ref) . "' and entity = " . $conf->entity;
961
                $resql = $this->db->query($sql);
962
                if (!$resql) {
963
                    $error++;
964
                    $this->error = $this->db->lasterror();
965
                }
966
                $sql = 'UPDATE ' . MAIN_DB_PREFIX . "ecm_files set filepath = 'bom/" . $this->db->escape($this->newref) . "'";
967
                $sql .= " WHERE filepath = 'bom/" . $this->db->escape($this->ref) . "' and entity = " . $conf->entity;
968
                $resql = $this->db->query($sql);
969
                if (!$resql) {
970
                    $error++;
971
                    $this->error = $this->db->lasterror();
972
                }
973
974
                // We rename directory ($this->ref = old ref, $num = new ref) in order not to lose the attachments
975
                $oldref = dol_sanitizeFileName($this->ref);
976
                $newref = dol_sanitizeFileName($num);
977
                $dirsource = $conf->bom->dir_output . '/' . $oldref;
978
                $dirdest = $conf->bom->dir_output . '/' . $newref;
979
                if (!$error && file_exists($dirsource)) {
980
                    dol_syslog(get_class($this) . "::validate() rename dir " . $dirsource . " into " . $dirdest);
981
982
                    if (@rename($dirsource, $dirdest)) {
983
                        dol_syslog("Rename ok");
984
                        // Rename docs starting with $oldref with $newref
985
                        $listoffiles = dol_dir_list($conf->bom->dir_output . '/' . $newref, 'files', 1, '^' . preg_quote($oldref, '/'));
986
                        foreach ($listoffiles as $fileentry) {
987
                            $dirsource = $fileentry['name'];
988
                            $dirdest = preg_replace('/^' . preg_quote($oldref, '/') . '/', $newref, $dirsource);
989
                            $dirsource = $fileentry['path'] . '/' . $dirsource;
990
                            $dirdest = $fileentry['path'] . '/' . $dirdest;
991
                            @rename($dirsource, $dirdest);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for rename(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

991
                            /** @scrutinizer ignore-unhandled */ @rename($dirsource, $dirdest);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
992
                        }
993
                    }
994
                }
995
            }
996
        }
997
998
        // Set new ref and current status
999
        if (!$error) {
1000
            $this->ref = $num;
1001
            $this->status = self::STATUS_VALIDATED;
1002
        }
1003
1004
        if (!$error) {
1005
            $this->db->commit();
1006
            return 1;
1007
        } else {
1008
            $this->db->rollback();
1009
            return -1;
1010
        }
1011
    }
1012
1013
    /**
1014
     *  Set draft status
1015
     *
1016
     *  @param  User    $user           Object user that modify
1017
     *  @param  int     $notrigger      1=Does not execute triggers, 0=Execute triggers
1018
     *  @return int                     Return integer <0 if KO, >0 if OK
1019
     */
1020
    public function setDraft($user, $notrigger = 0)
1021
    {
1022
        // Protection
1023
        if ($this->status <= self::STATUS_DRAFT) {
1024
            return 0;
1025
        }
1026
1027
        return $this->setStatusCommon($user, self::STATUS_DRAFT, $notrigger, 'BOM_UNVALIDATE');
1028
    }
1029
1030
    /**
1031
     *  Set cancel status
1032
     *
1033
     *  @param  User    $user           Object user that modify
1034
     *  @param  int     $notrigger      1=Does not execute triggers, 0=Execute triggers
1035
     *  @return int                     Return integer <0 if KO, 0=Nothing done, >0 if OK
1036
     */
1037
    public function cancel($user, $notrigger = 0)
1038
    {
1039
        // Protection
1040
        if ($this->status != self::STATUS_VALIDATED) {
1041
            return 0;
1042
        }
1043
1044
        return $this->setStatusCommon($user, self::STATUS_CANCELED, $notrigger, 'BOM_CLOSE');
1045
    }
1046
1047
    /**
1048
     *  Set cancel status
1049
     *
1050
     *  @param  User    $user           Object user that modify
1051
     *  @param  int     $notrigger      1=Does not execute triggers, 0=Execute triggers
1052
     *  @return int                     Return integer <0 if KO, 0=Nothing done, >0 if OK
1053
     */
1054
    public function reopen($user, $notrigger = 0)
1055
    {
1056
        // Protection
1057
        if ($this->status != self::STATUS_CANCELED) {
1058
            return 0;
1059
        }
1060
1061
        return $this->setStatusCommon($user, self::STATUS_VALIDATED, $notrigger, 'BOM_REOPEN');
1062
    }
1063
1064
    /**
1065
     * getTooltipContentArray
1066
     * @param array $params params to construct tooltip data
1067
     * @since v18
1068
     * @return array
1069
     */
1070
    public function getTooltipContentArray($params)
1071
    {
1072
        global $conf, $langs, $user;
1073
1074
        $langs->loadLangs(['product', 'mrp']);
1075
1076
        $datas = [];
1077
1078
        if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
1079
            return ['optimize' => $langs->trans("ShowBillOfMaterials")];
1080
        }
1081
        $picto = img_picto('', $this->picto) . ' <u class="paddingrightonly">' . $langs->trans("BillOfMaterials") . '</u>';
1082
        if (isset($this->status)) {
1083
            $picto .= ' ' . $this->getLibStatut(5);
1084
        }
1085
        $datas['picto'] = $picto;
1086
        $datas['ref'] = '<br><b>' . $langs->trans('Ref') . ':</b> ' . $this->ref;
1087
        if (isset($this->label)) {
1088
            $datas['label'] = '<br><b>' . $langs->trans('Label') . ':</b> ' . $this->label;
1089
        }
1090
        if (!empty($this->fk_product) && $this->fk_product > 0) {
1091
                        $product = new Product($this->db);
1092
            $resultFetch = $product->fetch($this->fk_product);
1093
            if ($resultFetch > 0) {
1094
                $datas['product'] = "<br><b>" . $langs->trans("Product") . '</b>: ' . $product->ref . ' - ' . $product->label;
1095
            }
1096
        }
1097
1098
        return $datas;
1099
    }
1100
1101
    /**
1102
     *  Return a link to the object card (with optionally the picto)
1103
     *
1104
     *  @param  int     $withpicto                  Include picto in link (0=No picto, 1=Include picto into link, 2=Only picto)
1105
     *  @param  string  $option                     On what the link point to ('nolink', ...)
1106
     *  @param  int     $notooltip                  1=Disable tooltip
1107
     *  @param  string  $morecss                    Add more css on link
1108
     *  @param  int     $save_lastsearch_value      -1=Auto, 0=No save of lastsearch_values when clicking, 1=Save lastsearch_values whenclicking
1109
     *  @return string                              String with URL
1110
     */
1111
    public function getNomUrl($withpicto = 0, $option = '', $notooltip = 0, $morecss = '', $save_lastsearch_value = -1)
1112
    {
1113
        global $db, $conf, $langs, $hookmanager;
1114
1115
        if (!empty($conf->dol_no_mouse_hover)) {
1116
            $notooltip = 1; // Force disable tooltips
1117
        }
1118
1119
        $result = '';
1120
        $params = [
1121
            'id' => $this->id,
1122
            'objecttype' => $this->element,
1123
            'option' => $option,
1124
        ];
1125
        $classfortooltip = 'classfortooltip';
1126
        $dataparams = '';
1127
        if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
1128
            $classfortooltip = 'classforajaxtooltip';
1129
            $dataparams = ' data-params="' . dol_escape_htmltag(json_encode($params)) . '"';
1130
            $label = '';
1131
        } else {
1132
            $label = implode($this->getTooltipContentArray($params));
1133
        }
1134
1135
        $url = constant('BASE_URL') . '/bom/bom_card.php?id=' . $this->id;
1136
1137
        if ($option != 'nolink') {
1138
            // Add param to save lastsearch_values or not
1139
            $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
1140
            if ($save_lastsearch_value == -1 && isset($_SERVER["PHP_SELF"]) && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
1141
                $add_save_lastsearch_values = 1;
1142
            }
1143
            if ($add_save_lastsearch_values) {
1144
                $url .= '&save_lastsearch_values=1';
1145
            }
1146
        }
1147
1148
        $linkclose = '';
1149
        if (empty($notooltip)) {
1150
            if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
1151
                $label = $langs->trans("ShowBillOfMaterials");
1152
                $linkclose .= ' alt="' . dol_escape_htmltag($label, 1) . '"';
1153
            }
1154
            $linkclose .= ($label ? ' title="' . dol_escape_htmltag($label, 1) . '"' : ' title="tocomplete"');
1155
            $linkclose .= $dataparams . ' class="' . $classfortooltip . ($morecss ? ' ' . $morecss : '') . '"';
1156
        } else {
1157
            $linkclose = ($morecss ? ' class="' . $morecss . '"' : '');
1158
        }
1159
1160
        $linkstart = '<a href="' . $url . '"';
1161
        $linkstart .= $linkclose . '>';
1162
        $linkend = '</a>';
1163
1164
        $result .= $linkstart;
1165
        if ($withpicto) {
1166
            $result .= img_object(($notooltip ? '' : $label), ($this->picto ? $this->picto : 'generic'), (($withpicto != 2) ? 'class="paddingright"' : ''), 0, 0, $notooltip ? 0 : 1);
1167
        }
1168
        if ($withpicto != 2) {
1169
            $result .= $this->ref;
1170
        }
1171
        $result .= $linkend;
1172
        //if ($withpicto != 2) $result.=(($addlabel && $this->label) ? $sep . dol_trunc($this->label, ($addlabel > 1 ? $addlabel : 0)) : '');
1173
1174
        global $action, $hookmanager;
1175
        $hookmanager->initHooks(array('bomdao'));
1176
        $parameters = array('id' => $this->id, 'getnomurl' => &$result);
1177
        $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
1178
        if ($reshook > 0) {
1179
            $result = $hookmanager->resPrint;
1180
        } else {
1181
            $result .= $hookmanager->resPrint;
1182
        }
1183
1184
        return $result;
1185
    }
1186
1187
    /**
1188
     *  Return label of the status
1189
     *
1190
     *  @param  int     $mode          0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=Short label + Picto, 6=Long label + Picto
1191
     *  @return string                 Label of status
1192
     */
1193
    public function getLibStatut($mode = 0)
1194
    {
1195
        return $this->LibStatut($this->status, $mode);
1196
    }
1197
1198
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1199
    /**
1200
     *  Return the status
1201
     *
1202
     *  @param  int     $status        Id status
1203
     *  @param  int     $mode          0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=Short label + Picto, 6=Long label + Picto
1204
     *  @return string                 Label of status
1205
     */
1206
    public function LibStatut($status, $mode = 0)
1207
    {
1208
		// phpcs:enable
1209
        if (empty($this->labelStatus)) {
1210
            global $langs;
1211
            //$langs->load("mrp");
1212
            $this->labelStatus[self::STATUS_DRAFT] = $langs->transnoentitiesnoconv('Draft');
1213
            $this->labelStatus[self::STATUS_VALIDATED] = $langs->transnoentitiesnoconv('Enabled');
1214
            $this->labelStatus[self::STATUS_CANCELED] = $langs->transnoentitiesnoconv('Disabled');
1215
        }
1216
1217
        $statusType = 'status' . $status;
1218
        if ($status == self::STATUS_VALIDATED) {
1219
            $statusType = 'status4';
1220
        }
1221
        if ($status == self::STATUS_CANCELED) {
1222
            $statusType = 'status6';
1223
        }
1224
1225
        return dolGetStatus($this->labelStatus[$status], $this->labelStatus[$status], '', $statusType, $mode);
1226
    }
1227
1228
    /**
1229
     *  Load the info information in the object
1230
     *
1231
     *  @param  int     $id       Id of object
1232
     *  @return void
1233
     */
1234
    public function info($id)
1235
    {
1236
        $sql = 'SELECT rowid, date_creation as datec, tms as datem,';
1237
        $sql .= ' fk_user_creat, fk_user_modif';
1238
        $sql .= ' FROM ' . MAIN_DB_PREFIX . $this->table_element . ' as t';
1239
        $sql .= ' WHERE t.rowid = ' . ((int) $id);
1240
        $result = $this->db->query($sql);
1241
        if ($result) {
1242
            if ($this->db->num_rows($result)) {
1243
                $obj = $this->db->fetch_object($result);
1244
1245
                $this->id = $obj->rowid;
1246
1247
                $this->user_creation_id = $obj->fk_user_creat;
1248
                $this->user_modification_id = $obj->fk_user_modif;
1249
                $this->date_creation     = $this->db->jdate($obj->datec);
1250
                $this->date_modification = empty($obj->datem) ? '' : $this->db->jdate($obj->datem);
1251
            }
1252
1253
            $this->db->free($result);
1254
        } else {
1255
            dol_print_error($this->db);
1256
        }
1257
    }
1258
1259
    /**
1260
     *  Create an array of lines
1261
     *
1262
     *  @return array|int       array of lines if OK, <0 if KO
1263
     */
1264
    public function getLinesArray()
1265
    {
1266
        $this->lines = array();
1267
1268
        $objectline = new BOMLine($this->db);
1269
        $result = $objectline->fetchAll('ASC', 'position', 0, 0, '(fk_bom:=:' . ((int) $this->id) . ')');
1270
1271
        if (is_numeric($result)) {
1272
            $this->error = $objectline->error;
1273
            $this->errors = $objectline->errors;
1274
            return $result;
1275
        } else {
1276
            $this->lines = $result;
1277
            return $this->lines;
1278
        }
1279
    }
1280
1281
    /**
1282
     *  Create a document onto disk according to template module.
1283
     *
1284
     *  @param      string      $modele         Force template to use ('' to not force)
1285
     *  @param      Translate   $outputlangs    object lang a utiliser pour traduction
1286
     *  @param      int         $hidedetails    Hide details of lines
1287
     *  @param      int         $hidedesc       Hide description
1288
     *  @param      int         $hideref        Hide ref
1289
     *  @param      null|array  $moreparams     Array to provide more information
1290
     *  @return     int                         0 if KO, 1 if OK
1291
     */
1292
    public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0, $moreparams = null)
1293
    {
1294
        global $conf, $langs;
1295
1296
        $langs->load("mrp");
1297
        $outputlangs->load("products");
1298
1299
        if (!dol_strlen($modele)) {
1300
            $modele = '';
1301
1302
            if ($this->model_pdf) {
1303
                $modele = $this->model_pdf;
1304
            } elseif (getDolGlobalString('BOM_ADDON_PDF')) {
1305
                $modele = getDolGlobalString('BOM_ADDON_PDF');
1306
            }
1307
        }
1308
1309
        $modelpath = "core/modules/bom/doc/";
1310
        if (!empty($modele)) {
1311
            return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams);
1312
        } else {
1313
            return 0;
1314
        }
1315
    }
1316
1317
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1318
    /**
1319
     *  Return if at least one photo is available
1320
     *
1321
     * @param  string $sdir Directory to scan
1322
     * @return boolean                 True if at least one photo is available, False if not
1323
     */
1324
    public function is_photo_available($sdir)
1325
    {
1326
		// phpcs:enable
1327
        include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
1328
        include_once DOL_DOCUMENT_ROOT . '/core/lib/images.lib.php';
1329
1330
        $sdir .= '/' . get_exdir(0, 0, 0, 0, $this, 'bom');
1331
1332
        $dir_osencoded = dol_osencode($sdir);
1333
        if (file_exists($dir_osencoded)) {
1334
            $handle = opendir($dir_osencoded);
1335
            if (is_resource($handle)) {
1336
                while (($file = readdir($handle)) !== false) {
1337
                    if (!utf8_check($file)) {
1338
                        $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // To be sure data is stored in UTF8 in memory
1339
                    }
1340
                    if (dol_is_file($sdir . $file) && image_format_supported($file) >= 0) {
1341
                        return true;
1342
                    }
1343
                }
1344
            }
1345
        }
1346
        return false;
1347
    }
1348
1349
    /**
1350
     * Initialise object with example values
1351
     * Id must be 0 if object instance is a specimen
1352
     *
1353
     * @return int
1354
     */
1355
    public function initAsSpecimen()
1356
    {
1357
        $this->initAsSpecimenCommon();
1358
        $this->ref = 'BOM-123';
1359
        $this->date_creation = dol_now() - 20000;
1360
1361
        return 1;
1362
    }
1363
1364
1365
    /**
1366
     * Action executed by scheduler
1367
     * CAN BE A CRON TASK. In such a case, parameters come from the schedule job setup field 'Parameters'
1368
     *
1369
     * @return  int         0 if OK, <>0 if KO (this function is used also by cron so only 0 is OK)
1370
     */
1371
    public function doScheduledJob()
1372
    {
1373
        global $conf, $langs;
1374
1375
        //$conf->global->SYSLOG_FILE = 'DOL_DATA_ROOT/dolibarr_mydedicatedlofile.log';
1376
1377
        $error = 0;
1378
        $this->output = '';
1379
        $this->error = '';
1380
1381
        dol_syslog(__METHOD__, LOG_DEBUG);
1382
1383
        $now = dol_now();
1384
1385
        $this->db->begin();
1386
1387
        // ...
1388
1389
        $this->db->commit();
1390
1391
        return $error;
1392
    }
1393
1394
    /**
1395
     * BOM costs calculation based on cost_price or pmp of each BOM line.
1396
     * Set the property ->total_cost and ->unit_cost of BOM.
1397
     *
1398
     * @return int|string   Return integer <0 if KO, >0 if OK, or printable error result from hook
1399
     */
1400
    public function calculateCosts()
1401
    {
1402
        global $conf, $hookmanager;
1403
1404
                $this->unit_cost = 0;
1405
        $this->total_cost = 0;
1406
1407
        $parameters = array();
1408
        $reshook = $hookmanager->executeHooks('calculateCostsBom', $parameters, $this); // Note that $action and $object may have been modified by hook
1409
1410
        if ($reshook > 0) {
1411
            return $hookmanager->resPrint;
1412
        }
1413
1414
        if (is_array($this->lines) && count($this->lines)) {
1415
            $productFournisseur = new ProductFournisseur($this->db);
1416
            $tmpproduct = new Product($this->db);
1417
1418
            foreach ($this->lines as &$line) {
1419
                $tmpproduct->cost_price = 0;
1420
                $tmpproduct->pmp = 0;
1421
                $result = $tmpproduct->fetch($line->fk_product, '', '', '', 0, 1, 1);   // We discard selling price and language loading
1422
1423
                if ($tmpproduct->type == $tmpproduct::TYPE_PRODUCT) {
1424
                    if (empty($line->fk_bom_child)) {
1425
                        if ($result < 0) {
1426
                            $this->error = $tmpproduct->error;
1427
                            return -1;
1428
                        }
1429
                        $unit_cost = (!empty($tmpproduct->cost_price)) ? $tmpproduct->cost_price : $tmpproduct->pmp;
1430
                        $line->unit_cost = (float) price2num($unit_cost);
1431
                        if (empty($line->unit_cost)) {
1432
                            if ($productFournisseur->find_min_price_product_fournisseur($line->fk_product) > 0) {
1433
                                if ($productFournisseur->fourn_remise_percent != "0") {
1434
                                    $line->unit_cost = $productFournisseur->fourn_unitprice_with_discount;
1435
                                } else {
1436
                                    $line->unit_cost = $productFournisseur->fourn_unitprice;
1437
                                }
1438
                            }
1439
                        }
1440
1441
                        $line->total_cost = (float) price2num($line->qty * $line->unit_cost, 'MT');
1442
1443
                        $this->total_cost += $line->total_cost;
1444
                    } else {
1445
                        $bom_child = new BOM($this->db);
1446
                        $res = $bom_child->fetch($line->fk_bom_child);
1447
                        if ($res > 0) {
1448
                            $bom_child->calculateCosts();
1449
                            $line->childBom[] = $bom_child;
1450
                            $this->total_cost += (float) price2num($bom_child->total_cost * $line->qty, 'MT');
1451
                            $this->total_cost += $line->total_cost;
1452
                        } else {
1453
                            $this->error = $bom_child->error;
1454
                            return -2;
1455
                        }
1456
                    }
1457
                } else {
1458
                    // Convert qty of line into hours
1459
                    $unitforline = measuringUnitString($line->fk_unit, '', '', 1);
1460
                    $qtyhourforline = convertDurationtoHour($line->qty, $unitforline);
1461
1462
                    if (isModEnabled('workstation') && !empty($line->fk_default_workstation)) {
1463
                        $workstation = new Workstation($this->db);
1464
                        $res = $workstation->fetch($line->fk_default_workstation);
1465
1466
                        if ($res > 0) {
1467
                            $line->total_cost = (float) price2num($qtyhourforline * ($workstation->thm_operator_estimated + $workstation->thm_machine_estimated), 'MT');
1468
                        } else {
1469
                            $this->error = $workstation->error;
1470
                            return -3;
1471
                        }
1472
                    } else {
1473
                        $defaultdurationofservice = $tmpproduct->duration;
1474
                        $reg = array();
1475
                        $qtyhourservice = 0;
1476
                        if (preg_match('/^(\d+)([a-z]+)$/', $defaultdurationofservice, $reg)) {
1477
                            $qtyhourservice = convertDurationtoHour($reg[1], $reg[2]);
1478
                        }
1479
1480
                        if ($qtyhourservice) {
1481
                            $line->total_cost = (float) price2num($qtyhourforline / $qtyhourservice * $tmpproduct->cost_price, 'MT');
1482
                        } else {
1483
                            $line->total_cost = (float) price2num($line->qty * $tmpproduct->cost_price, 'MT');
1484
                        }
1485
                    }
1486
1487
                    $this->total_cost += $line->total_cost;
1488
                }
1489
            }
1490
1491
            $this->total_cost = (float) price2num($this->total_cost, 'MT');
1492
1493
            if ($this->qty > 0) {
1494
                $this->unit_cost = (float) price2num($this->total_cost / $this->qty, 'MU');
1495
            } elseif ($this->qty < 0) {
1496
                $this->unit_cost = (float) price2num($this->total_cost * $this->qty, 'MU');
1497
            }
1498
        }
1499
1500
        return 1;
1501
    }
1502
1503
    /**
1504
     * Function used to replace a product id with another one.
1505
     *
1506
     * @param DoliDB $db Database handler
1507
     * @param int $origin_id Old product id
1508
     * @param int $dest_id New product id
1509
     * @return bool
1510
     */
1511
    public static function replaceProduct(DoliDB $db, $origin_id, $dest_id)
1512
    {
1513
        $tables = array(
1514
            'bom_bomline'
1515
        );
1516
1517
        return CommonObject::commonReplaceProduct($db, $origin_id, $dest_id, $tables);
1518
    }
1519
1520
    /**
1521
     * Get Net needs by product
1522
     *
1523
     * @param array<int,array{qty:float,fk_unit:?int}>  $TNetNeeds  Array of ChildBom and infos linked to
1524
     * @param float                                     $qty        qty needed (used as a factor to produce 1 unit)
1525
     * @return void
1526
     */
1527
    public function getNetNeeds(&$TNetNeeds = array(), $qty = 0)
1528
    {
1529
        if (!empty($this->lines)) {
1530
            foreach ($this->lines as $line) {
1531
                if (!empty($line->childBom)) {
1532
                    foreach ($line->childBom as $childBom) {
1533
                        $childBom->getNetNeeds($TNetNeeds, $line->qty * $qty);
1534
                    }
1535
                } else {
1536
                    if (empty($TNetNeeds[$line->fk_product]['qty'])) {
1537
                        $TNetNeeds[$line->fk_product]['qty'] = 0.0;
1538
                    }
1539
                    // When using nested level (or not), the qty for needs must always use the same unit to be able to be cumulated.
1540
                    // So if unit in bom is not the same than default, we must recalculate qty after units comparisons.
1541
                    $TNetNeeds[$line->fk_product]['fk_unit'] = $line->fk_unit;
1542
                    $TNetNeeds[$line->fk_product]['qty'] += $line->qty * $qty;
1543
                }
1544
            }
1545
        }
1546
    }
1547
1548
    /**
1549
     * Get/add Net needs Tree by product or bom
1550
     *
1551
     * @param array<int,array{product:array,bom:BOM,parentid:int,qty:float,level:int,fk_unit:?int}>     $TNetNeeds  Array of ChildBom and infos linked to
1552
     * @param float     $qty       qty needed (used as a factor to produce 1 unit)
1553
     * @param int       $level     level of recursivity
1554
     * @return void
1555
     */
1556
    public function getNetNeedsTree(&$TNetNeeds = array(), $qty = 0, $level = 0)
1557
    {
1558
        if (!empty($this->lines)) {
1559
            foreach ($this->lines as $line) {
1560
                if (!empty($line->childBom)) {
1561
                    foreach ($line->childBom as $childBom) {
1562
                        $TNetNeeds[$childBom->id]['bom'] = $childBom;
1563
                        $TNetNeeds[$childBom->id]['parentid'] = $this->id;
1564
                        // When using nested level (or not), the qty for needs must always use the same unit to be able to be cumulated.
1565
                        // So if unit in bom is not the same than default, we must recalculate qty after units comparisons.
1566
                        //$TNetNeeds[$childBom->id]['fk_unit'] = $line->fk_unit;
1567
                        $TNetNeeds[$childBom->id]['qty'] = $line->qty * $qty;
1568
                        $TNetNeeds[$childBom->id]['level'] = $level;
1569
                        $childBom->getNetNeedsTree($TNetNeeds, $line->qty * $qty, $level + 1);
1570
                    }
1571
                } else {
1572
                    // When using nested level (or not), the qty for needs must always use the same unit to be able to be cumulated.
1573
                    // So if unit in bom is not the same than default, we must recalculate qty after units comparisons.
1574
                    if (!isset($TNetNeeds[$this->id]['product'])) {
1575
                        $TNetNeeds[$this->id]['product'] = array();
1576
                    }
1577
                    if (!isset($TNetNeeds[$this->id]['product'][$line->fk_product])) {
1578
                        $TNetNeeds[$this->id]['product'][$line->fk_product] = array();
1579
                    }
1580
                    $TNetNeeds[$this->id]['product'][$line->fk_product]['fk_unit'] = $line->fk_unit;
1581
                    if (!isset($TNetNeeds[$this->id]['product'][$line->fk_product]['qty'])) {
1582
                        $TNetNeeds[$this->id]['product'][$line->fk_product]['qty'] = 0.0;
1583
                    }
1584
                    $TNetNeeds[$this->id]['product'][$line->fk_product]['qty'] += $line->qty * $qty;
1585
                    $TNetNeeds[$this->id]['product'][$line->fk_product]['level'] = $level;
1586
                }
1587
            }
1588
        }
1589
    }
1590
1591
    /**
1592
     * Recursively retrieves all parent bom in the tree that leads to the $bom_id bom
1593
     *
1594
     * @param   array   $TParentBom     We put all found parent bom in $TParentBom
1595
     * @param   int     $bom_id         ID of bom from which we want to get parent bom ids
1596
     * @param   int     $level      Protection against infinite loop
1597
     * @return  void
1598
     */
1599
    public function getParentBomTreeRecursive(&$TParentBom, $bom_id = 0, $level = 1)
1600
    {
1601
1602
        // Protection against infinite loop
1603
        if ($level > 1000) {
1604
            return;
1605
        }
1606
1607
        if (empty($bom_id)) {
1608
            $bom_id = $this->id;
1609
        }
1610
1611
        $sql = 'SELECT l.fk_bom, b.label
1612
				FROM ' . MAIN_DB_PREFIX . 'bom_bomline l
1613
				INNER JOIN ' . MAIN_DB_PREFIX . $this->table_element . ' b ON b.rowid = l.fk_bom
1614
				WHERE fk_bom_child = ' . ((int) $bom_id);
1615
1616
        $resql = $this->db->query($sql);
1617
        if (!empty($resql)) {
1618
            while ($res = $this->db->fetch_object($resql)) {
1619
                $TParentBom[$res->fk_bom] = $res->fk_bom;
1620
                $this->getParentBomTreeRecursive($TParentBom, $res->fk_bom, $level + 1);
1621
            }
1622
        }
1623
    }
1624
1625
    /**
1626
     *  Return clicable link of object (with eventually picto)
1627
     *
1628
     *  @param      string      $option                 Where point the link (0=> main card, 1,2 => shipment, 'nolink'=>No link)
1629
     *  @param      array       $arraydata              Array of data
1630
     *  @return     string                              HTML Code for Kanban thumb.
1631
     */
1632
    public function getKanbanView($option = '', $arraydata = null)
1633
    {
1634
        global $db,$langs;
1635
1636
        $selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);
1637
1638
        $return = '<div class="box-flex-item box-flex-grow-zero">';
1639
        $return .= '<div class="info-box info-box-sm">';
1640
        $return .= '<span class="info-box-icon bg-infobox-action">';
1641
        $return .= img_picto('', $this->picto);
1642
        $return .= '</span>';
1643
        $return .= '<div class="info-box-content">';
1644
        $return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">' . (method_exists($this, 'getNomUrl') ? $this->getNomUrl() : '') . '</span>';
1645
        if ($selected >= 0) {
1646
            $return .= '<input id="cb' . $this->id . '" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="' . $this->id . '"' . ($selected ? ' checked="checked"' : '') . '>';
1647
        }
1648
        if (property_exists($this, 'fields') && !empty($this->fields['bomtype']['arrayofkeyval'])) {
1649
            $return .= '<br><span class="info-box-label opacitymedium">' . $langs->trans("Type") . ' : </span>';
1650
            if ($this->bomtype == 0) {
1651
                $return .= '<span class="info-box-label">' . $this->fields['bomtype']['arrayofkeyval'][0] . '</span>';
1652
            } else {
1653
                $return .= '<span class="info-box-label">' . $this->fields['bomtype']['arrayofkeyval'][1] . '</span>';
1654
            }
1655
        }
1656
        if (!empty($arraydata['prod'])) {
1657
            $prod = $arraydata['prod'];
1658
            $return .= '<br><span class="info-box-label">' . $prod->getNomUrl(1) . '</span>';
1659
        }
1660
        if (method_exists($this, 'getLibStatut')) {
1661
            $return .= '<br><div class="info-box-status">' . $this->getLibStatut(3) . '</div>';
1662
        }
1663
1664
        $return .= '</div>';
1665
        $return .= '</div>';
1666
        $return .= '</div>';
1667
        return $return;
1668
    }
1669
}
1670