Passed
Push — EXTRACT_CLASSES ( ff35ec...a2ff75 )
by Rafael
48:13
created

Product::update()   F

Complexity

Conditions 134
Paths > 20000

Size

Total Lines 408
Code Lines 280

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 134
eloc 280
nc 1342177280
nop 5
dl 0
loc 408
rs 0
c 0
b 0
f 0

How to fix   Long Method    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) 2001-2007  Rodolphe Quiedeville        <[email protected]>
4
 * Copyright (C) 2004-2014	Laurent Destailleur		    <[email protected]>
5
 * Copyright (C) 2005-2015	Regis Houssin			    <[email protected]>
6
 * Copyright (C) 2006		Andre Cianfarani		    <[email protected]>
7
 * Copyright (C) 2007-2011	Jean Heimburger			    <[email protected]>
8
 * Copyright (C) 2010-2018	Juanjo Menent			    <[email protected]>
9
 * Copyright (C) 2012       Cedric Salvador             <[email protected]>
10
 * Copyright (C) 2013-2014	Cedric GROSS			    <[email protected]>
11
 * Copyright (C) 2013-2016	Marcos García			    <[email protected]>
12
 * Copyright (C) 2011-2021	Open-DSI				    <[email protected]>
13
 * Copyright (C) 2014		Henry Florian			    <[email protected]>
14
 * Copyright (C) 2014-2016	Philippe Grand			    <[email protected]>
15
 * Copyright (C) 2014		Ion agorria			        <[email protected]>
16
 * Copyright (C) 2016-2024	Ferran Marcet			    <[email protected]>
17
 * Copyright (C) 2017		Gustavo Novaro
18
 * Copyright (C) 2019-2024  Frédéric France             <[email protected]>
19
 * Copyright (C) 2023		Benjamin Falière		    <[email protected]>
20
 * Copyright (C) 2024		MDW						    <[email protected]>
21
 * Copyright (C) 2024       Rafael San José             <[email protected]>
22
 *
23
 * This program is free software; you can redistribute it and/or modify
24
 * it under the terms of the GNU General Public License as published by
25
 * the Free Software Foundation; either version 3 of the License, or
26
 * (at your option) any later version.
27
 *
28
 * This program is distributed in the hope that it will be useful,
29
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
30
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
31
 * GNU General Public License for more details.
32
 *
33
 * You should have received a copy of the GNU General Public License
34
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
35
 */
36
37
namespace Dolibarr\Code\Product\Classes;
38
39
use Dolibarr\Core\Base\CommonObject;
40
41
/**
42
 *    \file       htdocs/product/class/product.class.php
43
 *    \ingroup    produit
44
 *    \brief      File of class to manage the predefined products or services
45
 */
46
47
require_once constant('DOL_DOCUMENT_ROOT') . '/core/lib/product.lib.php';
48
require_once constant('DOL_DOCUMENT_ROOT') . '/product/class/productbatch.class.php';
49
require_once constant('DOL_DOCUMENT_ROOT') . '/product/stock/class/productlot.class.php';
50
require_once constant('DOL_DOCUMENT_ROOT') . '/product/stock/class/entrepot.class.php';
51
52
/**
53
 * Class to manage products or services
54
 */
55
class Product extends CommonObject
56
{
57
    /**
58
     * Const sell or eat by mandatory id
59
     */
60
    const SELL_OR_EAT_BY_MANDATORY_ID_NONE = 0;
61
    const SELL_OR_EAT_BY_MANDATORY_ID_SELL_BY = 1;
62
    const SELL_OR_EAT_BY_MANDATORY_ID_EAT_BY = 2;
63
    const SELL_OR_EAT_BY_MANDATORY_ID_SELL_AND_EAT = 3;
64
65
    /**
66
     * @var string ID to identify managed object
67
     */
68
    public $element = 'product';
69
70
    /**
71
     * @var string Name of table without prefix where object is stored
72
     */
73
    public $table_element = 'product';
74
75
    /**
76
     * @var string Field with ID of parent key if this field has a parent
77
     */
78
    public $fk_element = 'fk_product';
79
80
    /**
81
     * @var static
82
     */
83
    public $oldcopy;
84
85
    /**
86
     * @var array<string, array<string>>    List of child tables. To test if we can delete object.
87
     */
88
    protected $childtables = array(
89
        'supplier_proposaldet' => array('name' => 'SupplierProposal', 'parent' => 'supplier_proposal', 'parentkey' => 'fk_supplier_proposal'),
90
        'propaldet' => array('name' => 'Proposal', 'parent' => 'propal', 'parentkey' => 'fk_propal'),
91
        'commandedet' => array('name' => 'Order', 'parent' => 'commande', 'parentkey' => 'fk_commande'),
92
        'facturedet' => array('name' => 'Invoice', 'parent' => 'facture', 'parentkey' => 'fk_facture'),
93
        'contratdet' => array('name' => 'Contract', 'parent' => 'contrat', 'parentkey' => 'fk_contrat'),
94
        'facture_fourn_det' => array('name' => 'SupplierInvoice', 'parent' => 'facture_fourn', 'parentkey' => 'fk_facture_fourn'),
95
        'commande_fournisseurdet' => array('name' => 'SupplierOrder', 'parent' => 'commande_fournisseur', 'parentkey' => 'fk_commande'),
96
        'mrp_production' => array('name' => 'Mo', 'parent' => 'mrp_mo', 'parentkey' => 'fk_mo')
97
    );
98
99
    /**
100
     * @var string picto
101
     */
102
    public $picto = 'product';
103
104
    /**
105
     * {@inheritdoc}
106
     */
107
    protected $table_ref_field = 'ref';
108
109
    public $regeximgext = '\.gif|\.jpg|\.jpeg|\.png|\.bmp|\.webp|\.xpm|\.xbm'; // See also into images.lib.php
110
111
    /**
112
     * @deprecated  Use $label instead
113
     * @see $label
114
     */
115
    public $libelle;
116
117
    /**
118
     * Product label
119
     *
120
     * @var string
121
     */
122
    public $label;
123
124
    /**
125
     * Product description
126
     *
127
     * @var string
128
     */
129
    public $description;
130
131
    /**
132
     * Product other fields PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION
133
     *
134
     * @var string
135
     */
136
    public $other;
137
138
    /**
139
     * Check TYPE constants
140
     *
141
     * @var int
142
     */
143
    public $type = self::TYPE_PRODUCT;
144
145
    /**
146
     * Selling price without tax
147
     *
148
     * @var float
149
     */
150
    public $price;
151
152
    public $price_formated;         // used by takepos/ajax/ajax.php
153
154
    /**
155
     * Selling price with tax
156
     *
157
     * @var float
158
     */
159
    public $price_ttc;
160
161
    public $price_ttc_formated;     // used by takepos/ajax/ajax.php
162
163
    /**
164
     * Minimum price net
165
     *
166
     * @var float
167
     */
168
    public $price_min;
169
170
    /**
171
     * Minimum price with tax
172
     *
173
     * @var float
174
     */
175
    public $price_min_ttc;
176
177
    /**
178
     * Base price ('TTC' for price including tax or 'HT' for net price)
179
     * @var string
180
     */
181
    public $price_base_type;
182
    public $price_label;
183
184
    //! Arrays for multiprices
185
    public $multiprices = array();
186
    public $multiprices_ttc = array();
187
    public $multiprices_base_type = array();
188
    public $multiprices_default_vat_code = array();
189
    public $multiprices_min = array();
190
    public $multiprices_min_ttc = array();
191
    public $multiprices_tva_tx = array();
192
    public $multiprices_recuperableonly = array();
193
194
    //! Price by quantity arrays
195
    public $price_by_qty;
196
    public $prices_by_qty = array();
197
    public $prices_by_qty_id = array();
198
    public $prices_by_qty_list = array();
199
200
    /**
201
     * @var int price level set after updateprice for trigger
202
     */
203
    public $level;
204
205
    //! Array for multilangs
206
    public $multilangs = array();
207
208
    //! Default VAT code for product (link to code into llx_c_tva but without foreign keys)
209
    public $default_vat_code;
210
211
    //! Default VAT rate of product
212
    public $tva_tx;
213
214
    /**
215
     * int  French VAT NPR is used (0 or 1)
216
     */
217
    public $tva_npr = 0;
218
219
    //! Default discount percent
220
    public $remise_percent;
221
222
    //! Other local taxes
223
    public $localtax1_tx;
224
    public $localtax2_tx;
225
    public $localtax1_type;
226
    public $localtax2_type;
227
228
    // Properties set by get_buyprice() for return
229
230
    public $desc_supplier;
231
    public $vatrate_supplier;
232
    public $default_vat_code_supplier;
233
    public $fourn_multicurrency_price;
234
    public $fourn_multicurrency_unitprice;
235
    public $fourn_multicurrency_tx;
236
    public $fourn_multicurrency_id;
237
    public $fourn_multicurrency_code;
238
    public $packaging;
239
240
241
    /**
242
     * Lifetime (in seconds)
243
     *
244
     * @var int|null
245
     * @see ProductLot
246
     */
247
    public $lifetime;
248
249
    /**
250
     * Quality control frequency (in days ?)
251
     *
252
     * @var int|null
253
     * @see ProductLot
254
     */
255
    public $qc_frequency;
256
257
    /**
258
     * Stock real (denormalized data)
259
     *
260
     * @var int
261
     */
262
    public $stock_reel = 0;
263
264
    /**
265
     * Stock virtual
266
     *
267
     * @var int
268
     */
269
    public $stock_theorique;
270
271
    /**
272
     * Cost price
273
     *
274
     * @var float
275
     */
276
    public $cost_price;
277
278
    //! Average price value for product entry into stock (PMP)
279
    public $pmp;
280
281
    /**
282
     * Stock alert
283
     *
284
     * @var float
285
     */
286
    public $seuil_stock_alerte = 0;
287
288
    /**
289
     * Ask for replenishment when $desiredstock < $stock_reel
290
     */
291
    public $desiredstock = 0;
292
293
    /**
294
     * Service expiration
295
     */
296
    public $duration_value;
297
    /**
298
     * Service expiration unit
299
     */
300
    public $duration_unit;
301
    /**
302
     * Service expiration label (value + unit)
303
     */
304
    public $duration;
305
306
    /**
307
     * @var int Service Workstation
308
     */
309
    public $fk_default_workstation;
310
311
    /**
312
     * Status indicates whether the product is on sale '1' or not '0'
313
     *
314
     * @var int
315
     */
316
    public $status = 0;
317
318
    /**
319
     * Status indicates whether the product is on sale '1' or not '0'
320
     * @var int
321
     * @deprecated  Use $status instead
322
     * @see $status
323
     */
324
    public $tosell;
325
326
    /**
327
     * Status indicate whether the product is available for purchase '1' or not '0'
328
     *
329
     * @var int
330
     */
331
    public $status_buy = 0;
332
333
    /**
334
     * Status indicate whether the product is available for purchase '1' or not '0'
335
     * @var int
336
     * @deprecated Use $status_buy instead
337
     * @see $status_buy
338
     */
339
    public $tobuy;
340
341
    /**
342
     * Status indicates whether the product is a finished product '1' or a raw material '0'
343
     *
344
     * @var ?int
345
     */
346
    public $finished;
347
348
    /**
349
     * fk_default_bom indicates the default bom
350
     *
351
     * @var int
352
     */
353
    public $fk_default_bom;
354
355
    /**
356
     * product_fourn_price_id indicates the fourn price id
357
     *
358
     * @var int
359
     */
360
    public $product_fourn_price_id;
361
362
    /**
363
     * buyprice indicates buy price off the product
364
     *
365
     * @var float
366
     */
367
    public $buyprice;
368
369
    /**
370
     * for backward compatibility
371
     *
372
     * @var int
373
     */
374
    public $tobatch;
375
376
377
    /**
378
     * We must manage lot/batch number, sell-by date and so on : '0':no, '1':yes, '2": yes with unique serial number
379
     *
380
     * @var int
381
     */
382
    public $status_batch = 0;
383
384
    /**
385
     * Make sell-by or eat-by date mandatory
386
     *
387
     * @var int
388
     */
389
    public $sell_or_eat_by_mandatory = 0;
390
391
    /**
392
     * If allowed, we can edit batch or serial number mask for each product
393
     *
394
     * @var string
395
     */
396
    public $batch_mask = '';
397
398
    /**
399
     * Customs code
400
     *
401
     * @var string
402
     */
403
    public $customcode;
404
405
    /**
406
     * Product URL
407
     *
408
     * @var string
409
     */
410
    public $url;
411
412
    //! Metric of products
413
    public $weight;
414
    public $weight_units;   // scale -3, 0, 3, 6
415
    public $length;
416
    public $length_units;   // scale -3, 0, 3, 6
417
    public $width;
418
    public $width_units;    // scale -3, 0, 3, 6
419
    public $height;
420
    public $height_units;   // scale -3, 0, 3, 6
421
    public $surface;
422
    public $surface_units;  // scale -3, 0, 3, 6
423
    public $volume;
424
    public $volume_units;   // scale -3, 0, 3, 6
425
426
    public $net_measure;
427
    public $net_measure_units;  // scale -3, 0, 3, 6
428
429
    public $accountancy_code_sell;
430
    public $accountancy_code_sell_intra;
431
    public $accountancy_code_sell_export;
432
    public $accountancy_code_buy;
433
    public $accountancy_code_buy_intra;
434
    public $accountancy_code_buy_export;
435
436
    /**
437
     * @var string|int  Main Barcode value, -1 or 'auto' for auto code
438
     */
439
    public $barcode;
440
441
    /**
442
     * @var int     Main Barcode type ID
443
     */
444
    public $barcode_type;
445
446
    /**
447
     * @var string  Main Barcode type code
448
     */
449
    public $barcode_type_code;
450
451
    public $stats_propale = array();
452
    public $stats_commande = array();
453
    public $stats_contrat = array();
454
    public $stats_facture = array();
455
    public $stats_proposal_supplier = array();
456
    public $stats_commande_fournisseur = array();
457
    public $stats_expedition = array();
458
    public $stats_reception = array();
459
    public $stats_mo = array();
460
    public $stats_bom = array();
461
    public $stats_mrptoconsume = array();
462
    public $stats_mrptoproduce = array();
463
    public $stats_facturerec = array();
464
    public $stats_facture_fournisseur = array();
465
466
    //! Size of image
467
    public $imgWidth;
468
    public $imgHeight;
469
470
    /**
471
     * @var integer|string date_creation
472
     */
473
    public $date_creation;
474
475
    /**
476
     * @var integer|string date_modification
477
     */
478
    public $date_modification;
479
480
    //! Id du fournisseur
481
    public $product_fourn_id;
482
483
    //! Product ID already linked to a reference supplier
484
    public $product_id_already_linked;
485
486
    public $nbphoto = 0;
487
488
    //! Contains detail of stock of product into each warehouse
489
    public $stock_warehouse = array();
490
491
    /**
492
     * @var int Default warehouse Id
493
     */
494
    public $fk_default_warehouse;
495
    /**
496
     * @var int ID
497
     */
498
    public $fk_price_expression;
499
500
    /* To store supplier price found */
501
    public $fourn_qty;
502
    public $fourn_pu;
503
    public $fourn_price_base_type;
504
505
    /**
506
     * @var int ID
507
     */
508
    public $fourn_socid;
509
510
    /**
511
     * @deprecated
512
     * @see        $ref_supplier
513
     */
514
    public $ref_fourn;
515
516
    /**
517
     * @var string ref supplier
518
     */
519
    public $ref_supplier;
520
521
    /**
522
     * @var int|null                ID of the unit of measurement (rowid in llx_c_units table)
523
     * @see measuringUnitString()
524
     * @see getLabelOfUnit()
525
     */
526
    public $fk_unit;
527
528
    /**
529
     * Price is generated using multiprice rules
530
     *
531
     * @var int
532
     */
533
    public $price_autogen = 0;
534
535
    /**
536
     * Array with list of supplier prices of product
537
     *
538
     * @var array
539
     */
540
    public $supplierprices;
541
542
    /**
543
     * Array with list of sub-products for Kits
544
     *
545
     * @var array
546
     */
547
    public $sousprods;
548
549
    /**
550
     * @var array Path of subproducts. Build from ->sousprods with get_arbo_each_prod()
551
     */
552
    public $res;
553
554
555
    /**
556
     * Property set to save result of isObjectUsed(). Used for example by Product API.
557
     *
558
     * @var boolean
559
     */
560
    public $is_object_used;
561
562
    /**
563
     * If this Product is within a kit:
564
     * Quantity of this Product within this kit
565
     *
566
     * @var float
567
     * @see Product::is_sousproduit()       To set this property
568
     * @see Product::add_sousproduit()
569
     * @see Product::update_sousproduit()
570
     */
571
    public $is_sousproduit_qty;
572
573
    /**
574
     * If this Product is within a kit:
575
     * 1 = modify the stock of this child Product upon modification of the stock of its parent Product
576
     * ("incdec" stands for increase/decrease)
577
     *
578
     * @var 0|1
0 ignored issues
show
Documentation Bug introduced by
The doc comment 0|1 at position 0 could not be parsed: Unknown type name '0' at position 0 in 0|1.
Loading history...
579
     * @see Product::is_sousproduit()       To set this property
580
     * @see Product::add_sousproduit()
581
     * @see Product::update_sousproduit()
582
     */
583
    public $is_sousproduit_incdec;
584
585
    public $mandatory_period;
586
587
588
    /**
589
     *  'type' if the field format ('integer', 'integer:ObjectClass:PathToClass[:AddCreateButtonOrNot[:Filter]]', 'varchar(x)', 'double(24,8)', 'real', 'price', 'text', 'html', 'date', 'datetime', 'timestamp', 'duration', 'mail', 'phone', 'url', 'password')
590
     *         Note: Filter can be a string like "(t.ref:like:'SO-%') or (t.date_creation:<:'20160101') or (t.nature:is:NULL)"
591
     *  'label' the translation key.
592
     *  'enabled' is a condition when the field must be managed (Example: 1 or 'getDolGlobalString("MY_SETUP_PARAM")'
593
     *  'position' is the sort order of field.
594
     *  'notnull' is set to 1 if not null in database. Set to -1 if we must set data to null if empty ('' or 0).
595
     *  '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)
596
     *  'noteditable' says if field is not editable (1 or 0)
597
     *  '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.
598
     *  'index' if we want an index in database.
599
     *  'foreignkey'=>'tablename.field' if the field is a foreign key (it is recommended to name the field fk_...).
600
     *  'searchall' is 1 if we want to search in this field when making a search from the quick search button.
601
     *  '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).
602
     *  'css' is the CSS style to use on field. For example: 'maxwidth200'
603
     *  'help' is a string visible as a tooltip on field
604
     *  'showoncombobox' if value of the field must be visible into the label of the combobox that list record
605
     *  '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.
606
     *  'arrayofkeyval' to set list of value if type is a list of predefined values. For example: array("0"=>"Draft","1"=>"Active","-1"=>"Cancel")
607
     *  'autofocusoncreate' to have field having the focus on a create form. Only 1 field should have this property set to 1.
608
     *  'comment' is not used. You can store here any text of your choice. It is not used by application.
609
     *
610
     *  Note: To have value dynamic, you can set value to 0 in definition and edit the value on the fly into the constructor.
611
     */
612
613
    /**
614
     * @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...
615
     */
616
    public $fields = array(
617
        'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'index' => 1, 'position' => 1, 'comment' => 'Id'),
618
        'ref'           => array('type' => 'varchar(128)', 'label' => 'Ref', 'enabled' => 1, 'visible' => 1, 'notnull' => 1, 'showoncombobox' => 1, 'index' => 1, 'position' => 10, 'searchall' => 1, 'comment' => 'Reference of object'),
619
        'entity'        => array('type' => 'integer', 'label' => 'Entity', 'enabled' => 1, 'visible' => 0, 'default' => '1', 'notnull' => 1, 'index' => 1, 'position' => 5),
620
        'label'         => array('type' => 'varchar(255)', 'label' => 'Label', 'enabled' => 1, 'visible' => 1, 'notnull' => 1, 'showoncombobox' => 2, 'position' => 15, 'csslist' => 'tdoverflowmax250'),
621
        'barcode'       => array('type' => 'varchar(255)', 'label' => 'Barcode', 'enabled' => 'isModEnabled("barcode")', 'position' => 20, 'visible' => -1, 'showoncombobox' => 3, 'cssview' => 'tdwordbreak', 'csslist' => 'tdoverflowmax125'),
622
        'fk_barcode_type' => array('type' => 'integer', 'label' => 'BarcodeType', 'enabled' => 1, 'position' => 21, 'notnull' => 0, 'visible' => -1,),
623
        'note_public'   => array('type' => 'html', 'label' => 'NotePublic', 'enabled' => 1, 'visible' => 0, 'position' => 61),
624
        'note'          => array('type' => 'html', 'label' => 'NotePrivate', 'enabled' => 1, 'visible' => 0, 'position' => 62),
625
        'datec'         => array('type' => 'datetime', 'label' => 'DateCreation', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 500),
626
        'tms'           => array('type' => 'timestamp', 'label' => 'DateModification', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 501),
627
        //'date_valid'    =>array('type'=>'datetime',     'label'=>'DateCreation',     'enabled'=>1, 'visible'=>-2, 'position'=>502),
628
        'fk_user_author' => array('type' => 'integer', 'label' => 'UserAuthor', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 510, 'foreignkey' => 'llx_user.rowid'),
629
        'fk_user_modif' => array('type' => 'integer', 'label' => 'UserModif', 'enabled' => 1, 'visible' => -2, 'notnull' => -1, 'position' => 511),
630
        //'fk_user_valid' =>array('type'=>'integer',      'label'=>'UserValidation',        'enabled'=>1, 'visible'=>-1, 'position'=>512),
631
        'localtax1_tx' => array('type' => 'double(6,3)', 'label' => 'Localtax1tx', 'enabled' => 1, 'position' => 150, 'notnull' => 0, 'visible' => -1,),
632
        'localtax1_type' => array('type' => 'varchar(10)', 'label' => 'Localtax1type', 'enabled' => 1, 'position' => 155, 'notnull' => 1, 'visible' => -1,),
633
        'localtax2_tx' => array('type' => 'double(6,3)', 'label' => 'Localtax2tx', 'enabled' => 1, 'position' => 160, 'notnull' => 0, 'visible' => -1,),
634
        'localtax2_type' => array('type' => 'varchar(10)', 'label' => 'Localtax2type', 'enabled' => 1, 'position' => 165, 'notnull' => 1, 'visible' => -1,),
635
        'last_main_doc' => array('type' => 'varchar(255)', 'label' => 'LastMainDoc', 'enabled' => 1, 'visible' => -1, 'position' => 170),
636
        'import_key'    => array('type' => 'varchar(14)', 'label' => 'ImportId', 'enabled' => 1, 'visible' => -2, 'notnull' => -1, 'index' => 0, 'position' => 1000),
637
        //'tosell'       =>array('type'=>'integer',      'label'=>'Status',           'enabled'=>1, 'visible'=>1,  'notnull'=>1, 'default'=>'0', 'index'=>1,  'position'=>1000, 'arrayofkeyval'=>array(0=>'Draft', 1=>'Active', -1=>'Cancel')),
638
        //'tobuy'        =>array('type'=>'integer',      'label'=>'Status',           'enabled'=>1, 'visible'=>1,  'notnull'=>1, 'default'=>'0', 'index'=>1,  'position'=>1000, 'arrayofkeyval'=>array(0=>'Draft', 1=>'Active', -1=>'Cancel')),
639
        'mandatory_period' => array('type' => 'integer', 'label' => 'mandatoryperiod', 'enabled' => 1, 'visible' => 1,  'notnull' => 1, 'default' => '0', 'index' => 1,  'position' => 1000),
640
    );
641
642
    /**
643
     * Regular product
644
     */
645
    const TYPE_PRODUCT = 0;
646
    /**
647
     * Service
648
     */
649
    const TYPE_SERVICE = 1;
650
651
    /**
652
     *  Constructor
653
     *
654
     * @param DoliDB $db Database handler
0 ignored issues
show
Bug introduced by
The type Dolibarr\Code\Product\Classes\DoliDB was not found. Did you mean DoliDB? If so, make sure to prefix the type with \.
Loading history...
655
     */
656
    public function __construct($db)
657
    {
658
        $this->db = $db;
659
660
        $this->ismultientitymanaged = 1;
661
        $this->isextrafieldmanaged = 1;
662
663
        $this->canvas = '';
664
    }
665
666
    /**
667
     *    Check that ref and label are ok
668
     *
669
     * @return int         >1 if OK, <=0 if KO
670
     */
671
    public function check()
672
    {
673
        if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
674
            $this->ref = trim($this->ref);
675
        } else {
676
            $this->ref = dol_sanitizeFileName(stripslashes($this->ref));
677
        }
678
679
        $err = 0;
680
        if (dol_strlen(trim($this->ref)) == 0) {
681
            $err++;
682
        }
683
684
        if (dol_strlen(trim($this->label)) == 0) {
685
            $err++;
686
        }
687
688
        if ($err > 0) {
689
            return 0;
690
        } else {
691
            return 1;
692
        }
693
    }
694
695
    /**
696
     * Insert product into database
697
     *
698
     * @param  User     $user           User making insert
0 ignored issues
show
Bug introduced by
The type Dolibarr\Code\Product\Classes\User was not found. Did you mean User? If so, make sure to prefix the type with \.
Loading history...
699
     * @param  int      $notrigger      Disable triggers
700
     * @return int                      Id of product/service if OK, < 0 if KO
701
     */
702
    public function create($user, $notrigger = 0)
703
    {
704
        global $conf, $langs;
705
706
        $error = 0;
707
708
        // Clean parameters
709
        if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
710
            $this->ref = trim($this->ref);
711
        } else {
712
            $this->ref = dol_sanitizeFileName(dol_string_nospecial(trim($this->ref)));
713
        }
714
        $this->label = trim($this->label);
715
        $this->price_ttc = (float) price2num($this->price_ttc);
716
        $this->price = (float) price2num($this->price);
717
        $this->price_min_ttc = (float) price2num($this->price_min_ttc);
718
        $this->price_min = (float) price2num($this->price_min);
719
        $this->price_label = trim($this->price_label);
720
        if (empty($this->tva_tx)) {
721
            $this->tva_tx = 0;
722
        }
723
        if (empty($this->tva_npr)) {
724
            $this->tva_npr = 0;
725
        }
726
        //Local taxes
727
        if (empty($this->localtax1_tx)) {
728
            $this->localtax1_tx = 0;
729
        }
730
        if (empty($this->localtax2_tx)) {
731
            $this->localtax2_tx = 0;
732
        }
733
        if (empty($this->localtax1_type)) {
734
            $this->localtax1_type = '0';
735
        }
736
        if (empty($this->localtax2_type)) {
737
            $this->localtax2_type = '0';
738
        }
739
        if (empty($this->price)) {
740
            $this->price = 0;
741
        }
742
        if (empty($this->price_min)) {
743
            $this->price_min = 0;
744
        }
745
        // Price by quantity
746
        if (empty($this->price_by_qty)) {
747
            $this->price_by_qty = 0;
748
        }
749
750
        if (empty($this->status)) {
751
            $this->status = 0;
752
        }
753
        if (empty($this->status_buy)) {
754
            $this->status_buy = 0;
755
        }
756
757
        $price_ht = 0;
758
        $price_ttc = 0;
759
        $price_min_ht = 0;
760
        $price_min_ttc = 0;
761
762
        //
763
        if ($this->price_base_type == 'TTC' && $this->price_ttc > 0) {
764
            $price_ttc = price2num($this->price_ttc, 'MU');
765
            $price_ht = price2num($this->price_ttc / (1 + ($this->tva_tx / 100)), 'MU');
766
        }
767
768
        //
769
        if ($this->price_base_type != 'TTC' && $this->price > 0) {
770
            $price_ht = price2num($this->price, 'MU');
771
            $price_ttc = price2num($this->price * (1 + ($this->tva_tx / 100)), 'MU');
772
        }
773
774
        //
775
        if (($this->price_min_ttc > 0) && ($this->price_base_type == 'TTC')) {
776
            $price_min_ttc = price2num($this->price_min_ttc, 'MU');
777
            $price_min_ht = price2num($this->price_min_ttc / (1 + ($this->tva_tx / 100)), 'MU');
778
        }
779
780
        //
781
        if (($this->price_min > 0) && ($this->price_base_type != 'TTC')) {
782
            $price_min_ht = price2num($this->price_min, 'MU');
783
            $price_min_ttc = price2num($this->price_min * (1 + ($this->tva_tx / 100)), 'MU');
784
        }
785
786
        $this->accountancy_code_buy = trim($this->accountancy_code_buy);
787
        $this->accountancy_code_buy_intra = trim($this->accountancy_code_buy_intra);
788
        $this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export);
789
        $this->accountancy_code_sell = trim($this->accountancy_code_sell);
790
        $this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra);
791
        $this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export);
792
793
        // Barcode value
794
        $this->barcode = trim($this->barcode);
795
        $this->mandatory_period = empty($this->mandatory_period) ? 0 : $this->mandatory_period;
796
        // Check parameters
797
        if (empty($this->label)) {
798
            $this->error = 'ErrorMandatoryParametersNotProvided';
799
            return -1;
800
        }
801
802
        if (empty($this->ref) || $this->ref == 'auto') {
803
            // Load object modCodeProduct
804
            $module = getDolGlobalString('PRODUCT_CODEPRODUCT_ADDON', 'mod_codeproduct_leopard');
805
            if ($module != 'mod_codeproduct_leopard') {    // Do not load module file for leopard
806
                if (substr($module, 0, 16) == 'mod_codeproduct_' && substr($module, -3) == 'php') {
807
                    $module = substr($module, 0, dol_strlen($module) - 4);
808
                }
809
                dol_include_once('/core/modules/product/' . $module . '.php');
810
                $modCodeProduct = new $module();
811
                '@phan-var-force ModeleProductCode $modCodeProduct';
812
                if (!empty($modCodeProduct->code_auto)) {
813
                    $this->ref = $modCodeProduct->getNextValue($this, $this->type);
814
                }
815
                unset($modCodeProduct);
816
            }
817
818
            if (empty($this->ref)) {
819
                $this->error = 'ProductModuleNotSetupForAutoRef';
820
                return -2;
821
            }
822
        }
823
824
        dol_syslog(get_class($this) . "::create ref=" . $this->ref . " price=" . $this->price . " price_ttc=" . $this->price_ttc . " tva_tx=" . $this->tva_tx . " price_base_type=" . $this->price_base_type, LOG_DEBUG);
825
826
        $now = dol_now();
827
828
        if (empty($this->date_creation)) {
829
            $this->date_creation = $now;
830
        }
831
832
        $this->db->begin();
833
834
        // For automatic creation during create action (not used by Dolibarr GUI, can be used by scripts)
835
        if ($this->barcode == '-1' || $this->barcode == 'auto') {
836
            $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
837
        }
838
839
        // Check more parameters
840
        // If error, this->errors[] is filled
841
        $result = $this->verify();
842
843
        if ($result >= 0) {
844
            $sql = "SELECT count(*) as nb";
845
            $sql .= " FROM " . $this->db->prefix() . "product";
846
            $sql .= " WHERE entity IN (" . getEntity('product') . ")";
847
            $sql .= " AND ref = '" . $this->db->escape($this->ref) . "'";
848
849
            $result = $this->db->query($sql);
850
            if ($result) {
851
                $obj = $this->db->fetch_object($result);
852
                if ($obj->nb == 0) {
853
                    // Insert new product, no previous one found
854
                    $sql = "INSERT INTO " . $this->db->prefix() . "product (";
855
                    $sql .= "datec";
856
                    $sql .= ", entity";
857
                    $sql .= ", ref";
858
                    $sql .= ", ref_ext";
859
                    $sql .= ", price_min";
860
                    $sql .= ", price_min_ttc";
861
                    $sql .= ", label";
862
                    $sql .= ", fk_user_author";
863
                    $sql .= ", fk_product_type";
864
                    $sql .= ", price";
865
                    $sql .= ", price_ttc";
866
                    $sql .= ", price_base_type";
867
                    $sql .= ", price_label";
868
                    $sql .= ", tobuy";
869
                    $sql .= ", tosell";
870
                    if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
871
                        $sql .= ", accountancy_code_buy";
872
                        $sql .= ", accountancy_code_buy_intra";
873
                        $sql .= ", accountancy_code_buy_export";
874
                        $sql .= ", accountancy_code_sell";
875
                        $sql .= ", accountancy_code_sell_intra";
876
                        $sql .= ", accountancy_code_sell_export";
877
                    }
878
                    $sql .= ", canvas";
879
                    $sql .= ", finished";
880
                    $sql .= ", tobatch";
881
                    $sql .= ", sell_or_eat_by_mandatory";
882
                    $sql .= ", batch_mask";
883
                    $sql .= ", fk_unit";
884
                    $sql .= ", mandatory_period";
885
                    $sql .= ") VALUES (";
886
                    $sql .= "'" . $this->db->idate($this->date_creation) . "'";
887
                    $sql .= ", " . (!empty($this->entity) ? (int) $this->entity : (int) $conf->entity);
888
                    $sql .= ", '" . $this->db->escape($this->ref) . "'";
889
                    $sql .= ", " . (!empty($this->ref_ext) ? "'" . $this->db->escape($this->ref_ext) . "'" : "null");
890
                    $sql .= ", " . price2num($price_min_ht);
891
                    $sql .= ", " . price2num($price_min_ttc);
892
                    $sql .= ", " . (!empty($this->label) ? "'" . $this->db->escape($this->label) . "'" : "null");
893
                    $sql .= ", " . ((int) $user->id);
894
                    $sql .= ", " . ((int) $this->type);
895
                    $sql .= ", " . price2num($price_ht, 'MT');
896
                    $sql .= ", " . price2num($price_ttc, 'MT');
897
                    $sql .= ", '" . $this->db->escape($this->price_base_type) . "'";
898
                    $sql .= ", " . (!empty($this->price_label) ? "'" . $this->db->escape($this->price_label) . "'" : "null");
899
                    $sql .= ", " . ((int) $this->status);
900
                    $sql .= ", " . ((int) $this->status_buy);
901
                    if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
902
                        $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
903
                        $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
904
                        $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
905
                        $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
906
                        $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
907
                        $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
908
                    }
909
                    $sql .= ", '" . $this->db->escape($this->canvas) . "'";
910
                    $sql .= ", " . ((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? 'NULL' : (int) $this->finished);
911
                    $sql .= ", " . ((empty($this->status_batch) || $this->status_batch < 0) ? '0' : ((int) $this->status_batch));
912
                    $sql .= ", " . ((empty($this->sell_or_eat_by_mandatory) || $this->sell_or_eat_by_mandatory < 0) ? 0 : ((int) $this->sell_or_eat_by_mandatory));
913
                    $sql .= ", '" . $this->db->escape($this->batch_mask) . "'";
914
                    $sql .= ", " . ($this->fk_unit > 0 ? ((int) $this->fk_unit) : 'NULL');
915
                    $sql .= ", '" . $this->db->escape($this->mandatory_period) . "'";
916
                    $sql .= ")";
917
918
                    dol_syslog(get_class($this) . "::Create", LOG_DEBUG);
919
920
                    $result = $this->db->query($sql);
921
                    if ($result) {
922
                        $id = $this->db->last_insert_id($this->db->prefix() . "product");
923
924
                        if ($id > 0) {
925
                            $this->id = $id;
926
                            $this->price            = $price_ht;
0 ignored issues
show
Documentation Bug introduced by
It seems like $price_ht can also be of type string. However, the property $price is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
927
                            $this->price_ttc        = $price_ttc;
0 ignored issues
show
Documentation Bug introduced by
It seems like $price_ttc can also be of type string. However, the property $price_ttc is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
928
                            $this->price_min        = $price_min_ht;
0 ignored issues
show
Documentation Bug introduced by
It seems like $price_min_ht can also be of type string. However, the property $price_min is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
929
                            $this->price_min_ttc    = $price_min_ttc;
0 ignored issues
show
Documentation Bug introduced by
It seems like $price_min_ttc can also be of type string. However, the property $price_min_ttc is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
930
931
                            $result = $this->_log_price($user);
932
                            if ($result > 0) {
933
                                if ($this->update($id, $user, true, 'add') <= 0) {
934
                                    $error++;
935
                                }
936
                            } else {
937
                                $error++;
938
                                $this->error = $this->db->lasterror();
939
                            }
940
941
                            // update accountancy for this entity
942
                            if (!$error && getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
943
                                $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " . ((int) $this->id) . " AND entity = " . ((int) $conf->entity));
944
945
                                $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
946
                                $sql .= " fk_product";
947
                                $sql .= ", entity";
948
                                $sql .= ", accountancy_code_buy";
949
                                $sql .= ", accountancy_code_buy_intra";
950
                                $sql .= ", accountancy_code_buy_export";
951
                                $sql .= ", accountancy_code_sell";
952
                                $sql .= ", accountancy_code_sell_intra";
953
                                $sql .= ", accountancy_code_sell_export";
954
                                $sql .= ") VALUES (";
955
                                $sql .= $this->id;
956
                                $sql .= ", " . $conf->entity;
957
                                $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
958
                                $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
959
                                $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
960
                                $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
961
                                $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
962
                                $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
963
                                $sql .= ")";
964
                                $result = $this->db->query($sql);
965
                                if (!$result) {
966
                                    $error++;
967
                                    $this->error = 'ErrorFailedToInsertAccountancyForEntity';
968
                                }
969
                            }
970
                        } else {
971
                            $error++;
972
                            $this->error = 'ErrorFailedToGetInsertedId';
973
                        }
974
                    } else {
975
                        $error++;
976
                        $this->error = $this->db->lasterror();
977
                    }
978
                } else {
979
                    // Product already exists with this ref
980
                    $langs->load("products");
981
                    $error++;
982
                    $this->error = "ErrorProductAlreadyExists";
983
                    dol_syslog(get_class($this) . "::Create fails, ref " . $this->ref . " already exists");
984
                }
985
            } else {
986
                $error++;
987
                $this->error = $this->db->lasterror();
988
            }
989
990
            if (!$error && !$notrigger) {
991
                // Call trigger
992
                $result = $this->call_trigger('PRODUCT_CREATE', $user);
993
                if ($result < 0) {
994
                    $error++;
995
                }
996
                // End call triggers
997
            }
998
999
            if (!$error) {
1000
                $this->db->commit();
1001
                return $this->id;
1002
            } else {
1003
                $this->db->rollback();
1004
                return -$error;
1005
            }
1006
        } else {
1007
            $this->db->rollback();
1008
            dol_syslog(get_class($this) . "::Create fails verify " . implode(',', $this->errors), LOG_WARNING);
1009
            return -3;
1010
        }
1011
    }
1012
1013
1014
    /**
1015
     *    Check properties of product are ok (like name, barcode, ...).
1016
     *    All properties must be already loaded on object (this->barcode, this->barcode_type_code, ...).
1017
     *
1018
     * @return int        0 if OK, <0 if KO
1019
     */
1020
    public function verify()
1021
    {
1022
        global $langs;
1023
1024
        $this->errors = array();
1025
1026
        $result = 0;
1027
        $this->ref = trim($this->ref);
1028
1029
        if (!$this->ref) {
1030
            $this->errors[] = 'ErrorBadRef';
1031
            $result = -2;
1032
        }
1033
1034
        $arrayofnonnegativevalue = array('weight' => 'Weight', 'width' => 'Width', 'height' => 'Height', 'length' => 'Length', 'surface' => 'Surface', 'volume' => 'Volume');
1035
        foreach ($arrayofnonnegativevalue as $key => $value) {
1036
            if (property_exists($this, $key) && !empty($this->$key) && ($this->$key < 0)) {
1037
                $langs->loadLangs(array("main", "other"));
1038
                $this->error = $langs->trans("FieldCannotBeNegative", $langs->transnoentitiesnoconv($value));
1039
                $this->errors[] = $this->error;
1040
                $result = -4;
1041
            }
1042
        }
1043
1044
        $rescode = $this->check_barcode($this->barcode, $this->barcode_type_code);
1045
        if ($rescode) {
1046
            if ($rescode == -1) {
1047
                $this->errors[] = 'ErrorBadBarCodeSyntax';
1048
            } elseif ($rescode == -2) {
1049
                $this->errors[] = 'ErrorBarCodeRequired';
1050
            } elseif ($rescode == -3) {
1051
                // Note: Common usage is to have barcode unique. For variants, we should have a different barcode.
1052
                $this->errors[] = 'ErrorBarCodeAlreadyUsed';
1053
            }
1054
1055
            $result = -3;
1056
        }
1057
1058
        return $result;
1059
    }
1060
1061
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1062
    /**
1063
     *  Check barcode
1064
     *
1065
     * @param  string $valuetotest Value to test
1066
     * @param  string $typefortest Type of barcode (ISBN, EAN, ...)
1067
     * @return int                        0 if OK
1068
     *                                     -1 ErrorBadBarCodeSyntax
1069
     *                                     -2 ErrorBarCodeRequired
1070
     *                                     -3 ErrorBarCodeAlreadyUsed
1071
     */
1072
    public function check_barcode($valuetotest, $typefortest)
1073
    {
1074
		// phpcs:enable
1075
        global $conf;
1076
        if (isModEnabled('barcode') && getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
1077
            $module = strtolower(getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM'));
1078
1079
            $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
1080
            foreach ($dirsociete as $dirroot) {
1081
                $res = dol_include_once($dirroot . $module . '.php');
1082
                if ($res) {
1083
                    break;
1084
                }
1085
            }
1086
1087
            $mod = new $module();
1088
            '@phan-var-force ModeleNumRefBarCode $mod';
1089
1090
            dol_syslog(get_class($this) . "::check_barcode value=" . $valuetotest . " type=" . $typefortest . " module=" . $module);
1091
            $result = $mod->verif($this->db, $valuetotest, $this, 0, $typefortest);
1092
            return $result;
1093
        } else {
1094
            return 0;
1095
        }
1096
    }
1097
1098
    /**
1099
     *  Update a record into database.
1100
     *  If batch flag is set to on, we create records into llx_product_batch
1101
     *
1102
     * @param  int     $id          Id of product
1103
     * @param  User    $user        Object user making update
1104
     * @param  int     $notrigger   Disable triggers
1105
     * @param  string  $action      Current action for hookmanager ('add' or 'update')
1106
     * @param  boolean $updatetype  Update product type
1107
     * @return int                  1 if OK, -1 if ref already exists, -2 if other error
1108
     */
1109
    public function update($id, $user, $notrigger = 0, $action = 'update', $updatetype = false)
1110
    {
1111
        global $langs, $conf, $hookmanager;
1112
1113
        $error = 0;
1114
1115
        // Check parameters
1116
        if (!$this->label) {
1117
            $this->label = 'MISSING LABEL';
1118
        }
1119
1120
        // Clean parameters
1121
        if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
1122
            $this->ref = trim($this->ref);
1123
        } else {
1124
            $this->ref = dol_string_nospecial(trim($this->ref));
1125
        }
1126
        $this->label = trim($this->label);
1127
        $this->description = trim($this->description);
1128
        $this->note_private = (isset($this->note_private) ? trim($this->note_private) : null);
1129
        $this->note_public = (isset($this->note_public) ? trim($this->note_public) : null);
1130
        $this->net_measure = price2num($this->net_measure);
1131
        $this->net_measure_units = (empty($this->net_measure_units) ? '' : trim($this->net_measure_units));
1132
        $this->weight = price2num($this->weight);
1133
        $this->weight_units = (empty($this->weight_units) ? '' : trim($this->weight_units));
1134
        $this->length = price2num($this->length);
1135
        $this->length_units = (empty($this->length_units) ? '' : trim($this->length_units));
1136
        $this->width = price2num($this->width);
1137
        $this->width_units = (empty($this->width_units) ? '' : trim($this->width_units));
1138
        $this->height = price2num($this->height);
1139
        $this->height_units = (empty($this->height_units) ? '' : trim($this->height_units));
1140
        $this->surface = price2num($this->surface);
1141
        $this->surface_units = (empty($this->surface_units) ? '' : trim($this->surface_units));
1142
        $this->volume = price2num($this->volume);
1143
        $this->volume_units = (empty($this->volume_units) ? '' : trim($this->volume_units));
1144
1145
        // set unit not defined
1146
        if (is_numeric($this->length_units)) {
1147
            $this->width_units = $this->length_units; // Not used yet
1148
        }
1149
        if (is_numeric($this->length_units)) {
1150
            $this->height_units = $this->length_units; // Not used yet
1151
        }
1152
1153
        // Automated compute surface and volume if not filled
1154
        if (empty($this->surface) && !empty($this->length) && !empty($this->width) && $this->length_units == $this->width_units) {
1155
            $this->surface = (float) $this->length * (float) $this->width;
1156
            $this->surface_units = measuring_units_squared($this->length_units);
1157
        }
1158
        if (empty($this->volume) && !empty($this->surface) && !empty($this->height) && $this->length_units == $this->height_units) {
1159
            $this->volume = $this->surface * (float) $this->height;
1160
            $this->volume_units = measuring_units_cubed($this->height_units);
1161
        }
1162
1163
        if (empty($this->tva_tx)) {
1164
            $this->tva_tx = 0;
1165
        }
1166
        if (empty($this->tva_npr)) {
1167
            $this->tva_npr = 0;
1168
        }
1169
        if (empty($this->localtax1_tx)) {
1170
            $this->localtax1_tx = 0;
1171
        }
1172
        if (empty($this->localtax2_tx)) {
1173
            $this->localtax2_tx = 0;
1174
        }
1175
        if (empty($this->localtax1_type)) {
1176
            $this->localtax1_type = '0';
1177
        }
1178
        if (empty($this->localtax2_type)) {
1179
            $this->localtax2_type = '0';
1180
        }
1181
        if (empty($this->status)) {
1182
            $this->status = 0;
1183
        }
1184
        if (empty($this->status_buy)) {
1185
            $this->status_buy = 0;
1186
        }
1187
1188
        if (empty($this->country_id)) {
1189
            $this->country_id = 0;
1190
        }
1191
1192
        if (empty($this->state_id)) {
1193
            $this->state_id = 0;
1194
        }
1195
1196
        // Barcode value
1197
        $this->barcode = (empty($this->barcode) ? '' : trim($this->barcode));
1198
1199
        $this->accountancy_code_buy = trim($this->accountancy_code_buy);
1200
        $this->accountancy_code_buy_intra = (!empty($this->accountancy_code_buy_intra) ? trim($this->accountancy_code_buy_intra) : '');
1201
        $this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export);
1202
        $this->accountancy_code_sell = trim($this->accountancy_code_sell);
1203
        $this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra);
1204
        $this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export);
1205
1206
1207
        $this->db->begin();
1208
1209
        $result = 0;
1210
        // Check name is required and codes are ok or unique. If error, this->errors[] is filled
1211
        if ($action != 'add') {
1212
            $result = $this->verify(); // We don't check when update called during a create because verify was already done
1213
        } else {
1214
            // we can continue
1215
            $result = 0;
1216
        }
1217
1218
        if ($result >= 0) {
1219
            // $this->oldcopy should have been set by the caller of update (here properties were already modified)
1220
            if (is_null($this->oldcopy) || (is_object($this->oldcopy) && $this->oldcopy->isEmpty())) {
1221
                $this->oldcopy = dol_clone($this, 1);
1222
            }
1223
            // Test if batch management is activated on existing product
1224
            // If yes, we create missing entries into product_batch
1225
            if ($this->hasbatch() && !$this->oldcopy->hasbatch()) {
1226
                //$valueforundefinedlot = 'Undefined';  // In previous version, 39 and lower
1227
                $valueforundefinedlot = '000000';
1228
                if (getDolGlobalString('STOCK_DEFAULT_BATCH')) {
1229
                    $valueforundefinedlot = getDolGlobalString('STOCK_DEFAULT_BATCH');
1230
                }
1231
1232
                dol_syslog("Flag batch of product id=" . $this->id . " is set to ON, so we will create missing records into product_batch");
1233
1234
                $this->load_stock();
1235
                foreach ($this->stock_warehouse as $idW => $ObjW) {   // For each warehouse where we have stocks defined for this product (for each lines in product_stock)
1236
                    $qty_batch = 0;
1237
                    foreach ($ObjW->detail_batch as $detail) {    // Each lines of detail in product_batch of the current $ObjW = product_stock
1238
                        if ($detail->batch == $valueforundefinedlot || $detail->batch == 'Undefined') {
1239
                            // We discard this line, we will create it later
1240
                            $sqlclean = "DELETE FROM " . $this->db->prefix() . "product_batch WHERE batch in('Undefined', '" . $this->db->escape($valueforundefinedlot) . "') AND fk_product_stock = " . ((int) $ObjW->id);
1241
                            $result = $this->db->query($sqlclean);
1242
                            if (!$result) {
1243
                                dol_print_error($this->db);
1244
                                exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
1245
                            }
1246
                            continue;
1247
                        }
1248
1249
                        $qty_batch += $detail->qty;
1250
                    }
1251
                    // Quantities in batch details are not same as stock quantity,
1252
                    // so we add a default batch record to complete and get same qty in parent and child table
1253
                    if ($ObjW->real != $qty_batch) {
1254
                        $ObjBatch = new Productbatch($this->db);
1255
                        $ObjBatch->batch = $valueforundefinedlot;
1256
                        $ObjBatch->qty = ($ObjW->real - $qty_batch);
1257
                        $ObjBatch->fk_product_stock = $ObjW->id;
1258
1259
                        if ($ObjBatch->create($user, 1) < 0) {
1260
                            $error++;
1261
                            $this->errors = $ObjBatch->errors;
1262
                        } else {
1263
                            // we also add lot record if not exist
1264
                            $ObjLot = new Productlot($this->db);
1265
                            // @phan-suppress-next-line PhanPluginSuspiciousParamPosition
1266
                            if ($ObjLot->fetch(0, $this->id, $valueforundefinedlot) == 0) {
1267
                                $ObjLot->fk_product = $this->id;
1268
                                $ObjLot->entity = $this->entity;
1269
                                $ObjLot->fk_user_creat = $user->id;
1270
                                $ObjLot->batch = $valueforundefinedlot;
1271
                                if ($ObjLot->create($user, true) < 0) {
1272
                                    $error++;
1273
                                    $this->errors = $ObjLot->errors;
1274
                                }
1275
                            }
1276
                        }
1277
                    }
1278
                }
1279
            }
1280
1281
            // For automatic creation
1282
            if ($this->barcode == -1) {
1283
                $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
1284
            }
1285
1286
            $sql = "UPDATE " . $this->db->prefix() . "product";
1287
            $sql .= " SET label = '" . $this->db->escape($this->label) . "'";
1288
1289
            if ($updatetype && ($this->isProduct() || $this->isService())) {
1290
                $sql .= ", fk_product_type = " . ((int) $this->type);
1291
            }
1292
1293
            $sql .= ", ref = '" . $this->db->escape($this->ref) . "'";
1294
            $sql .= ", ref_ext = " . (!empty($this->ref_ext) ? "'" . $this->db->escape($this->ref_ext) . "'" : "null");
1295
            $sql .= ", default_vat_code = " . ($this->default_vat_code ? "'" . $this->db->escape($this->default_vat_code) . "'" : "null");
1296
            $sql .= ", tva_tx = " . ((float) $this->tva_tx);
1297
            $sql .= ", recuperableonly = " . ((int) $this->tva_npr);
1298
            $sql .= ", localtax1_tx = " . ((float) $this->localtax1_tx);
1299
            $sql .= ", localtax2_tx = " . ((float) $this->localtax2_tx);
1300
            $sql .= ", localtax1_type = " . ($this->localtax1_type != '' ? "'" . $this->db->escape($this->localtax1_type) . "'" : "'0'");
1301
            $sql .= ", localtax2_type = " . ($this->localtax2_type != '' ? "'" . $this->db->escape($this->localtax2_type) . "'" : "'0'");
1302
1303
            $sql .= ", barcode = " . (empty($this->barcode) ? "null" : "'" . $this->db->escape($this->barcode) . "'");
1304
            $sql .= ", fk_barcode_type = " . (empty($this->barcode_type) ? "null" : $this->db->escape($this->barcode_type));
1305
1306
            $sql .= ", tosell = " . (int) $this->status;
1307
            $sql .= ", tobuy = " . (int) $this->status_buy;
1308
            $sql .= ", tobatch = " . ((empty($this->status_batch) || $this->status_batch < 0) ? '0' : (int) $this->status_batch);
1309
            $sql .= ", sell_or_eat_by_mandatory = " . ((empty($this->sell_or_eat_by_mandatory) || $this->sell_or_eat_by_mandatory < 0) ? 0 : (int) $this->sell_or_eat_by_mandatory);
1310
            $sql .= ", batch_mask = '" . $this->db->escape($this->batch_mask) . "'";
1311
1312
            $sql .= ", finished = " . ((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? "null" : (int) $this->finished);
1313
            $sql .= ", fk_default_bom = " . ((!isset($this->fk_default_bom) || $this->fk_default_bom < 0 || $this->fk_default_bom == '') ? "null" : (int) $this->fk_default_bom);
1314
            $sql .= ", net_measure = " . ($this->net_measure != '' ? "'" . $this->db->escape($this->net_measure) . "'" : 'null');
1315
            $sql .= ", net_measure_units = " . ($this->net_measure_units != '' ? "'" . $this->db->escape($this->net_measure_units) . "'" : 'null');
1316
            $sql .= ", weight = " . ($this->weight != '' ? "'" . $this->db->escape($this->weight) . "'" : 'null');
1317
            $sql .= ", weight_units = " . ($this->weight_units != '' ? "'" . $this->db->escape($this->weight_units) . "'" : 'null');
1318
            $sql .= ", length = " . ($this->length != '' ? "'" . $this->db->escape($this->length) . "'" : 'null');
1319
            $sql .= ", length_units = " . ($this->length_units != '' ? "'" . $this->db->escape($this->length_units) . "'" : 'null');
1320
            $sql .= ", width= " . ($this->width != '' ? "'" . $this->db->escape($this->width) . "'" : 'null');
1321
            $sql .= ", width_units = " . ($this->width_units != '' ? "'" . $this->db->escape($this->width_units) . "'" : 'null');
1322
            $sql .= ", height = " . ($this->height != '' ? "'" . $this->db->escape($this->height) . "'" : 'null');
1323
            $sql .= ", height_units = " . ($this->height_units != '' ? "'" . $this->db->escape($this->height_units) . "'" : 'null');
1324
            $sql .= ", surface = " . ($this->surface != '' ? "'" . $this->db->escape($this->surface) . "'" : 'null');
1325
            $sql .= ", surface_units = " . ($this->surface_units != '' ? "'" . $this->db->escape($this->surface_units) . "'" : 'null');
1326
            $sql .= ", volume = " . ($this->volume != '' ? "'" . $this->db->escape($this->volume) . "'" : 'null');
1327
            $sql .= ", volume_units = " . ($this->volume_units != '' ? "'" . $this->db->escape($this->volume_units) . "'" : 'null');
1328
            $sql .= ", fk_default_warehouse = " . ($this->fk_default_warehouse > 0 ? ((int) $this->fk_default_warehouse) : 'null');
1329
            $sql .= ", fk_default_workstation = " . ($this->fk_default_workstation > 0 ? ((int) $this->fk_default_workstation) : 'null');
1330
            $sql .= ", seuil_stock_alerte = " . ((isset($this->seuil_stock_alerte) && is_numeric($this->seuil_stock_alerte)) ? (float) $this->seuil_stock_alerte : 'null');
1331
            $sql .= ", description = '" . $this->db->escape($this->description) . "'";
1332
            $sql .= ", url = " . ($this->url ? "'" . $this->db->escape($this->url) . "'" : 'null');
1333
            $sql .= ", customcode = '" . $this->db->escape($this->customcode) . "'";
1334
            $sql .= ", fk_country = " . ($this->country_id > 0 ? (int) $this->country_id : 'null');
1335
            $sql .= ", fk_state = " . ($this->state_id > 0 ? (int) $this->state_id : 'null');
1336
            $sql .= ", lifetime = " . ($this->lifetime > 0 ? (int) $this->lifetime : 'null');
1337
            $sql .= ", qc_frequency = " . ($this->qc_frequency > 0 ? (int) $this->qc_frequency : 'null');
1338
            $sql .= ", note = " . (isset($this->note_private) ? "'" . $this->db->escape($this->note_private) . "'" : 'null');
1339
            $sql .= ", note_public = " . (isset($this->note_public) ? "'" . $this->db->escape($this->note_public) . "'" : 'null');
1340
            $sql .= ", duration = '" . $this->db->escape($this->duration_value . $this->duration_unit) . "'";
1341
            if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1342
                $sql .= ", accountancy_code_buy = '" . $this->db->escape($this->accountancy_code_buy) . "'";
1343
                $sql .= ", accountancy_code_buy_intra = '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1344
                $sql .= ", accountancy_code_buy_export = '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1345
                $sql .= ", accountancy_code_sell= '" . $this->db->escape($this->accountancy_code_sell) . "'";
1346
                $sql .= ", accountancy_code_sell_intra= '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1347
                $sql .= ", accountancy_code_sell_export= '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1348
            }
1349
            $sql .= ", desiredstock = " . ((isset($this->desiredstock) && is_numeric($this->desiredstock)) ? (float) $this->desiredstock : "null");
1350
            $sql .= ", cost_price = " . ($this->cost_price != '' ? $this->db->escape($this->cost_price) : 'null');
1351
            $sql .= ", fk_unit= " . (!$this->fk_unit ? 'NULL' : (int) $this->fk_unit);
1352
            $sql .= ", price_autogen = " . (!$this->price_autogen ? 0 : 1);
1353
            $sql .= ", fk_price_expression = " . ($this->fk_price_expression != 0 ? (int) $this->fk_price_expression : 'NULL');
1354
            $sql .= ", fk_user_modif = " . ($user->id > 0 ? $user->id : 'NULL');
1355
            $sql .= ", mandatory_period = " . ($this->mandatory_period);
1356
            // stock field is not here because it is a denormalized value from product_stock.
1357
            $sql .= " WHERE rowid = " . ((int) $id);
1358
1359
            dol_syslog(get_class($this) . "::update", LOG_DEBUG);
1360
1361
            $resql = $this->db->query($sql);
1362
            if ($resql) {
1363
                $this->id = $id;
1364
1365
                // Multilangs
1366
                if (getDolGlobalInt('MAIN_MULTILANGS')) {
1367
                    if ($this->setMultiLangs($user) < 0) {
1368
                        $this->error = $langs->trans("Error") . " : " . $this->db->error() . " - " . $sql;
1369
                        $this->db->rollback();
1370
                        return -2;
1371
                    }
1372
                }
1373
1374
                $action = 'update';
1375
1376
                // update accountancy for this entity
1377
                if (!$error && getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1378
                    $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " . ((int) $this->id) . " AND entity = " . ((int) $conf->entity));
1379
1380
                    $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
1381
                    $sql .= " fk_product";
1382
                    $sql .= ", entity";
1383
                    $sql .= ", accountancy_code_buy";
1384
                    $sql .= ", accountancy_code_buy_intra";
1385
                    $sql .= ", accountancy_code_buy_export";
1386
                    $sql .= ", accountancy_code_sell";
1387
                    $sql .= ", accountancy_code_sell_intra";
1388
                    $sql .= ", accountancy_code_sell_export";
1389
                    $sql .= ") VALUES (";
1390
                    $sql .= $this->id;
1391
                    $sql .= ", " . $conf->entity;
1392
                    $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
1393
                    $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1394
                    $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1395
                    $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
1396
                    $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1397
                    $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1398
                    $sql .= ")";
1399
                    $result = $this->db->query($sql);
1400
                    if (!$result) {
1401
                        $error++;
1402
                        $this->error = 'ErrorFailedToUpdateAccountancyForEntity';
1403
                    }
1404
                }
1405
1406
                if (!$this->hasbatch() && $this->oldcopy->hasbatch()) {
1407
                    // Selection of all product stock movements that contains batchs
1408
                    $sql = 'SELECT pb.qty, ps.fk_entrepot, pb.batch FROM ' . MAIN_DB_PREFIX . 'product_batch as pb';
1409
                    $sql .= ' INNER JOIN ' . MAIN_DB_PREFIX . 'product_stock as ps ON (ps.rowid = pb.fk_product_stock)';
1410
                    $sql .= ' WHERE ps.fk_product = ' . (int) $this->id;
1411
1412
                    $resql = $this->db->query($sql);
1413
                    if ($resql) {
1414
                        $inventorycode = dol_print_date(dol_now(), '%Y%m%d%H%M%S');
1415
1416
                        while ($obj = $this->db->fetch_object($resql)) {
1417
                            $value = $obj->qty;
1418
                            $fk_entrepot = $obj->fk_entrepot;
1419
                            $price = 0;
1420
                            $dlc = '';
1421
                            $dluo = '';
1422
                            $batch = $obj->batch;
1423
1424
                            // To know how to revert stockMouvement (add or remove)
1425
                            $addOremove = $value > 0 ? 1 : 0; // 1 if remove, 0 if add
1426
                            $label = $langs->trans('BatchStockMouvementAddInGlobal');
1427
                            $res = $this->correct_stock_batch($user, $fk_entrepot, abs($value), $addOremove, $label, $price, $dlc, $dluo, $batch, $inventorycode, '', null, 0, null, true);
1428
1429
                            if ($res > 0) {
1430
                                $label = $langs->trans('BatchStockMouvementAddInGlobal');
1431
                                $res = $this->correct_stock($user, $fk_entrepot, abs($value), (int) empty($addOremove), $label, $price, $inventorycode, '', null, 0);
1432
                                if ($res < 0) {
1433
                                    $error++;
1434
                                }
1435
                            } else {
1436
                                $error++;
1437
                            }
1438
                        }
1439
                    }
1440
                }
1441
1442
                // Actions on extra fields
1443
                if (!$error) {
1444
                    $result = $this->insertExtraFields();
1445
                    if ($result < 0) {
1446
                        $error++;
1447
                    }
1448
                }
1449
1450
                if (!$error && !$notrigger) {
1451
                    // Call trigger
1452
                    $result = $this->call_trigger('PRODUCT_MODIFY', $user);
1453
                    if ($result < 0) {
1454
                        $error++;
1455
                    }
1456
                    // End call triggers
1457
                }
1458
1459
                if (!$error && (is_object($this->oldcopy) && $this->oldcopy->ref !== $this->ref)) {
1460
                    // We remove directory
1461
                    if ($conf->product->dir_output) {
1462
                        $olddir = $conf->product->dir_output . "/" . dol_sanitizeFileName($this->oldcopy->ref);
1463
                        $newdir = $conf->product->dir_output . "/" . dol_sanitizeFileName($this->ref);
1464
                        if (file_exists($olddir)) {
1465
                            //include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
1466
                            //$res = dol_move($olddir, $newdir);
1467
                            // do not use dol_move with directory
1468
                            $res = @rename($olddir, $newdir);
1469
                            if (!$res) {
1470
                                $langs->load("errors");
1471
                                $this->error = $langs->trans('ErrorFailToRenameDir', $olddir, $newdir);
1472
                                $error++;
1473
                            }
1474
                        }
1475
                    }
1476
                }
1477
1478
                if (!$error) {
1479
                    if (isModEnabled('variants')) {
1480
                        include_once DOL_DOCUMENT_ROOT . '/variants/class/ProductCombination.class.php';
1481
1482
                        $comb = new ProductCombination($this->db);
0 ignored issues
show
Bug introduced by
The type Dolibarr\Code\Product\Classes\ProductCombination was not found. Did you mean ProductCombination? If so, make sure to prefix the type with \.
Loading history...
1483
1484
                        foreach ($comb->fetchAllByFkProductParent($this->id) as $currcomb) {
1485
                            $currcomb->updateProperties($this, $user);
1486
                        }
1487
                    }
1488
1489
                    $this->db->commit();
1490
                    return 1;
1491
                } else {
1492
                    $this->db->rollback();
1493
                    return -$error;
1494
                }
1495
            } else {
1496
                if ($this->db->errno() == 'DB_ERROR_RECORD_ALREADY_EXISTS') {
1497
                    $langs->load("errors");
1498
                    if (empty($conf->barcode->enabled) || empty($this->barcode)) {
1499
                        $this->error = $langs->trans("Error") . " : " . $langs->trans("ErrorProductAlreadyExists", $this->ref);
1500
                    } else {
1501
                        $this->error = $langs->trans("Error") . " : " . $langs->trans("ErrorProductBarCodeAlreadyExists", $this->barcode);
1502
                    }
1503
                    $this->errors[] = $this->error;
1504
                    $this->db->rollback();
1505
                    return -1;
1506
                } else {
1507
                    $this->error = $langs->trans("Error") . " : " . $this->db->error() . " - " . $sql;
1508
                    $this->errors[] = $this->error;
1509
                    $this->db->rollback();
1510
                    return -2;
1511
                }
1512
            }
1513
        } else {
1514
            $this->db->rollback();
1515
            dol_syslog(get_class($this) . "::Update fails verify " . implode(',', $this->errors), LOG_WARNING);
1516
            return -3;
1517
        }
1518
    }
1519
1520
    /**
1521
     *  Delete a product from database (if not used)
1522
     *
1523
     * @param  User $user      User (object) deleting product
1524
     * @param  int  $notrigger Do not execute trigger
1525
     * @return int                    Return integer < 0 if KO, 0 = Not possible, > 0 if OK
1526
     */
1527
    public function delete(User $user, $notrigger = 0)
1528
    {
1529
        global $conf, $langs;
1530
        include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
1531
1532
        $error = 0;
1533
1534
        // Check parameters
1535
        if (empty($this->id)) {
1536
            $this->error = "Object must be fetched before calling delete";
1537
            return -1;
1538
        }
1539
        if (($this->type == Product::TYPE_PRODUCT && !$user->hasRight('produit', 'supprimer')) || ($this->type == Product::TYPE_SERVICE && !$user->hasRight('service', 'supprimer'))) {
1540
            $this->error = "ErrorForbidden";
1541
            return 0;
1542
        }
1543
1544
        $objectisused = $this->isObjectUsed($this->id);
1545
        if (empty($objectisused)) {
1546
            $this->db->begin();
1547
1548
            if (!$error && empty($notrigger)) {
1549
                // Call trigger
1550
                $result = $this->call_trigger('PRODUCT_DELETE', $user);
1551
                if ($result < 0) {
1552
                    $error++;
1553
                }
1554
                // End call triggers
1555
            }
1556
1557
            // Delete from product_batch on product delete
1558
            if (!$error) {
1559
                $sql = "DELETE FROM " . $this->db->prefix() . 'product_batch';
1560
                $sql .= " WHERE fk_product_stock IN (";
1561
                $sql .= "SELECT rowid FROM " . $this->db->prefix() . 'product_stock';
1562
                $sql .= " WHERE fk_product = " . ((int) $this->id) . ")";
1563
1564
                $result = $this->db->query($sql);
1565
                if (!$result) {
1566
                    $error++;
1567
                    $this->errors[] = $this->db->lasterror();
1568
                }
1569
            }
1570
1571
            // Delete all child tables
1572
            if (!$error) {
1573
                $elements = array('product_fournisseur_price', 'product_price', 'product_lang', 'categorie_product', 'product_stock', 'product_customer_price', 'product_lot'); // product_batch is done before
1574
                foreach ($elements as $table) {
1575
                    if (!$error) {
1576
                        $sql = "DELETE FROM " . $this->db->prefix() . $table;
1577
                        $sql .= " WHERE fk_product = " . (int) $this->id;
1578
1579
                        $result = $this->db->query($sql);
1580
                        if (!$result) {
1581
                            $error++;
1582
                            $this->errors[] = $this->db->lasterror();
1583
                        }
1584
                    }
1585
                }
1586
            }
1587
1588
            if (!$error) {
1589
                include_once DOL_DOCUMENT_ROOT . '/variants/class/ProductCombination.class.php';
1590
                include_once DOL_DOCUMENT_ROOT . '/variants/class/ProductCombination2ValuePair.class.php';
1591
1592
                //If it is a parent product, then we remove the association with child products
1593
                $prodcomb = new ProductCombination($this->db);
1594
1595
                if ($prodcomb->deleteByFkProductParent($user, $this->id) < 0) {
1596
                    $error++;
1597
                    $this->errors[] = 'Error deleting combinations';
1598
                }
1599
1600
                //We also check if it is a child product
1601
                if (!$error && ($prodcomb->fetchByFkProductChild($this->id) > 0) && ($prodcomb->delete($user) < 0)) {
1602
                    $error++;
1603
                    $this->errors[] = 'Error deleting child combination';
1604
                }
1605
            }
1606
1607
            // Delete from product_association
1608
            if (!$error) {
1609
                $sql = "DELETE FROM " . $this->db->prefix() . "product_association";
1610
                $sql .= " WHERE fk_product_pere = " . (int) $this->id . " OR fk_product_fils = " . (int) $this->id;
1611
1612
                $result = $this->db->query($sql);
1613
                if (!$result) {
1614
                    $error++;
1615
                    $this->errors[] = $this->db->lasterror();
1616
                }
1617
            }
1618
1619
            // Remove extrafields
1620
            if (!$error) {
1621
                $result = $this->deleteExtraFields();
1622
                if ($result < 0) {
1623
                    $error++;
1624
                    dol_syslog(get_class($this) . "::delete error -4 " . $this->error, LOG_ERR);
1625
                }
1626
            }
1627
1628
            // Delete product
1629
            if (!$error) {
1630
                $sqlz = "DELETE FROM " . $this->db->prefix() . "product";
1631
                $sqlz .= " WHERE rowid = " . (int) $this->id;
1632
1633
                $resultz = $this->db->query($sqlz);
1634
                if (!$resultz) {
1635
                    $error++;
1636
                    $this->errors[] = $this->db->lasterror();
1637
                }
1638
            }
1639
1640
            // Delete record into ECM index and physically
1641
            if (!$error) {
1642
                $res = $this->deleteEcmFiles(0); // Deleting files physically is done later with the dol_delete_dir_recursive
1643
                $res = $this->deleteEcmFiles(1); // Deleting files physically is done later with the dol_delete_dir_recursive
1644
                if (!$res) {
1645
                    $error++;
1646
                }
1647
            }
1648
1649
            if (!$error) {
1650
                // We remove directory
1651
                $ref = dol_sanitizeFileName($this->ref);
1652
                if ($conf->product->dir_output) {
1653
                    $dir = $conf->product->dir_output . "/" . $ref;
1654
                    if (file_exists($dir)) {
1655
                        $res = @dol_delete_dir_recursive($dir);
1656
                        if (!$res) {
1657
                            $this->errors[] = 'ErrorFailToDeleteDir';
1658
                            $error++;
1659
                        }
1660
                    }
1661
                }
1662
            }
1663
1664
            if (!$error) {
1665
                $this->db->commit();
1666
                return 1;
1667
            } else {
1668
                foreach ($this->errors as $errmsg) {
1669
                    dol_syslog(get_class($this) . "::delete " . $errmsg, LOG_ERR);
1670
                    $this->error .= ($this->error ? ', ' . $errmsg : $errmsg);
1671
                }
1672
                $this->db->rollback();
1673
                return -$error;
1674
            }
1675
        } else {
1676
            $this->error = "ErrorRecordIsUsedCantDelete";
1677
            return 0;
1678
        }
1679
    }
1680
1681
    /**
1682
     * Get sell or eat by mandatory list
1683
     *
1684
     * @return  array   Sell or eat by mandatory list
1685
     */
1686
    public static function getSellOrEatByMandatoryList()
1687
    {
1688
        global $langs;
1689
1690
        $sellByLabel = $langs->trans('SellByDate');
1691
        $eatByLabel = $langs->trans('EatByDate');
1692
        return array(
1693
            self::SELL_OR_EAT_BY_MANDATORY_ID_NONE => $langs->trans('BatchSellOrEatByMandatoryNone'),
1694
            self::SELL_OR_EAT_BY_MANDATORY_ID_SELL_BY => $sellByLabel,
1695
            self::SELL_OR_EAT_BY_MANDATORY_ID_EAT_BY => $eatByLabel,
1696
            self::SELL_OR_EAT_BY_MANDATORY_ID_SELL_AND_EAT => $langs->trans('BatchSellOrEatByMandatoryAll', $sellByLabel, $eatByLabel),
1697
        );
1698
    }
1699
1700
    /**
1701
     * Get sell or eat by mandatory label
1702
     *
1703
     * @return  string  Sell or eat by mandatory label
1704
     */
1705
    public function getSellOrEatByMandatoryLabel()
1706
    {
1707
        $sellOrEatByMandatoryLabel = '';
1708
1709
        $sellOrEatByMandatoryList = self::getSellOrEatByMandatoryList();
1710
        if (isset($sellOrEatByMandatoryList[$this->sell_or_eat_by_mandatory])) {
1711
            $sellOrEatByMandatoryLabel = $sellOrEatByMandatoryList[$this->sell_or_eat_by_mandatory];
1712
        }
1713
1714
        return $sellOrEatByMandatoryLabel;
1715
    }
1716
1717
    /**
1718
     *    Update or add a translation for a product
1719
     *
1720
     * @param  User $user Object user making update
1721
     * @return int        Return integer <0 if KO, >0 if OK
1722
     */
1723
    public function setMultiLangs($user)
1724
    {
1725
        global $conf, $langs;
1726
1727
        $langs_available = $langs->get_available_languages(DOL_DOCUMENT_ROOT, 0, 2);
1728
        $current_lang = $langs->getDefaultLang();
1729
1730
        foreach ($langs_available as $key => $value) {
1731
            if ($key == $current_lang) {
1732
                $sql = "SELECT rowid";
1733
                $sql .= " FROM " . $this->db->prefix() . "product_lang";
1734
                $sql .= " WHERE fk_product = " . ((int) $this->id);
1735
                $sql .= " AND lang = '" . $this->db->escape($key) . "'";
1736
1737
                $result = $this->db->query($sql);
1738
1739
                if ($this->db->num_rows($result)) { // if there is already a description line for this language
1740
                    $sql2 = "UPDATE " . $this->db->prefix() . "product_lang";
1741
                    $sql2 .= " SET ";
1742
                    $sql2 .= " label='" . $this->db->escape($this->label) . "',";
1743
                    $sql2 .= " description='" . $this->db->escape($this->description) . "'";
1744
                    if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1745
                        $sql2 .= ", note='" . $this->db->escape($this->other) . "'";
1746
                    }
1747
                    $sql2 .= " WHERE fk_product = " . ((int) $this->id) . " AND lang = '" . $this->db->escape($key) . "'";
1748
                } else {
1749
                    $sql2 = "INSERT INTO " . $this->db->prefix() . "product_lang (fk_product, lang, label, description";
1750
                    if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1751
                        $sql2 .= ", note";
1752
                    }
1753
                    $sql2 .= ")";
1754
                    $sql2 .= " VALUES(" . ((int) $this->id) . ",'" . $this->db->escape($key) . "','" . $this->db->escape($this->label) . "',";
1755
                    $sql2 .= " '" . $this->db->escape($this->description) . "'";
1756
                    if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1757
                        $sql2 .= ", '" . $this->db->escape($this->other) . "'";
1758
                    }
1759
                    $sql2 .= ")";
1760
                }
1761
                dol_syslog(get_class($this) . '::setMultiLangs key = current_lang = ' . $key);
1762
                if (!$this->db->query($sql2)) {
1763
                    $this->error = $this->db->lasterror();
1764
                    return -1;
1765
                }
1766
            } elseif (isset($this->multilangs[$key])) {
1767
                if (empty($this->multilangs["$key"]["label"])) {
1768
                    $this->errors[] = $key . ' : ' . $langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("Label"));
1769
                    return -1;
1770
                }
1771
1772
                $sql = "SELECT rowid";
1773
                $sql .= " FROM " . $this->db->prefix() . "product_lang";
1774
                $sql .= " WHERE fk_product = " . ((int) $this->id);
1775
                $sql .= " AND lang = '" . $this->db->escape($key) . "'";
1776
1777
                $result = $this->db->query($sql);
1778
1779
                if ($this->db->num_rows($result)) { // if there is already a description line for this language
1780
                    $sql2 = "UPDATE " . $this->db->prefix() . "product_lang";
1781
                    $sql2 .= " SET ";
1782
                    $sql2 .= " label = '" . $this->db->escape($this->multilangs["$key"]["label"]) . "',";
1783
                    $sql2 .= " description = '" . $this->db->escape($this->multilangs["$key"]["description"]) . "'";
1784
                    if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1785
                        $sql2 .= ", note = '" . $this->db->escape($this->multilangs["$key"]["other"]) . "'";
1786
                    }
1787
                    $sql2 .= " WHERE fk_product = " . ((int) $this->id) . " AND lang = '" . $this->db->escape($key) . "'";
1788
                } else {
1789
                    $sql2 = "INSERT INTO " . $this->db->prefix() . "product_lang (fk_product, lang, label, description";
1790
                    if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1791
                        $sql2 .= ", note";
1792
                    }
1793
                    $sql2 .= ")";
1794
                    $sql2 .= " VALUES(" . ((int) $this->id) . ",'" . $this->db->escape($key) . "','" . $this->db->escape($this->multilangs["$key"]["label"]) . "',";
1795
                    $sql2 .= " '" . $this->db->escape($this->multilangs["$key"]["description"]) . "'";
1796
                    if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1797
                        $sql2 .= ", '" . $this->db->escape($this->multilangs["$key"]["other"]) . "'";
1798
                    }
1799
                    $sql2 .= ")";
1800
                }
1801
1802
                // We do not save if main fields are empty
1803
                if ($this->multilangs["$key"]["label"] || $this->multilangs["$key"]["description"]) {
1804
                    if (!$this->db->query($sql2)) {
1805
                        $this->error = $this->db->lasterror();
1806
                        return -1;
1807
                    }
1808
                }
1809
            } else {
1810
                // language is not current language and we didn't provide a multilang description for this language
1811
            }
1812
        }
1813
1814
        // Call trigger
1815
        $result = $this->call_trigger('PRODUCT_SET_MULTILANGS', $user);
1816
        if ($result < 0) {
1817
            $this->error = $this->db->lasterror();
1818
            return -1;
1819
        }
1820
        // End call triggers
1821
1822
        return 1;
1823
    }
1824
1825
    /**
1826
     *    Delete a language for this product
1827
     *
1828
     * @param string $langtodelete Language code to delete
1829
     * @param User   $user         Object user making delete
1830
     *
1831
     * @return int                            Return integer <0 if KO, >0 if OK
1832
     */
1833
    public function delMultiLangs($langtodelete, $user)
1834
    {
1835
        $sql = "DELETE FROM " . $this->db->prefix() . "product_lang";
1836
        $sql .= " WHERE fk_product = " . ((int) $this->id) . " AND lang = '" . $this->db->escape($langtodelete) . "'";
1837
1838
        dol_syslog(get_class($this) . '::delMultiLangs', LOG_DEBUG);
1839
        $result = $this->db->query($sql);
1840
        if ($result) {
1841
            // Call trigger
1842
            $result = $this->call_trigger('PRODUCT_DEL_MULTILANGS', $user);
1843
            if ($result < 0) {
1844
                $this->error = $this->db->lasterror();
1845
                dol_syslog(get_class($this) . '::delMultiLangs error=' . $this->error, LOG_ERR);
1846
                return -1;
1847
            }
1848
            // End call triggers
1849
            return 1;
1850
        } else {
1851
            $this->error = $this->db->lasterror();
1852
            dol_syslog(get_class($this) . '::delMultiLangs error=' . $this->error, LOG_ERR);
1853
            return -1;
1854
        }
1855
    }
1856
1857
    /**
1858
     * Sets an accountancy code for a product.
1859
     * Also calls PRODUCT_MODIFY trigger when modified
1860
     *
1861
     * @param   string $type    It can be 'buy', 'buy_intra', 'buy_export', 'sell', 'sell_intra' or 'sell_export'
1862
     * @param   string $value   Accountancy code
1863
     * @return  int             Return integer <0 KO >0 OK
1864
     */
1865
    public function setAccountancyCode($type, $value)
1866
    {
1867
        global $user, $langs, $conf;
1868
1869
        $error = 0;
1870
1871
        $this->db->begin();
1872
1873
        if ($type == 'buy') {
1874
            $field = 'accountancy_code_buy';
1875
        } elseif ($type == 'buy_intra') {
1876
            $field = 'accountancy_code_buy_intra';
1877
        } elseif ($type == 'buy_export') {
1878
            $field = 'accountancy_code_buy_export';
1879
        } elseif ($type == 'sell') {
1880
            $field = 'accountancy_code_sell';
1881
        } elseif ($type == 'sell_intra') {
1882
            $field = 'accountancy_code_sell_intra';
1883
        } elseif ($type == 'sell_export') {
1884
            $field = 'accountancy_code_sell_export';
1885
        } else {
1886
            return -1;
1887
        }
1888
1889
        $sql = "UPDATE " . $this->db->prefix() . $this->table_element . " SET ";
1890
        $sql .= "$field = '" . $this->db->escape($value) . "'";
1891
        $sql .= " WHERE rowid = " . ((int) $this->id);
1892
1893
        dol_syslog(__METHOD__, LOG_DEBUG);
1894
        $resql = $this->db->query($sql);
1895
1896
        if ($resql) {
1897
            // Call trigger
1898
            $result = $this->call_trigger('PRODUCT_MODIFY', $user);
1899
            if ($result < 0) {
1900
                $error++;
1901
            }
1902
            // End call triggers
1903
1904
            if ($error) {
1905
                $this->db->rollback();
1906
                return -1;
1907
            }
1908
1909
            $this->$field = $value;
1910
1911
            $this->db->commit();
1912
            return 1;
1913
        } else {
1914
            $this->error = $this->db->lasterror();
1915
            $this->db->rollback();
1916
            return -1;
1917
        }
1918
    }
1919
1920
    /**
1921
     *    Load array this->multilangs
1922
     *
1923
     * @return int        Return integer <0 if KO, >0 if OK
1924
     */
1925
    public function getMultiLangs()
1926
    {
1927
        global $langs;
1928
1929
        $current_lang = $langs->getDefaultLang();
1930
1931
        $sql = "SELECT lang, label, description, note as other";
1932
        $sql .= " FROM " . $this->db->prefix() . "product_lang";
1933
        $sql .= " WHERE fk_product = " . ((int) $this->id);
1934
1935
        $result = $this->db->query($sql);
1936
        if ($result) {
1937
            while ($obj = $this->db->fetch_object($result)) {
1938
                //print 'lang='.$obj->lang.' current='.$current_lang.'<br>';
1939
                if ($obj->lang == $current_lang) {  // si on a les traduct. dans la langue courante on les charge en infos principales.
1940
                    $this->label       = $obj->label;
1941
                    $this->description = $obj->description;
1942
                    $this->other       = $obj->other;
1943
                }
1944
                $this->multilangs[(string) $obj->lang]["label"]       = $obj->label;
1945
                $this->multilangs[(string) $obj->lang]["description"] = $obj->description;
1946
                $this->multilangs[(string) $obj->lang]["other"]       = $obj->other;
1947
            }
1948
            return 1;
1949
        } else {
1950
            $this->error = "Error: " . $this->db->lasterror() . " - " . $sql;
1951
            return -1;
1952
        }
1953
    }
1954
1955
    /**
1956
     *  used to check if price have really change to avoid log pollution
1957
     *
1958
     * @param  int  $level price level to change
1959
     * @return array
1960
     */
1961
    private function getArrayForPriceCompare($level = 0)
1962
    {
1963
        $testExit = array('multiprices','multiprices_ttc','multiprices_base_type','multiprices_min','multiprices_min_ttc','multiprices_tva_tx','multiprices_recuperableonly');
1964
1965
        foreach ($testExit as $field) {
1966
            if (!isset($this->$field)) {
1967
                return array();
1968
            }
1969
            $tmparray = $this->$field;
1970
            if (!isset($tmparray[$level])) {
1971
                return array();
1972
            }
1973
        }
1974
1975
        $lastPrice = array(
1976
            'level' => $level ? $level : 1,
1977
            'multiprices' => (float) $this->multiprices[$level],
1978
            'multiprices_ttc' => (float) $this->multiprices_ttc[$level],
1979
            'multiprices_base_type' => $this->multiprices_base_type[$level],
1980
            'multiprices_min' => (float) $this->multiprices_min[$level],
1981
            'multiprices_min_ttc' => (float) $this->multiprices_min_ttc[$level],
1982
            'multiprices_tva_tx' => (float) $this->multiprices_tva_tx[$level],
1983
            'multiprices_recuperableonly' => (float) $this->multiprices_recuperableonly[$level],
1984
        );
1985
1986
        return $lastPrice;
1987
    }
1988
1989
1990
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1991
    /**
1992
     *  Insert a track that we changed a customer price
1993
     *
1994
     * @param  User $user  User making change
1995
     * @param  int  $level price level to change
1996
     * @return int                    Return integer <0 if KO, >0 if OK
1997
     */
1998
    private function _log_price($user, $level = 0)
1999
    {
2000
		// phpcs:enable
2001
        global $conf;
2002
2003
        $now = dol_now();
2004
2005
        // Clean parameters
2006
        if (empty($this->price_by_qty)) {
2007
            $this->price_by_qty = 0;
2008
        }
2009
2010
        // Add new price
2011
        $sql = "INSERT INTO " . $this->db->prefix() . "product_price(price_level,date_price, fk_product, fk_user_author, price_label, price, price_ttc, price_base_type,tosell, tva_tx, default_vat_code, recuperableonly,";
2012
        $sql .= " localtax1_tx, localtax2_tx, localtax1_type, localtax2_type, price_min,price_min_ttc,price_by_qty,entity,fk_price_expression) ";
2013
        $sql .= " VALUES(" . ($level ? ((int) $level) : 1) . ", '" . $this->db->idate($now) . "', " . ((int) $this->id) . ", " . ((int) $user->id) . ", " . (empty($this->price_label) ? "null" : "'" . $this->db->escape($this->price_label) . "'") . ", " . ((float) price2num($this->price)) . ", " . ((float) price2num($this->price_ttc)) . ",'" . $this->db->escape($this->price_base_type) . "'," . ((int) $this->status) . ", " . ((float) price2num($this->tva_tx)) . ", " . ($this->default_vat_code ? ("'" . $this->db->escape($this->default_vat_code) . "'") : "null") . ", " . ((int) $this->tva_npr) . ",";
2014
        $sql .= " " . price2num($this->localtax1_tx) . ", " . price2num($this->localtax2_tx) . ", '" . $this->db->escape($this->localtax1_type) . "', '" . $this->db->escape($this->localtax2_type) . "', " . price2num($this->price_min) . ", " . price2num($this->price_min_ttc) . ", " . price2num($this->price_by_qty) . ", " . ((int) $conf->entity) . "," . ($this->fk_price_expression > 0 ? ((int) $this->fk_price_expression) : 'null');
2015
        $sql .= ")";
2016
2017
        dol_syslog(get_class($this) . "::_log_price", LOG_DEBUG);
2018
        $resql = $this->db->query($sql);
2019
        if (!$resql) {
2020
            $this->error = $this->db->lasterror();
2021
            dol_print_error($this->db);
2022
            return -1;
2023
        } else {
2024
            return 1;
2025
        }
2026
    }
2027
2028
2029
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2030
    /**
2031
     *  Delete a price line
2032
     *
2033
     * @param  User $user  Object user
2034
     * @param  int  $rowid Line id to delete
2035
     * @return int                Return integer <0 if KO, >0 if OK
2036
     */
2037
    public function log_price_delete($user, $rowid)
2038
    {
2039
		// phpcs:enable
2040
        $sql = "DELETE FROM " . $this->db->prefix() . "product_price_by_qty";
2041
        $sql .= " WHERE fk_product_price = " . ((int) $rowid);
2042
        $resql = $this->db->query($sql);
2043
2044
        $sql = "DELETE FROM " . $this->db->prefix() . "product_price";
2045
        $sql .= " WHERE rowid=" . ((int) $rowid);
2046
        $resql = $this->db->query($sql);
2047
        if ($resql) {
2048
            return 1;
2049
        } else {
2050
            $this->error = $this->db->lasterror();
2051
            return -1;
2052
        }
2053
    }
2054
2055
2056
    /**
2057
     * Return price of sell of a product for a seller/buyer/product.
2058
     *
2059
     * @param   Societe     $thirdparty_seller      Seller
2060
     * @param   Societe     $thirdparty_buyer       Buyer
0 ignored issues
show
Bug introduced by
The type Dolibarr\Code\Product\Classes\Societe was not found. Did you mean Societe? If so, make sure to prefix the type with \.
Loading history...
2061
     * @param   int         $pqp                    Id of product price per quantity if a selection was done of such a price
2062
     * @return  array                               Array of price information array('pu_ht'=> , 'pu_ttc'=> , 'tva_tx'=>'X.Y (code)', ...), 'tva_npr'=>0, ...)
2063
     * @see get_buyprice(), find_min_price_product_fournisseur()
2064
     */
2065
    public function getSellPrice($thirdparty_seller, $thirdparty_buyer, $pqp = 0)
2066
    {
2067
        global $conf, $hookmanager, $action;
2068
2069
        // Call hook if any
2070
        if (is_object($hookmanager)) {
2071
            $parameters = array('thirdparty_seller' => $thirdparty_seller, 'thirdparty_buyer' => $thirdparty_buyer, 'pqp' => $pqp);
2072
            // Note that $action and $object may have been modified by some hooks
2073
            $reshook = $hookmanager->executeHooks('getSellPrice', $parameters, $this, $action);
2074
            if ($reshook > 0) {
2075
                return $hookmanager->resArray;
2076
            }
2077
        }
2078
2079
        // Update if prices fields are defined
2080
        $tva_tx = get_default_tva($thirdparty_seller, $thirdparty_buyer, $this->id);
2081
        $tva_npr = get_default_npr($thirdparty_seller, $thirdparty_buyer, $this->id);
2082
        if (empty($tva_tx)) {
2083
            $tva_npr = 0;
2084
        }
2085
2086
        $pu_ht = $this->price;
2087
        $pu_ttc = $this->price_ttc;
2088
        $price_min = $this->price_min;
2089
        $price_base_type = $this->price_base_type;
2090
2091
        // If price per segment
2092
        if (getDolGlobalString('PRODUIT_MULTIPRICES') && !empty($thirdparty_buyer->price_level)) {
2093
            $pu_ht = $this->multiprices[$thirdparty_buyer->price_level];
2094
            $pu_ttc = $this->multiprices_ttc[$thirdparty_buyer->price_level];
2095
            $price_min = $this->multiprices_min[$thirdparty_buyer->price_level];
2096
            $price_base_type = $this->multiprices_base_type[$thirdparty_buyer->price_level];
2097
            if (getDolGlobalString('PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL')) {  // using this option is a bug. kept for backward compatibility
2098
                if (isset($this->multiprices_tva_tx[$thirdparty_buyer->price_level])) {
2099
                    $tva_tx = $this->multiprices_tva_tx[$thirdparty_buyer->price_level];
2100
                }
2101
                if (isset($this->multiprices_recuperableonly[$thirdparty_buyer->price_level])) {
2102
                    $tva_npr = $this->multiprices_recuperableonly[$thirdparty_buyer->price_level];
2103
                }
2104
                if (empty($tva_tx)) {
2105
                    $tva_npr = 0;
2106
                }
2107
            }
2108
        } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES')) {
2109
            // If price per customer
2110
            require_once constant('DOL_DOCUMENT_ROOT') . '/product/class/productcustomerprice.class.php';
2111
2112
            $prodcustprice = new ProductCustomerPrice($this->db);
2113
2114
            $filter = array('t.fk_product' => $this->id, 't.fk_soc' => $thirdparty_buyer->id);
2115
2116
            $result = $prodcustprice->fetchAll('', '', 0, 0, $filter);
2117
            if ($result) {
2118
                if (count($prodcustprice->lines) > 0) {
2119
                    $pu_ht = price($prodcustprice->lines[0]->price);
2120
                    $price_min = price($prodcustprice->lines[0]->price_min);
2121
                    $pu_ttc = price($prodcustprice->lines[0]->price_ttc);
2122
                    $price_base_type = $prodcustprice->lines[0]->price_base_type;
2123
                    $tva_tx = $prodcustprice->lines[0]->tva_tx;
2124
                    if ($prodcustprice->lines[0]->default_vat_code && !preg_match('/\(.*\)/', $tva_tx)) {
2125
                        $tva_tx .= ' (' . $prodcustprice->lines[0]->default_vat_code . ')';
2126
                    }
2127
                    $tva_npr = $prodcustprice->lines[0]->recuperableonly;
2128
                    if (empty($tva_tx)) {
2129
                        $tva_npr = 0;
2130
                    }
2131
                }
2132
            }
2133
        } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY')) {
2134
            // If price per quantity
2135
            if ($this->prices_by_qty[0]) {
2136
                // yes, this product has some prices per quantity
2137
                // Search price into product_price_by_qty from $this->id
2138
                foreach ($this->prices_by_qty_list[0] as $priceforthequantityarray) {
2139
                    if ($priceforthequantityarray['rowid'] != $pqp) {
2140
                        continue;
2141
                    }
2142
                    // We found the price
2143
                    if ($priceforthequantityarray['price_base_type'] == 'HT') {
2144
                        $pu_ht = $priceforthequantityarray['unitprice'];
2145
                    } else {
2146
                        $pu_ttc = $priceforthequantityarray['unitprice'];
2147
                    }
2148
                    break;
2149
                }
2150
            }
2151
        } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES')) {
2152
            // If price per quantity and customer
2153
            if ($this->prices_by_qty[$thirdparty_buyer->price_level]) {
2154
                // yes, this product has some prices per quantity
2155
                // Search price into product_price_by_qty from $this->id
2156
                foreach ($this->prices_by_qty_list[$thirdparty_buyer->price_level] as $priceforthequantityarray) {
2157
                    if ($priceforthequantityarray['rowid'] != $pqp) {
2158
                        continue;
2159
                    }
2160
                    // We found the price
2161
                    if ($priceforthequantityarray['price_base_type'] == 'HT') {
2162
                        $pu_ht = $priceforthequantityarray['unitprice'];
2163
                    } else {
2164
                        $pu_ttc = $priceforthequantityarray['unitprice'];
2165
                    }
2166
                    break;
2167
                }
2168
            }
2169
        }
2170
2171
        return array('pu_ht' => $pu_ht, 'pu_ttc' => $pu_ttc, 'price_min' => $price_min, 'price_base_type' => $price_base_type, 'tva_tx' => $tva_tx, 'tva_npr' => $tva_npr);
2172
    }
2173
2174
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2175
    /**
2176
     * Read price used by a provider.
2177
     * We enter as input couple prodfournprice/qty or triplet qty/product_id/fourn_ref.
2178
     * This also set some properties on product like ->buyprice, ->fourn_pu, ...
2179
     *
2180
     * @param  int    $prodfournprice Id du tarif = rowid table product_fournisseur_price
2181
     * @param  double $qty            Quantity asked or -1 to get first entry found
2182
     * @param  int    $product_id     Filter on a particular product id
2183
     * @param  string $fourn_ref      Filter on a supplier price ref. 'none' to exclude ref in search.
2184
     * @param  int    $fk_soc         If of supplier
2185
     * @return int|string             Return integer <-1 if KO, -1 if qty not enough, 0 if OK but nothing found, id_product if OK and found. May also initialize some properties like (->ref_supplier, buyprice, fourn_pu, vatrate_supplier...), or printable result of hook
2186
     * @see getSellPrice(), find_min_price_product_fournisseur()
2187
     */
2188
    public function get_buyprice($prodfournprice, $qty, $product_id = 0, $fourn_ref = '', $fk_soc = 0)
2189
    {
2190
		// phpcs:enable
2191
        global $action, $hookmanager;
2192
2193
        // Call hook if any
2194
        if (is_object($hookmanager)) {
2195
            $parameters = array(
2196
                'prodfournprice' => $prodfournprice,
2197
                'qty' => $qty,
2198
                'product_id' => $product_id,
2199
                'fourn_ref' => $fourn_ref,
2200
                'fk_soc' => $fk_soc,
2201
            );
2202
            // Note that $action and $object may have been modified by some hooks
2203
            $reshook = $hookmanager->executeHooks('getBuyPrice', $parameters, $this, $action);
2204
            if ($reshook > 0) {
2205
                return $hookmanager->resArray;
2206
            }
2207
        }
2208
2209
        $result = 0;
2210
2211
        // We do a first search with a select by searching with couple prodfournprice and qty only (later we will search on triplet qty/product_id/fourn_ref)
2212
        $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2213
        $sql .= " pfp.fk_product, pfp.ref_fourn as ref_supplier, pfp.desc_fourn as desc_supplier, pfp.tva_tx, pfp.default_vat_code, pfp.fk_supplier_price_expression,";
2214
        $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2215
        $sql .= " pfp.packaging";
2216
        $sql .= " FROM " . $this->db->prefix() . "product_fournisseur_price as pfp";
2217
        $sql .= " WHERE pfp.rowid = " . ((int) $prodfournprice);
2218
        if ($qty > 0) {
2219
            $sql .= " AND pfp.quantity <= " . ((float) $qty);
2220
        }
2221
        $sql .= " ORDER BY pfp.quantity DESC";
2222
2223
        dol_syslog(get_class($this) . "::get_buyprice first search by prodfournprice/qty", LOG_DEBUG);
2224
        $resql = $this->db->query($sql);
2225
        if ($resql) {
2226
            $obj = $this->db->fetch_object($resql);
2227
            if ($obj && $obj->quantity > 0) {        // If we found a supplier prices from the id of supplier price
2228
                if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2229
                    $prod_supplier = new ProductFournisseur($this->db);
2230
                    $prod_supplier->product_fourn_price_id = $obj->rowid;
2231
                    $prod_supplier->id = $obj->fk_product;
2232
                    $prod_supplier->fourn_qty = $obj->quantity;
2233
                    $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2234
                    $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2235
2236
                    include_once DOL_DOCUMENT_ROOT . '/product/dynamic_price/class/price_parser.class.php';
2237
                    $priceparser = new PriceParser($this->db);
2238
                    $price_result = $priceparser->parseProductSupplier($prod_supplier);
2239
                    if ($price_result >= 0) {
2240
                        $obj->price = $price_result;
2241
                    }
2242
                }
2243
                $this->product_fourn_price_id = $obj->rowid;
2244
                $this->buyprice = $obj->price; // deprecated
2245
                $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product of supplier
2246
                $this->fourn_price_base_type = 'HT'; // Price base type
2247
                $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2248
                $this->ref_fourn = $obj->ref_supplier; // deprecated
2249
                $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2250
                $this->desc_supplier = $obj->desc_supplier; // desc supplier
2251
                $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2252
                $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2253
                $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2254
                $this->fourn_multicurrency_price = $obj->multicurrency_price;
2255
                $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2256
                $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2257
                $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2258
                $this->fourn_multicurrency_code = $obj->multicurrency_code;
2259
                if (getDolGlobalString('PRODUCT_USE_SUPPLIER_PACKAGING')) {
2260
                    $this->packaging = $obj->packaging;
2261
                }
2262
                $result = $obj->fk_product;
2263
                return $result;
2264
            } else { // If not found
2265
                // We do a second search by doing a select again but searching with less reliable criteria: couple qty/id product, and if set fourn_ref or fk_soc.
2266
                $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2267
                $sql .= " pfp.fk_product, pfp.ref_fourn as ref_supplier, pfp.desc_fourn as desc_supplier, pfp.tva_tx, pfp.default_vat_code, pfp.fk_supplier_price_expression,";
2268
                $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2269
                $sql .= " pfp.packaging";
2270
                $sql .= " FROM " . $this->db->prefix() . "product_fournisseur_price as pfp";
2271
                $sql .= " WHERE 1 = 1";
2272
                if ($product_id > 0) {
2273
                    $sql .= " AND pfp.fk_product = " . ((int) $product_id);
2274
                }
2275
                if ($fourn_ref != 'none') {
2276
                    $sql .= " AND pfp.ref_fourn = '" . $this->db->escape($fourn_ref) . "'";
2277
                }
2278
                if ($fk_soc > 0) {
2279
                    $sql .= " AND pfp.fk_soc = " . ((int) $fk_soc);
2280
                }
2281
                if ($qty > 0) {
2282
                    $sql .= " AND pfp.quantity <= " . ((float) $qty);
2283
                }
2284
                $sql .= " ORDER BY pfp.quantity DESC";
2285
                $sql .= " LIMIT 1";
2286
2287
                dol_syslog(get_class($this) . "::get_buyprice second search from qty/ref/product_id", LOG_DEBUG);
2288
                $resql = $this->db->query($sql);
2289
                if ($resql) {
2290
                    $obj = $this->db->fetch_object($resql);
2291
                    if ($obj && $obj->quantity > 0) {        // If found
2292
                        if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2293
                            $prod_supplier = new ProductFournisseur($this->db);
2294
                            $prod_supplier->product_fourn_price_id = $obj->rowid;
2295
                            $prod_supplier->id = $obj->fk_product;
2296
                            $prod_supplier->fourn_qty = $obj->quantity;
2297
                            $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2298
                            $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2299
2300
                            include_once DOL_DOCUMENT_ROOT . '/product/dynamic_price/class/price_parser.class.php';
2301
                            $priceparser = new PriceParser($this->db);
2302
                            $price_result = $priceparser->parseProductSupplier($prod_supplier);
2303
                            if ($result >= 0) {
2304
                                $obj->price = $price_result;
2305
                            }
2306
                        }
2307
                        $this->product_fourn_price_id = $obj->rowid;
2308
                        $this->buyprice = $obj->price; // deprecated
2309
                        $this->fourn_qty = $obj->quantity; // min quantity for price for a virtual supplier
2310
                        $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product for a virtual supplier
2311
                        $this->fourn_price_base_type = 'HT'; // Price base type for a virtual supplier
2312
                        $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2313
                        $this->ref_fourn = $obj->ref_supplier; // deprecated
2314
                        $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2315
                        $this->desc_supplier = $obj->desc_supplier; // desc supplier
2316
                        $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2317
                        $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2318
                        $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2319
                        $this->fourn_multicurrency_price = $obj->multicurrency_price;
2320
                        $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2321
                        $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2322
                        $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2323
                        $this->fourn_multicurrency_code = $obj->multicurrency_code;
2324
                        if (getDolGlobalString('PRODUCT_USE_SUPPLIER_PACKAGING')) {
2325
                            $this->packaging = $obj->packaging;
2326
                        }
2327
                        $result = $obj->fk_product;
2328
                        return $result;
2329
                    } else {
2330
                        return -1; // Ce produit n'existe pas avec cet id tarif fournisseur ou existe mais qte insuffisante, ni pour le couple produit/ref fournisseur dans la quantité.
2331
                    }
2332
                } else {
2333
                    $this->error = $this->db->lasterror();
2334
                    return -3;
2335
                }
2336
            }
2337
        } else {
2338
            $this->error = $this->db->lasterror();
2339
            return -2;
2340
        }
2341
    }
2342
2343
2344
    /**
2345
     * Modify customer price of a product/Service for a given level
2346
     *
2347
     * @param  double $newprice          New price
2348
     * @param  string $newpricebase      HT or TTC
2349
     * @param  User   $user              Object user that make change
2350
     * @param  ?double $newvat           New VAT Rate (For example 8.5. Should not be a string)
2351
     * @param  double $newminprice       New price min
2352
     * @param  int    $level             0=standard, >0 = level if multilevel prices
2353
     * @param  int    $newnpr            0=Standard vat rate, 1=Special vat rate for French NPR VAT
2354
     * @param  int    $newpbq            1 if it has price by quantity
2355
     * @param  int    $ignore_autogen    Used to avoid infinite loops
2356
     * @param  array  $localtaxes_array  Array with localtaxes info array('0'=>type1,'1'=>rate1,'2'=>type2,'3'=>rate2) (loaded by getLocalTaxesFromRate(vatrate, 0, ...) function).
2357
     * @param  string $newdefaultvatcode Default vat code
2358
     * @param  string $price_label       Price Label
2359
     * @param  int    $notrigger         Disable triggers
2360
     * @return int                            Return integer <0 if KO, >0 if OK
2361
     */
2362
    public function updatePrice($newprice, $newpricebase, $user, $newvat = null, $newminprice = 0, $level = 0, $newnpr = 0, $newpbq = 0, $ignore_autogen = 0, $localtaxes_array = array(), $newdefaultvatcode = '', $price_label = '', $notrigger = 0)
2363
    {
2364
        global $conf, $langs;
2365
2366
        $lastPriceData = $this->getArrayForPriceCompare($level); // temporary store current price before update
2367
2368
        $id = $this->id;
2369
2370
        dol_syslog(get_class($this) . "::update_price id=" . $id . " newprice=" . $newprice . " newpricebase=" . $newpricebase . " newminprice=" . $newminprice . " level=" . $level . " npr=" . $newnpr . " newdefaultvatcode=" . $newdefaultvatcode);
2371
2372
        // Clean parameters
2373
        if (empty($this->tva_tx)) {
2374
            $this->tva_tx = 0;
2375
        }
2376
        if (empty($newnpr)) {
2377
            $newnpr = 0;
2378
        }
2379
        if (empty($newminprice)) {
2380
            $newminprice = 0;
2381
        }
2382
2383
        // Check parameters
2384
        if ($newvat === null || $newvat == '') {  // Maintain '' for backwards compatibility
2385
            $newvat = $this->tva_tx;
2386
        }
2387
2388
        // If multiprices are enabled, then we check if the current product is subject to price autogeneration
2389
        // Price will be modified ONLY when the first one is the one that is being modified
2390
        if ((getDolGlobalString('PRODUIT_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES')) && !$ignore_autogen && $this->price_autogen && ($level == 1)) {
2391
            return $this->generateMultiprices($user, $newprice, $newpricebase, $newvat, $newnpr, $newpbq);
2392
        }
2393
2394
        if (!empty($newminprice) && ($newminprice > $newprice)) {
2395
            $this->error = 'ErrorPriceCantBeLowerThanMinPrice';
2396
            return -1;
2397
        }
2398
2399
        if ($newprice !== '' || $newprice === 0) {
2400
            if ($newpricebase == 'TTC') {
2401
                $price_ttc = price2num($newprice, 'MU');
2402
                $price = (float) price2num($newprice) / (1 + ((float) $newvat / 100));
2403
                $price = price2num($price, 'MU');
2404
2405
                if ($newminprice != '' || $newminprice == 0) {
2406
                    $price_min_ttc = price2num($newminprice, 'MU');
2407
                    $price_min = (float) price2num($newminprice) / (1 + ($newvat / 100));
2408
                    $price_min = price2num($price_min, 'MU');
2409
                } else {
2410
                    $price_min = 0;
2411
                    $price_min_ttc = 0;
2412
                }
2413
            } else {
2414
                $price = (float) price2num($newprice, 'MU');
2415
                $price_ttc = ($newnpr != 1) ? price2num($newprice) * (1 + ($newvat / 100)) : $price;
2416
                $price_ttc = (float) price2num($price_ttc, 'MU');
2417
2418
                if ($newminprice !== '' || $newminprice === 0) {
2419
                    $price_min = price2num($newminprice, 'MU');
2420
                    $price_min_ttc = (float) price2num($newminprice) * (1 + ($newvat / 100));
2421
                    $price_min_ttc = price2num($price_min_ttc, 'MU');
2422
                    //print 'X'.$newminprice.'-'.$price_min;
2423
                } else {
2424
                    $price_min = 0;
2425
                    $price_min_ttc = 0;
2426
                }
2427
            }
2428
            //print 'x'.$id.'-'.$newprice.'-'.$newpricebase.'-'.$price.'-'.$price_ttc.'-'.$price_min.'-'.$price_min_ttc;
2429
2430
            if (count($localtaxes_array) > 0) {
2431
                $localtaxtype1 = $localtaxes_array['0'];
2432
                $localtax1 = $localtaxes_array['1'];
2433
                $localtaxtype2 = $localtaxes_array['2'];
2434
                $localtax2 = $localtaxes_array['3'];
2435
            } else {
2436
                // if array empty, we try to use the vat code
2437
                if (!empty($newdefaultvatcode)) {
2438
                    global $mysoc;
2439
                    // Get record from code
2440
                    $sql = "SELECT t.rowid, t.code, t.recuperableonly as tva_npr, t.localtax1, t.localtax2, t.localtax1_type, t.localtax2_type";
2441
                    $sql .= " FROM " . MAIN_DB_PREFIX . "c_tva as t, " . MAIN_DB_PREFIX . "c_country as c";
2442
                    $sql .= " WHERE t.fk_pays = c.rowid AND c.code = '" . $this->db->escape($mysoc->country_code) . "'";
2443
                    $sql .= " AND t.taux = " . ((float) $newdefaultvatcode) . " AND t.active = 1";
2444
                    $sql .= " AND t.code = '" . $this->db->escape($newdefaultvatcode) . "'";
2445
                    $resql = $this->db->query($sql);
2446
                    if ($resql) {
2447
                        $obj = $this->db->fetch_object($resql);
2448
                        if ($obj) {
2449
                            $npr = $obj->tva_npr;
2450
                            $localtax1 = $obj->localtax1;
2451
                            $localtax2 = $obj->localtax2;
2452
                            $localtaxtype1 = $obj->localtax1_type;
2453
                            $localtaxtype2 = $obj->localtax2_type;
2454
                        }
2455
                    }
2456
                } else {
2457
                    // old method. deprecated because we can't retrieve type
2458
                    $localtaxtype1 = '0';
2459
                    $localtax1 = get_localtax($newvat, 1);
2460
                    $localtaxtype2 = '0';
2461
                    $localtax2 = get_localtax($newvat, 2);
2462
                }
2463
            }
2464
            if (empty($localtax1)) {
2465
                $localtax1 = 0; // If = '' then = 0
2466
            }
2467
            if (empty($localtax2)) {
2468
                $localtax2 = 0; // If = '' then = 0
2469
            }
2470
2471
            $this->db->begin();
2472
2473
            // Ne pas mettre de quote sur les numeriques decimaux.
2474
            // Ceci provoque des stockages avec arrondis en base au lieu des valeurs exactes.
2475
            $sql = "UPDATE " . $this->db->prefix() . "product SET";
2476
            $sql .= " price_base_type = '" . $this->db->escape($newpricebase) . "',";
2477
            $sql .= " price = " . (float) $price . ",";
2478
            $sql .= " price_ttc = " . (float) $price_ttc . ",";
2479
            $sql .= " price_min = " . (float) $price_min . ",";
2480
            $sql .= " price_min_ttc = " . (float) $price_min_ttc . ",";
2481
            $sql .= " localtax1_tx = " . ($localtax1 >= 0 ? (float) $localtax1 : 'NULL') . ",";
2482
            $sql .= " localtax2_tx = " . ($localtax2 >= 0 ? (float) $localtax2 : 'NULL') . ",";
2483
            $sql .= " localtax1_type = " . ($localtaxtype1 != '' ? "'" . $this->db->escape($localtaxtype1) . "'" : "'0'") . ",";
2484
            $sql .= " localtax2_type = " . ($localtaxtype2 != '' ? "'" . $this->db->escape($localtaxtype2) . "'" : "'0'") . ",";
2485
            $sql .= " default_vat_code = " . ($newdefaultvatcode ? "'" . $this->db->escape($newdefaultvatcode) . "'" : "null") . ",";
2486
            $sql .= " price_label = " . (!empty($price_label) ? "'" . $this->db->escape($price_label) . "'" : "null") . ",";
2487
            $sql .= " tva_tx = " . (float) price2num($newvat) . ",";
2488
            $sql .= " recuperableonly = '" . $this->db->escape($newnpr) . "'";
2489
            $sql .= " WHERE rowid = " . ((int) $id);
2490
2491
            dol_syslog(get_class($this) . "::update_price", LOG_DEBUG);
2492
            $resql = $this->db->query($sql);
2493
            if ($resql) {
2494
                $this->multiprices[$level] = $price;
2495
                $this->multiprices_ttc[$level] = $price_ttc;
2496
                $this->multiprices_min[$level] = $price_min;
2497
                $this->multiprices_min_ttc[$level] = $price_min_ttc;
2498
                $this->multiprices_base_type[$level] = $newpricebase;
2499
                $this->multiprices_default_vat_code[$level] = $newdefaultvatcode;
2500
                $this->multiprices_tva_tx[$level] = $newvat;
2501
                $this->multiprices_recuperableonly[$level] = $newnpr;
2502
2503
                $this->price = $price;
0 ignored issues
show
Documentation Bug introduced by
It seems like $price can also be of type string. However, the property $price is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2504
                $this->price_label = $price_label;
2505
                $this->price_ttc = $price_ttc;
0 ignored issues
show
Documentation Bug introduced by
It seems like $price_ttc can also be of type string. However, the property $price_ttc is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2506
                $this->price_min = $price_min;
0 ignored issues
show
Documentation Bug introduced by
It seems like $price_min can also be of type string. However, the property $price_min is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2507
                $this->price_min_ttc = $price_min_ttc;
0 ignored issues
show
Documentation Bug introduced by
It seems like $price_min_ttc can also be of type string. However, the property $price_min_ttc is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2508
                $this->price_base_type = $newpricebase;
2509
                $this->default_vat_code = $newdefaultvatcode;
2510
                $this->tva_tx = $newvat;
2511
                $this->tva_npr = $newnpr;
2512
2513
                //Local taxes
2514
                $this->localtax1_tx = $localtax1;
2515
                $this->localtax2_tx = $localtax2;
2516
                $this->localtax1_type = $localtaxtype1;
2517
                $this->localtax2_type = $localtaxtype2;
2518
2519
                // Price by quantity
2520
                $this->price_by_qty = $newpbq;
2521
2522
                // check if price have really change before log
2523
                $newPriceData = $this->getArrayForPriceCompare($level);
2524
                if (!empty(array_diff_assoc($newPriceData, $lastPriceData)) || !getDolGlobalString('PRODUIT_MULTIPRICES')) {
2525
                    $this->_log_price($user, $level); // Save price for level into table product_price
2526
                }
2527
2528
                $this->level = $level; // Store level of price edited for trigger
2529
2530
                // Call trigger
2531
                if (!$notrigger) {
2532
                    $result = $this->call_trigger('PRODUCT_PRICE_MODIFY', $user);
2533
                    if ($result < 0) {
2534
                        $this->db->rollback();
2535
                        return -1;
2536
                    }
2537
                }
2538
                // End call triggers
2539
2540
                $this->db->commit();
2541
            } else {
2542
                $this->db->rollback();
2543
                $this->error = $this->db->lasterror();
2544
                return -1;
2545
            }
2546
        }
2547
2548
        return 1;
2549
    }
2550
2551
    /**
2552
     *  Sets the supplier price expression
2553
     *
2554
     * @param      int $expression_id Expression
2555
     * @return     int                     Return integer <0 if KO, >0 if OK
2556
     * @deprecated Use Product::update instead
2557
     */
2558
    public function setPriceExpression($expression_id)
2559
    {
2560
        global $user;
2561
2562
        $this->fk_price_expression = $expression_id;
2563
2564
        return $this->update($this->id, $user);
2565
    }
2566
2567
    /**
2568
     *  Load a product in memory from database
2569
     *
2570
     * @param  int    $id                Id of product/service to load
2571
     * @param  string $ref               Ref of product/service to load
2572
     * @param  string $ref_ext           Ref ext of product/service to load
2573
     * @param  string $barcode           Barcode of product/service to load
2574
     * @param  int    $ignore_expression When module dynamicprices is on, ignores the math expression for calculating price and uses the db value instead
2575
     * @param  int    $ignore_price_load Load product without loading $this->multiprices... array (when we are sure we don't need them)
2576
     * @param  int    $ignore_lang_load  Load product without loading $this->multilangs language arrays (when we are sure we don't need them)
2577
     * @return int                       Return integer <0 if KO, 0 if not found, >0 if OK
2578
     */
2579
    public function fetch($id = 0, $ref = '', $ref_ext = '', $barcode = '', $ignore_expression = 0, $ignore_price_load = 0, $ignore_lang_load = 0)
2580
    {
2581
        include_once DOL_DOCUMENT_ROOT . '/core/lib/company.lib.php';
2582
2583
        global $langs, $conf;
2584
2585
        dol_syslog(get_class($this) . "::fetch id=" . $id . " ref=" . $ref . " ref_ext=" . $ref_ext);
2586
2587
        // Check parameters
2588
        if (!$id && !$ref && !$ref_ext && !$barcode) {
2589
            $this->error = 'ErrorWrongParameters';
2590
            dol_syslog(get_class($this) . "::fetch " . $this->error, LOG_ERR);
2591
            return -1;
2592
        }
2593
2594
        $sql = "SELECT p.rowid, p.ref, p.ref_ext, p.label, p.description, p.url, p.note_public, p.note as note_private, p.customcode, p.fk_country, p.fk_state, p.lifetime, p.qc_frequency, p.price, p.price_ttc,";
2595
        $sql .= " p.price_min, p.price_min_ttc, p.price_base_type, p.cost_price, p.default_vat_code, p.tva_tx, p.recuperableonly as tva_npr, p.localtax1_tx, p.localtax2_tx, p.localtax1_type, p.localtax2_type, p.tosell,";
2596
        $sql .= " p.tobuy, p.fk_product_type, p.duration, p.fk_default_warehouse, p.fk_default_workstation, p.seuil_stock_alerte, p.canvas, p.net_measure, p.net_measure_units, p.weight, p.weight_units,";
2597
        $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units, p.last_main_doc,";
2598
        $sql .= " p.surface, p.surface_units, p.volume, p.volume_units, p.barcode, p.fk_barcode_type, p.finished, p.fk_default_bom, p.mandatory_period,";
2599
        if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
2600
            $sql .= " p.accountancy_code_buy, p.accountancy_code_buy_intra, p.accountancy_code_buy_export, p.accountancy_code_sell, p.accountancy_code_sell_intra, p.accountancy_code_sell_export,";
2601
        } else {
2602
            $sql .= " ppe.accountancy_code_buy, ppe.accountancy_code_buy_intra, ppe.accountancy_code_buy_export, ppe.accountancy_code_sell, ppe.accountancy_code_sell_intra, ppe.accountancy_code_sell_export,";
2603
        }
2604
2605
        //For MultiCompany
2606
        //PMP per entity & Stocks Sharings stock_reel includes only stocks shared with this entity
2607
        $separatedEntityPMP = false;    // Set to true to get the AWP from table llx_product_perentity instead of field 'pmp' into llx_product.
2608
        $separatedStock = false;        // Set to true will count stock from subtable llx_product_stock. It is slower than using denormalized field 'stock', but it is required when using multientity and shared warehouses.
2609
        $visibleWarehousesEntities = $conf->entity;
2610
        if (getDolGlobalString('MULTICOMPANY_PRODUCT_SHARING_ENABLED')) {
2611
            if (getDolGlobalString('MULTICOMPANY_PMP_PER_ENTITY_ENABLED')) {
2612
                $checkPMPPerEntity = $this->db->query("SELECT pmp FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " . ((int) $id) . " AND entity = " . (int) $conf->entity);
2613
                if ($this->db->num_rows($checkPMPPerEntity) > 0) {
2614
                    $separatedEntityPMP = true;
2615
                }
2616
            }
2617
            global $mc;
2618
            $separatedStock = true;
2619
            if (isset($mc->sharings['stock']) && !empty($mc->sharings['stock'])) {
2620
                $visibleWarehousesEntities .= "," . implode(",", $mc->sharings['stock']);
2621
            }
2622
        }
2623
        if ($separatedEntityPMP) {
2624
            $sql .= " ppe.pmp,";
2625
        } else {
2626
            $sql .= " p.pmp,";
2627
        }
2628
        $sql .= " p.datec, p.tms, p.import_key, p.entity, p.desiredstock, p.tobatch, p.sell_or_eat_by_mandatory, p.batch_mask, p.fk_unit,";
2629
        $sql .= " p.fk_price_expression, p.price_autogen, p.model_pdf,";
2630
        $sql .= " p.price_label,";
2631
        if ($separatedStock) {
2632
            $sql .= " SUM(sp.reel) as stock";
2633
        } else {
2634
            $sql .= " p.stock";
2635
        }
2636
        $sql .= " FROM " . $this->db->prefix() . "product as p";
2637
        if (getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED') || $separatedEntityPMP) {
2638
            $sql .= " LEFT JOIN " . $this->db->prefix() . "product_perentity as ppe ON ppe.fk_product = p.rowid AND ppe.entity = " . ((int) $conf->entity);
2639
        }
2640
        if ($separatedStock) {
2641
            $sql .= " LEFT JOIN " . $this->db->prefix() . "product_stock as sp ON sp.fk_product = p.rowid AND sp.fk_entrepot IN (SELECT rowid FROM " . $this->db->prefix() . "entrepot WHERE entity IN (" . $this->db->sanitize($visibleWarehousesEntities) . "))";
2642
        }
2643
2644
        if ($id) {
2645
            $sql .= " WHERE p.rowid = " . ((int) $id);
2646
        } else {
2647
            $sql .= " WHERE p.entity IN (" . getEntity($this->element) . ")";
2648
            if ($ref) {
2649
                $sql .= " AND p.ref = '" . $this->db->escape($ref) . "'";
2650
            } elseif ($ref_ext) {
2651
                $sql .= " AND p.ref_ext = '" . $this->db->escape($ref_ext) . "'";
2652
            } elseif ($barcode) {
2653
                $sql .= " AND p.barcode = '" . $this->db->escape($barcode) . "'";
2654
            }
2655
        }
2656
        if ($separatedStock) {
2657
            $sql .= " GROUP BY p.rowid, p.ref, p.ref_ext, p.label, p.description, p.url, p.note_public, p.note, p.customcode, p.fk_country, p.fk_state, p.lifetime, p.qc_frequency, p.price, p.price_ttc,";
2658
            $sql .= " p.price_min, p.price_min_ttc, p.price_base_type, p.cost_price, p.default_vat_code, p.tva_tx, p.recuperableonly, p.localtax1_tx, p.localtax2_tx, p.localtax1_type, p.localtax2_type, p.tosell,";
2659
            $sql .= " p.tobuy, p.fk_product_type, p.duration, p.fk_default_warehouse, p.fk_default_workstation, p.seuil_stock_alerte, p.canvas, p.net_measure, p.net_measure_units, p.weight, p.weight_units,";
2660
            $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units,";
2661
            $sql .= " p.surface, p.surface_units, p.volume, p.volume_units, p.barcode, p.fk_barcode_type, p.finished, p.fk_default_bom, p.mandatory_period,";
2662
            if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
2663
                $sql .= " p.accountancy_code_buy, p.accountancy_code_buy_intra, p.accountancy_code_buy_export, p.accountancy_code_sell, p.accountancy_code_sell_intra, p.accountancy_code_sell_export,";
2664
            } else {
2665
                $sql .= " ppe.accountancy_code_buy, ppe.accountancy_code_buy_intra, ppe.accountancy_code_buy_export, ppe.accountancy_code_sell, ppe.accountancy_code_sell_intra, ppe.accountancy_code_sell_export,";
2666
            }
2667
            if ($separatedEntityPMP) {
2668
                $sql .= " ppe.pmp,";
2669
            } else {
2670
                $sql .= " p.pmp,";
2671
            }
2672
            $sql .= " p.datec, p.tms, p.import_key, p.entity, p.desiredstock, p.tobatch, p.sell_or_eat_by_mandatory, p.batch_mask, p.fk_unit,";
2673
            $sql .= " p.fk_price_expression, p.price_autogen, p.model_pdf";
2674
            $sql .= " ,p.price_label";
2675
            if (!$separatedStock) {
2676
                $sql .= ", p.stock";
2677
            }
2678
        }
2679
2680
        $resql = $this->db->query($sql);
2681
        if ($resql) {
2682
            unset($this->oldcopy);
2683
2684
            if ($this->db->num_rows($resql) > 0) {
2685
                $obj = $this->db->fetch_object($resql);
2686
2687
                $this->id = $obj->rowid;
2688
                $this->ref = $obj->ref;
2689
                $this->ref_ext = $obj->ref_ext;
2690
                $this->label = $obj->label;
2691
                $this->description = $obj->description;
2692
                $this->url = $obj->url;
2693
                $this->note_public = $obj->note_public;
2694
                $this->note_private = $obj->note_private;
2695
                $this->note = $obj->note_private; // deprecated
0 ignored issues
show
Deprecated Code introduced by
The property Dolibarr\Core\Base\CommonObject::$note has been deprecated: Use $note_private instead. ( Ignorable by Annotation )

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

2695
                /** @scrutinizer ignore-deprecated */ $this->note = $obj->note_private; // deprecated

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...
2696
2697
                $this->type = $obj->fk_product_type;
2698
                $this->price_label = $obj->price_label;
2699
                $this->status = $obj->tosell;
2700
                $this->status_buy = $obj->tobuy;
2701
                $this->status_batch = $obj->tobatch;
2702
                $this->sell_or_eat_by_mandatory = $obj->sell_or_eat_by_mandatory;
2703
                $this->batch_mask = $obj->batch_mask;
2704
2705
                $this->customcode = $obj->customcode;
2706
                $this->country_id = $obj->fk_country;
2707
                $this->country_code = getCountry($this->country_id, 2, $this->db);
0 ignored issues
show
Documentation Bug introduced by
It seems like getCountry($this->country_id, 2, $this->db) can also be of type array. However, the property $country_code is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2708
                $this->state_id = $obj->fk_state;
2709
                $this->lifetime = $obj->lifetime;
2710
                $this->qc_frequency = $obj->qc_frequency;
2711
                $this->price = $obj->price;
2712
                $this->price_ttc = $obj->price_ttc;
2713
                $this->price_min = $obj->price_min;
2714
                $this->price_min_ttc = $obj->price_min_ttc;
2715
                $this->price_base_type = $obj->price_base_type;
2716
                $this->cost_price = $obj->cost_price;
2717
                $this->default_vat_code = $obj->default_vat_code;
2718
                $this->tva_tx = $obj->tva_tx;
2719
                //! French VAT NPR
2720
                $this->tva_npr = $obj->tva_npr;
2721
                //! Local taxes
2722
                $this->localtax1_tx = $obj->localtax1_tx;
2723
                $this->localtax2_tx = $obj->localtax2_tx;
2724
                $this->localtax1_type = $obj->localtax1_type;
2725
                $this->localtax2_type = $obj->localtax2_type;
2726
2727
                $this->finished = $obj->finished;
2728
                $this->fk_default_bom = $obj->fk_default_bom;
2729
2730
                $this->duration = $obj->duration;
2731
                $this->duration_value = $obj->duration ? substr($obj->duration, 0, dol_strlen($obj->duration) - 1) : null;
2732
                $this->duration_unit = $obj->duration ? substr($obj->duration, -1) : null;
2733
                $this->canvas = $obj->canvas;
2734
                $this->net_measure = $obj->net_measure;
2735
                $this->net_measure_units = $obj->net_measure_units;
2736
                $this->weight = $obj->weight;
2737
                $this->weight_units = $obj->weight_units;
2738
                $this->length = $obj->length;
2739
                $this->length_units = $obj->length_units;
2740
                $this->width = $obj->width;
2741
                $this->width_units = $obj->width_units;
2742
                $this->height = $obj->height;
2743
                $this->height_units = $obj->height_units;
2744
2745
                $this->surface = $obj->surface;
2746
                $this->surface_units = $obj->surface_units;
2747
                $this->volume = $obj->volume;
2748
                $this->volume_units = $obj->volume_units;
2749
                $this->barcode = $obj->barcode;
2750
                $this->barcode_type = $obj->fk_barcode_type;
2751
2752
                $this->accountancy_code_buy = $obj->accountancy_code_buy;
2753
                $this->accountancy_code_buy_intra = $obj->accountancy_code_buy_intra;
2754
                $this->accountancy_code_buy_export = $obj->accountancy_code_buy_export;
2755
                $this->accountancy_code_sell = $obj->accountancy_code_sell;
2756
                $this->accountancy_code_sell_intra = $obj->accountancy_code_sell_intra;
2757
                $this->accountancy_code_sell_export = $obj->accountancy_code_sell_export;
2758
2759
                $this->fk_default_warehouse = $obj->fk_default_warehouse;
2760
                $this->fk_default_workstation = $obj->fk_default_workstation;
2761
                $this->seuil_stock_alerte = $obj->seuil_stock_alerte;
2762
                $this->desiredstock = $obj->desiredstock;
2763
                $this->stock_reel = $obj->stock;
2764
                $this->pmp = $obj->pmp;
2765
2766
                $this->date_creation = $obj->datec;
2767
                $this->date_modification = $obj->tms;
2768
                $this->import_key = $obj->import_key;
2769
                $this->entity = $obj->entity;
2770
2771
                $this->ref_ext = $obj->ref_ext;
2772
                $this->fk_price_expression = $obj->fk_price_expression;
2773
                $this->fk_unit = $obj->fk_unit;
2774
                $this->price_autogen = $obj->price_autogen;
2775
                $this->model_pdf = $obj->model_pdf;
2776
                $this->last_main_doc = $obj->last_main_doc;
2777
2778
                $this->mandatory_period = $obj->mandatory_period;
2779
2780
                $this->db->free($resql);
2781
2782
                // fetch optionals attributes and labels
2783
                $this->fetch_optionals();
2784
2785
                // Multilangs
2786
                if (getDolGlobalInt('MAIN_MULTILANGS') && empty($ignore_lang_load)) {
2787
                    $this->getMultiLangs();
2788
                }
2789
2790
                // Load multiprices array
2791
                if (getDolGlobalString('PRODUIT_MULTIPRICES') && empty($ignore_price_load)) {                // prices per segment
2792
                    $produit_multiprices_limit = getDolGlobalString('PRODUIT_MULTIPRICES_LIMIT');
2793
                    for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
2794
                        $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
2795
                        $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
2796
                        $sql .= " ,price_label";
2797
                        $sql .= " FROM " . $this->db->prefix() . "product_price";
2798
                        $sql .= " WHERE entity IN (" . getEntity('productprice') . ")";
2799
                        $sql .= " AND price_level=" . ((int) $i);
2800
                        $sql .= " AND fk_product = " . ((int) $this->id);
2801
                        $sql .= " ORDER BY date_price DESC, rowid DESC";    // Get the most recent line
2802
                        $sql .= " LIMIT 1";                                 // Only the first one
2803
                        $resql = $this->db->query($sql);
2804
                        if ($resql) {
2805
                            $result = $this->db->fetch_array($resql);
2806
2807
                            $this->multiprices[$i] = $result ? $result["price"] : null;
2808
                            $this->multiprices_ttc[$i] = $result ? $result["price_ttc"] : null;
2809
                            $this->multiprices_min[$i] =  $result ? $result["price_min"] : null;
2810
                            $this->multiprices_min_ttc[$i] = $result ? $result["price_min_ttc"] : null;
2811
                            $this->multiprices_base_type[$i] = $result ? $result["price_base_type"] : null;
2812
                            // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
2813
                            $this->multiprices_tva_tx[$i] = $result ? $result["tva_tx"] . ($result ? ' (' . $result['default_vat_code'] . ')' : '') : null;
2814
                            $this->multiprices_recuperableonly[$i] = $result ? $result["recuperableonly"] : null;
2815
2816
                            // Price by quantity
2817
                            /*
2818
                             $this->prices_by_qty[$i]=$result["price_by_qty"];
2819
                             $this->prices_by_qty_id[$i]=$result["rowid"];
2820
                             // Récuperation de la liste des prix selon qty si flag positionné
2821
                             if ($this->prices_by_qty[$i] == 1)
2822
                             {
2823
                             $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
2824
                             $sql.= " FROM ".$this->db->prefix()."product_price_by_qty";
2825
                             $sql.= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
2826
                             $sql.= " ORDER BY quantity ASC";
2827
2828
                             $resql = $this->db->query($sql);
2829
                             if ($resql)
2830
                             {
2831
                             $resultat=array();
2832
                             $ii=0;
2833
                             while ($result= $this->db->fetch_array($resql)) {
2834
                             $resultat[$ii]=array();
2835
                             $resultat[$ii]["rowid"]=$result["rowid"];
2836
                             $resultat[$ii]["price"]= $result["price"];
2837
                             $resultat[$ii]["unitprice"]= $result["unitprice"];
2838
                             $resultat[$ii]["quantity"]= $result["quantity"];
2839
                             $resultat[$ii]["remise_percent"]= $result["remise_percent"];
2840
                             $resultat[$ii]["remise"]= $result["remise"];                    // deprecated
2841
                             $resultat[$ii]["price_base_type"]= $result["price_base_type"];
2842
                             $ii++;
2843
                             }
2844
                             $this->prices_by_qty_list[$i]=$resultat;
2845
                             }
2846
                             else
2847
                             {
2848
                             dol_print_error($this->db);
2849
                             return -1;
2850
                             }
2851
                             }*/
2852
                        } else {
2853
                            $this->error = $this->db->lasterror;
2854
                            return -1;
2855
                        }
2856
                    }
2857
                } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES') && empty($ignore_price_load)) {            // prices per customers
2858
                    // Nothing loaded by default. List may be very long.
2859
                } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY') && empty($ignore_price_load)) {    // prices per quantity
2860
                    $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
2861
                    $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid";
2862
                    $sql .= " FROM " . $this->db->prefix() . "product_price";
2863
                    $sql .= " WHERE fk_product = " . ((int) $this->id);
2864
                    $sql .= " ORDER BY date_price DESC, rowid DESC";
2865
                    $sql .= " LIMIT 1";
2866
2867
                    $resql = $this->db->query($sql);
2868
                    if ($resql) {
2869
                        $result = $this->db->fetch_array($resql);
2870
2871
                        if ($result) {
2872
                            // Price by quantity
2873
                            $this->prices_by_qty[0] = $result["price_by_qty"];
2874
                            $this->prices_by_qty_id[0] = $result["rowid"];
2875
                            // Récuperation de la liste des prix selon qty si flag positionné
2876
                            if ($this->prices_by_qty[0] == 1) {
2877
                                $sql = "SELECT rowid,price, unitprice, quantity, remise_percent, remise, remise, price_base_type";
2878
                                $sql .= " FROM " . $this->db->prefix() . "product_price_by_qty";
2879
                                $sql .= " WHERE fk_product_price = " . ((int) $this->prices_by_qty_id[0]);
2880
                                $sql .= " ORDER BY quantity ASC";
2881
2882
                                $resql = $this->db->query($sql);
2883
                                if ($resql) {
2884
                                    $resultat = array();
2885
                                    $ii = 0;
2886
                                    while ($result = $this->db->fetch_array($resql)) {
2887
                                        $resultat[$ii] = array();
2888
                                        $resultat[$ii]["rowid"] = $result["rowid"];
2889
                                        $resultat[$ii]["price"] = $result["price"];
2890
                                        $resultat[$ii]["unitprice"] = $result["unitprice"];
2891
                                        $resultat[$ii]["quantity"] = $result["quantity"];
2892
                                        $resultat[$ii]["remise_percent"] = $result["remise_percent"];
2893
                                        //$resultat[$ii]["remise"]= $result["remise"];                    // deprecated
2894
                                        $resultat[$ii]["price_base_type"] = $result["price_base_type"];
2895
                                        $ii++;
2896
                                    }
2897
                                    $this->prices_by_qty_list[0] = $resultat;
2898
                                } else {
2899
                                    $this->error = $this->db->lasterror;
2900
                                    return -1;
2901
                                }
2902
                            }
2903
                        }
2904
                    } else {
2905
                        $this->error = $this->db->lasterror;
2906
                        return -1;
2907
                    }
2908
                } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES') && empty($ignore_price_load)) {    // prices per customer and quantity
2909
                    $produit_multiprices_limit = getDolGlobalString('PRODUIT_MULTIPRICES_LIMIT');
2910
                    for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
2911
                        $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
2912
                        $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
2913
                        $sql .= " FROM " . $this->db->prefix() . "product_price";
2914
                        $sql .= " WHERE entity IN (" . getEntity('productprice') . ")";
2915
                        $sql .= " AND price_level=" . ((int) $i);
2916
                        $sql .= " AND fk_product = " . ((int) $this->id);
2917
                        $sql .= " ORDER BY date_price DESC, rowid DESC";
2918
                        $sql .= " LIMIT 1";
2919
                        $resql = $this->db->query($sql);
2920
                        if (!$resql) {
2921
                            $this->error = $this->db->lasterror;
2922
                            return -1;
2923
                        } elseif ($result = $this->db->fetch_array($resql)) {
2924
                            $this->multiprices[$i] = (!empty($result["price"]) ? $result["price"] : 0);
2925
                            $this->multiprices_ttc[$i] = (!empty($result["price_ttc"]) ? $result["price_ttc"] : 0);
2926
                            $this->multiprices_min[$i] = (!empty($result["price_min"]) ? $result["price_min"] : 0);
2927
                            $this->multiprices_min_ttc[$i] = (!empty($result["price_min_ttc"]) ? $result["price_min_ttc"] : 0);
2928
                            $this->multiprices_base_type[$i] = (!empty($result["price_base_type"]) ? $result["price_base_type"] : '');
2929
                            // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
2930
                            $this->multiprices_tva_tx[$i] = (!empty($result["tva_tx"]) ? $result["tva_tx"] : 0); // TODO Add ' ('.$result['default_vat_code'].')'
2931
                            $this->multiprices_recuperableonly[$i] = (!empty($result["recuperableonly"]) ? $result["recuperableonly"] : 0);
2932
2933
                            // Price by quantity
2934
                            $this->prices_by_qty[$i] = (!empty($result["price_by_qty"]) ? $result["price_by_qty"] : 0);
2935
                            $this->prices_by_qty_id[$i] = (!empty($result["rowid"]) ? $result["rowid"] : 0);
2936
                            // Récuperation de la liste des prix selon qty si flag positionné
2937
                            if ($this->prices_by_qty[$i] == 1) {
2938
                                $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
2939
                                $sql .= " FROM " . $this->db->prefix() . "product_price_by_qty";
2940
                                $sql .= " WHERE fk_product_price = " . ((int) $this->prices_by_qty_id[$i]);
2941
                                $sql .= " ORDER BY quantity ASC";
2942
2943
                                $resql = $this->db->query($sql);
2944
                                if ($resql) {
2945
                                    $resultat = array();
2946
                                    $ii = 0;
2947
                                    while ($result = $this->db->fetch_array($resql)) {
2948
                                        $resultat[$ii] = array();
2949
                                        $resultat[$ii]["rowid"] = $result["rowid"];
2950
                                        $resultat[$ii]["price"] = $result["price"];
2951
                                        $resultat[$ii]["unitprice"] = $result["unitprice"];
2952
                                        $resultat[$ii]["quantity"] = $result["quantity"];
2953
                                        $resultat[$ii]["remise_percent"] = $result["remise_percent"];
2954
                                        $resultat[$ii]["remise"] = $result["remise"]; // deprecated
2955
                                        $resultat[$ii]["price_base_type"] = $result["price_base_type"];
2956
                                        $ii++;
2957
                                    }
2958
                                    $this->prices_by_qty_list[$i] = $resultat;
2959
                                } else {
2960
                                    $this->error = $this->db->lasterror;
2961
                                    return -1;
2962
                                }
2963
                            }
2964
                        }
2965
                    }
2966
                }
2967
2968
                if (isModEnabled('dynamicprices') && !empty($this->fk_price_expression) && empty($ignore_expression)) {
2969
                    include_once DOL_DOCUMENT_ROOT . '/product/dynamic_price/class/price_parser.class.php';
2970
                    $priceparser = new PriceParser($this->db);
2971
                    $price_result = $priceparser->parseProduct($this);
2972
                    if ($price_result >= 0) {
2973
                        $this->price = $price_result;
2974
                        // Calculate the VAT
2975
                        $this->price_ttc = (float) price2num($this->price) * (1 + ($this->tva_tx / 100));
2976
                        $this->price_ttc = (float) price2num($this->price_ttc, 'MU');
2977
                    }
2978
                }
2979
2980
                // We should not load stock during the fetch. If someone need stock of product, he must call load_stock after fetching product.
2981
                // Instead we just init the stock_warehouse array
2982
                $this->stock_warehouse = array();
2983
2984
                return 1;
2985
            } else {
2986
                return 0;
2987
            }
2988
        } else {
2989
            $this->error = $this->db->lasterror();
2990
            return -1;
2991
        }
2992
    }
2993
2994
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2995
    /**
2996
     *  Charge tableau des stats OF pour le produit/service
2997
     *
2998
     * @param  int $socid Id societe
2999
     * @return int                     Array of stats in $this->stats_mo, <0 if ko or >0 if ok
3000
     */
3001
    public function load_stats_mo($socid = 0)
3002
    {
3003
		// phpcs:enable
3004
        global $user, $hookmanager, $action;
3005
3006
        $error = 0;
3007
3008
        foreach (array('toconsume', 'consumed', 'toproduce', 'produced') as $role) {
3009
            $this->stats_mo['customers_' . $role] = 0;
3010
            $this->stats_mo['nb_' . $role] = 0;
3011
            $this->stats_mo['qty_' . $role] = 0;
3012
3013
            $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3014
            $sql .= " SUM(mp.qty) as qty";
3015
            $sql .= " FROM " . $this->db->prefix() . "mrp_mo as c";
3016
            $sql .= " INNER JOIN " . $this->db->prefix() . "mrp_production as mp ON mp.fk_mo=c.rowid";
3017
            if (!$user->hasRight('societe', 'client', 'voir')) {
3018
                $sql .= " INNER JOIN " . $this->db->prefix() . "societe_commerciaux as sc ON sc.fk_soc=c.fk_soc AND sc.fk_user = " . ((int) $user->id);
3019
            }
3020
            $sql .= " WHERE ";
3021
            $sql .= " c.entity IN (" . getEntity('mo') . ")";
3022
3023
            $sql .= " AND mp.fk_product = " . ((int) $this->id);
3024
            $sql .= " AND mp.role ='" . $this->db->escape($role) . "'";
3025
            if ($socid > 0) {
3026
                $sql .= " AND c.fk_soc = " . ((int) $socid);
3027
            }
3028
3029
            $result = $this->db->query($sql);
3030
            if ($result) {
3031
                $obj = $this->db->fetch_object($result);
3032
                $this->stats_mo['customers_' . $role] = $obj->nb_customers ? $obj->nb_customers : 0;
3033
                $this->stats_mo['nb_' . $role] = $obj->nb ? $obj->nb : 0;
3034
                $this->stats_mo['qty_' . $role] = $obj->qty ? price2num($obj->qty, 'MS') : 0;     // qty may be a float due to the SUM()
3035
            } else {
3036
                $this->error = $this->db->error();
3037
                $error++;
3038
            }
3039
        }
3040
3041
        if (!empty($error)) {
3042
            return -1;
3043
        }
3044
3045
        $parameters = array('socid' => $socid);
3046
        $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
3047
        if ($reshook > 0) {
3048
            $this->stats_mo = $hookmanager->resArray['stats_mo'];
3049
        }
3050
3051
        return 1;
3052
    }
3053
3054
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3055
    /**
3056
     *  Charge tableau des stats OF pour le produit/service
3057
     *
3058
     * @param  int $socid Id societe
3059
     * @return int        Array of stats in $this->stats_bom, <0 if ko or >0 if ok
3060
     */
3061
    public function load_stats_bom($socid = 0)
3062
    {
3063
		// phpcs:enable
3064
        global $user, $hookmanager, $action;
3065
3066
        $error = 0;
3067
3068
        $this->stats_bom['nb_toproduce'] = 0;
3069
        $this->stats_bom['nb_toconsume'] = 0;
3070
        $this->stats_bom['qty_toproduce'] = 0;
3071
        $this->stats_bom['qty_toconsume'] = 0;
3072
3073
        $sql = "SELECT COUNT(DISTINCT b.rowid) as nb_toproduce,";
3074
        $sql .= " SUM(b.qty) as qty_toproduce";
3075
        $sql .= " FROM " . $this->db->prefix() . "bom_bom as b";
3076
        $sql .= " INNER JOIN " . $this->db->prefix() . "bom_bomline as bl ON bl.fk_bom=b.rowid";
3077
        $sql .= " WHERE ";
3078
        $sql .= " b.entity IN (" . getEntity('bom') . ")";
3079
        $sql .= " AND b.fk_product =" . ((int) $this->id);
3080
        $sql .= " GROUP BY b.rowid";
3081
3082
        $result = $this->db->query($sql);
3083
        if ($result) {
3084
            $obj = $this->db->fetch_object($result);
3085
            $this->stats_bom['nb_toproduce'] = !empty($obj->nb_toproduce) ? $obj->nb_toproduce : 0;
3086
            $this->stats_bom['qty_toproduce'] = !empty($obj->qty_toproduce) ? price2num($obj->qty_toproduce) : 0;
3087
        } else {
3088
            $this->error = $this->db->error();
3089
            $error++;
3090
        }
3091
3092
        $sql = "SELECT COUNT(DISTINCT bl.rowid) as nb_toconsume,";
3093
        $sql .= " SUM(bl.qty) as qty_toconsume";
3094
        $sql .= " FROM " . $this->db->prefix() . "bom_bom as b";
3095
        $sql .= " INNER JOIN " . $this->db->prefix() . "bom_bomline as bl ON bl.fk_bom=b.rowid";
3096
        $sql .= " WHERE ";
3097
        $sql .= " b.entity IN (" . getEntity('bom') . ")";
3098
        $sql .= " AND bl.fk_product =" . ((int) $this->id);
3099
3100
        $result = $this->db->query($sql);
3101
        if ($result) {
3102
            $obj = $this->db->fetch_object($result);
3103
            $this->stats_bom['nb_toconsume'] = !empty($obj->nb_toconsume) ? $obj->nb_toconsume : 0;
3104
            $this->stats_bom['qty_toconsume'] = !empty($obj->qty_toconsume) ? price2num($obj->qty_toconsume) : 0;
3105
        } else {
3106
            $this->error = $this->db->error();
3107
            $error++;
3108
        }
3109
3110
        if (!empty($error)) {
3111
            return -1;
3112
        }
3113
3114
        $parameters = array('socid' => $socid);
3115
        $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
3116
        if ($reshook > 0) {
3117
            $this->stats_bom = $hookmanager->resArray['stats_bom'];
3118
        }
3119
3120
        return 1;
3121
    }
3122
3123
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3124
    /**
3125
     *  Charge tableau des stats propale pour le produit/service
3126
     *
3127
     * @param  int $socid Id societe
3128
     * @return int        Array of stats in $this->stats_propale, <0 if ko or >0 if ok
3129
     */
3130
    public function load_stats_propale($socid = 0)
3131
    {
3132
		// phpcs:enable
3133
        global $conf, $user, $hookmanager, $action;
3134
3135
        $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_customers, COUNT(DISTINCT p.rowid) as nb,";
3136
        $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
3137
        $sql .= " FROM " . $this->db->prefix() . "propaldet as pd";
3138
        $sql .= ", " . $this->db->prefix() . "propal as p";
3139
        $sql .= ", " . $this->db->prefix() . "societe as s";
3140
        if (!$user->hasRight('societe', 'client', 'voir')) {
3141
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
3142
        }
3143
        $sql .= " WHERE p.rowid = pd.fk_propal";
3144
        $sql .= " AND p.fk_soc = s.rowid";
3145
        $sql .= " AND p.entity IN (" . getEntity('propal') . ")";
3146
        $sql .= " AND pd.fk_product = " . ((int) $this->id);
3147
        if (!$user->hasRight('societe', 'client', 'voir')) {
3148
            $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int) $user->id);
3149
        }
3150
        //$sql.= " AND pr.fk_statut != 0";
3151
        if ($socid > 0) {
3152
            $sql .= " AND p.fk_soc = " . ((int) $socid);
3153
        }
3154
3155
        $result = $this->db->query($sql);
3156
        if ($result) {
3157
            $obj = $this->db->fetch_object($result);
3158
            $this->stats_propale['customers'] = $obj->nb_customers;
3159
            $this->stats_propale['nb'] = $obj->nb;
3160
            $this->stats_propale['rows'] = $obj->nb_rows;
3161
            $this->stats_propale['qty'] = $obj->qty ? $obj->qty : 0;
3162
3163
            // if it's a virtual product, maybe it is in proposal by extension
3164
            if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3165
                $TFather = $this->getFather();
3166
                if (is_array($TFather) && !empty($TFather)) {
3167
                    foreach ($TFather as &$fatherData) {
3168
                        $pFather = new Product($this->db);
3169
                        $pFather->id = $fatherData['id'];
3170
                        $qtyCoef = $fatherData['qty'];
3171
3172
                        if ($fatherData['incdec']) {
3173
                            $pFather->load_stats_propale($socid);
3174
3175
                            $this->stats_propale['customers'] += $pFather->stats_propale['customers'];
3176
                            $this->stats_propale['nb'] += $pFather->stats_propale['nb'];
3177
                            $this->stats_propale['rows'] += $pFather->stats_propale['rows'];
3178
                            $this->stats_propale['qty'] += $pFather->stats_propale['qty'] * $qtyCoef;
3179
                        }
3180
                    }
3181
                }
3182
            }
3183
3184
            $parameters = array('socid' => $socid);
3185
            $reshook = $hookmanager->executeHooks('loadStatsCustomerProposal', $parameters, $this, $action);
3186
            if ($reshook > 0) {
3187
                $this->stats_propale = $hookmanager->resArray['stats_propale'];
3188
            }
3189
3190
            return 1;
3191
        } else {
3192
            $this->error = $this->db->error();
3193
            return -1;
3194
        }
3195
    }
3196
3197
3198
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3199
    /**
3200
     *  Charge tableau des stats propale pour le produit/service
3201
     *
3202
     * @param  int $socid Id thirdparty
3203
     * @return int        Array of stats in $this->stats_proposal_supplier, <0 if ko or >0 if ok
3204
     */
3205
    public function load_stats_proposal_supplier($socid = 0)
3206
    {
3207
		// phpcs:enable
3208
        global $conf, $user, $hookmanager, $action;
3209
3210
        $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_suppliers, COUNT(DISTINCT p.rowid) as nb,";
3211
        $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
3212
        $sql .= " FROM " . $this->db->prefix() . "supplier_proposaldet as pd";
3213
        $sql .= ", " . $this->db->prefix() . "supplier_proposal as p";
3214
        $sql .= ", " . $this->db->prefix() . "societe as s";
3215
        if (!$user->hasRight('societe', 'client', 'voir')) {
3216
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
3217
        }
3218
        $sql .= " WHERE p.rowid = pd.fk_supplier_proposal";
3219
        $sql .= " AND p.fk_soc = s.rowid";
3220
        $sql .= " AND p.entity IN (" . getEntity('supplier_proposal') . ")";
3221
        $sql .= " AND pd.fk_product = " . ((int) $this->id);
3222
        if (!$user->hasRight('societe', 'client', 'voir')) {
3223
            $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int) $user->id);
3224
        }
3225
        //$sql.= " AND pr.fk_statut != 0";
3226
        if ($socid > 0) {
3227
            $sql .= " AND p.fk_soc = " . ((int) $socid);
3228
        }
3229
3230
        $result = $this->db->query($sql);
3231
        if ($result) {
3232
            $obj = $this->db->fetch_object($result);
3233
            $this->stats_proposal_supplier['suppliers'] = $obj->nb_suppliers;
3234
            $this->stats_proposal_supplier['nb'] = $obj->nb;
3235
            $this->stats_proposal_supplier['rows'] = $obj->nb_rows;
3236
            $this->stats_proposal_supplier['qty'] = $obj->qty ? $obj->qty : 0;
3237
3238
            $parameters = array('socid' => $socid);
3239
            $reshook = $hookmanager->executeHooks('loadStatsSupplierProposal', $parameters, $this, $action);
3240
            if ($reshook > 0) {
3241
                $this->stats_proposal_supplier = $hookmanager->resArray['stats_proposal_supplier'];
3242
            }
3243
3244
            return 1;
3245
        } else {
3246
            $this->error = $this->db->error();
3247
            return -1;
3248
        }
3249
    }
3250
3251
3252
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3253
    /**
3254
     *  Charge tableau des stats commande client pour le produit/service
3255
     *
3256
     * @param  int    $socid           Id thirdparty to filter on a thirdparty
3257
     * @param  string $filtrestatut    Id status to filter on a status
3258
     * @param  int    $forVirtualStock Ignore rights filter for virtual stock calculation. Set when load_stats_commande is used for virtual stock calculation.
3259
     * @return integer                 Array of stats in $this->stats_commande (nb=nb of order, qty=qty ordered), <0 if ko or >0 if ok
3260
     */
3261
    public function load_stats_commande($socid = 0, $filtrestatut = '', $forVirtualStock = 0)
3262
    {
3263
		// phpcs:enable
3264
        global $conf, $user, $hookmanager, $action;
3265
3266
        $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3267
        $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3268
        $sql .= " FROM " . $this->db->prefix() . "commandedet as cd";
3269
        $sql .= ", " . $this->db->prefix() . "commande as c";
3270
        $sql .= ", " . $this->db->prefix() . "societe as s";
3271
        if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3272
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
3273
        }
3274
        $sql .= " WHERE c.rowid = cd.fk_commande";
3275
        $sql .= " AND c.fk_soc = s.rowid";
3276
        $sql .= " AND c.entity IN (" . getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'commande') . ")";
3277
        $sql .= " AND cd.fk_product = " . ((int) $this->id);
3278
        if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3279
            $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int) $user->id);
3280
        }
3281
        if ($socid > 0) {
3282
            $sql .= " AND c.fk_soc = " . ((int) $socid);
3283
        }
3284
        if ($filtrestatut != '') {
3285
            $sql .= " AND c.fk_statut in (" . $this->db->sanitize($filtrestatut) . ")";
3286
        }
3287
3288
        $result = $this->db->query($sql);
3289
        if ($result) {
3290
            $obj = $this->db->fetch_object($result);
3291
            $this->stats_commande['customers'] = $obj->nb_customers;
3292
            $this->stats_commande['nb'] = $obj->nb;
3293
            $this->stats_commande['rows'] = $obj->nb_rows;
3294
            $this->stats_commande['qty'] = $obj->qty ? $obj->qty : 0;
3295
3296
            // if it's a virtual product, maybe it is in order by extension
3297
            if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3298
                $TFather = $this->getFather();
3299
                if (is_array($TFather) && !empty($TFather)) {
3300
                    foreach ($TFather as &$fatherData) {
3301
                        $pFather = new Product($this->db);
3302
                        $pFather->id = $fatherData['id'];
3303
                        $qtyCoef = $fatherData['qty'];
3304
3305
                        if ($fatherData['incdec']) {
3306
                            $pFather->load_stats_commande($socid, $filtrestatut);
3307
3308
                            $this->stats_commande['customers'] += $pFather->stats_commande['customers'];
3309
                            $this->stats_commande['nb'] += $pFather->stats_commande['nb'];
3310
                            $this->stats_commande['rows'] += $pFather->stats_commande['rows'];
3311
                            $this->stats_commande['qty'] += $pFather->stats_commande['qty'] * $qtyCoef;
3312
                        }
3313
                    }
3314
                }
3315
            }
3316
3317
            // If stock decrease is on invoice validation, the theoretical stock continue to
3318
            // count the orders to ship in theoretical stock when some are already removed by invoice validation.
3319
            if ($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_ON_BILL')) {
3320
                if (getDolGlobalString('DECREASE_ONLY_UNINVOICEDPRODUCTS')) {
3321
                    // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is on, we make a compensation but only if order not yet invoice.
3322
                    $adeduire = 0;
3323
                    $sql = "SELECT SUM(" . $this->db->ifsql('f.type=2', -1, 1) . " * fd.qty) as count FROM " . $this->db->prefix() . "facturedet as fd ";
3324
                    $sql .= " JOIN " . $this->db->prefix() . "facture as f ON fd.fk_facture = f.rowid";
3325
                    $sql .= " JOIN " . $this->db->prefix() . "element_element as el ON ((el.fk_target = f.rowid AND el.targettype = 'facture' AND sourcetype = 'commande') OR (el.fk_source = f.rowid AND el.targettype = 'commande' AND sourcetype = 'facture'))";
3326
                    $sql .= " JOIN " . $this->db->prefix() . "commande as c ON el.fk_source = c.rowid";
3327
                    $sql .= " WHERE c.fk_statut IN (" . $this->db->sanitize($filtrestatut) . ") AND c.facture = 0 AND fd.fk_product = " . ((int) $this->id);
3328
3329
                    dol_syslog(__METHOD__ . ":: sql $sql", LOG_NOTICE);
3330
                    $resql = $this->db->query($sql);
3331
                    if ($resql) {
3332
                        if ($this->db->num_rows($resql) > 0) {
3333
                            $obj = $this->db->fetch_object($resql);
3334
                            $adeduire += $obj->count;
3335
                        }
3336
                    }
3337
3338
                    $this->stats_commande['qty'] -= $adeduire;
3339
                } else {
3340
                    // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is off, we make a compensation with lines of invoices linked to the order
3341
                    include_once DOL_DOCUMENT_ROOT . '/compta/facture/class/facture.class.php';
3342
3343
                    // For every order having invoice already validated we need to decrease stock cause it's in physical stock
3344
                    $adeduire = 0;
3345
                    $sql = "SELECT sum(" . $this->db->ifsql('f.type=2', -1, 1) . " * fd.qty) as count FROM " . MAIN_DB_PREFIX . "facturedet as fd ";
3346
                    $sql .= " JOIN " . MAIN_DB_PREFIX . "facture as f ON fd.fk_facture = f.rowid";
3347
                    $sql .= " JOIN " . MAIN_DB_PREFIX . "element_element as el ON ((el.fk_target = f.rowid AND el.targettype = 'facture' AND sourcetype = 'commande') OR (el.fk_source = f.rowid AND el.targettype = 'commande' AND sourcetype = 'facture'))";
3348
                    $sql .= " JOIN " . MAIN_DB_PREFIX . "commande as c ON el.fk_source = c.rowid";
3349
                    $sql .= " WHERE c.fk_statut IN (" . $this->db->sanitize($filtrestatut) . ") AND f.fk_statut > " . Facture::STATUS_DRAFT . " AND fd.fk_product = " . ((int) $this->id);
0 ignored issues
show
Bug introduced by
The type Dolibarr\Code\Product\Classes\Facture was not found. Did you mean Facture? If so, make sure to prefix the type with \.
Loading history...
3350
3351
                    dol_syslog(__METHOD__ . ":: sql $sql", LOG_NOTICE);
3352
                    $resql = $this->db->query($sql);
3353
                    if ($resql) {
3354
                        if ($this->db->num_rows($resql) > 0) {
3355
                            $obj = $this->db->fetch_object($resql);
3356
                            $adeduire += $obj->count;
3357
                        }
3358
                    } else {
3359
                        $this->error = $this->db->error();
3360
                        return -1;
3361
                    }
3362
3363
                    $this->stats_commande['qty'] -= $adeduire;
3364
                }
3365
            }
3366
3367
            $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3368
            $reshook = $hookmanager->executeHooks('loadStatsCustomerOrder', $parameters, $this, $action);
3369
            if ($reshook > 0) {
3370
                $this->stats_commande = $hookmanager->resArray['stats_commande'];
3371
            }
3372
            return 1;
3373
        } else {
3374
            $this->error = $this->db->error();
3375
            return -1;
3376
        }
3377
    }
3378
3379
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3380
    /**
3381
     *  Charge tableau des stats commande fournisseur pour le produit/service
3382
     *
3383
     * @param   int     $socid              Id thirdparty to filter on a thirdparty
3384
     * @param   string  $filtrestatut       Id of status to filter on status
3385
     * @param   int     $forVirtualStock    Ignore rights filter for virtual stock calculation.
3386
     * @param   int     $dateofvirtualstock Date of virtual stock
3387
     * @return  int                         Array of stats in $this->stats_commande_fournisseur, <0 if ko or >0 if ok
3388
     */
3389
    public function load_stats_commande_fournisseur($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3390
    {
3391
		// phpcs:enable
3392
        global $conf, $user, $hookmanager, $action;
3393
3394
        $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_suppliers, COUNT(DISTINCT c.rowid) as nb,";
3395
        $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3396
        $sql .= " FROM " . $this->db->prefix() . "commande_fournisseurdet as cd";
3397
        $sql .= ", " . $this->db->prefix() . "commande_fournisseur as c";
3398
        $sql .= ", " . $this->db->prefix() . "societe as s";
3399
        if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3400
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
3401
        }
3402
        $sql .= " WHERE c.rowid = cd.fk_commande";
3403
        $sql .= " AND c.fk_soc = s.rowid";
3404
        $sql .= " AND c.entity IN (" . getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'supplier_order') . ")";
3405
        $sql .= " AND cd.fk_product = " . ((int) $this->id);
3406
        if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3407
            $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int) $user->id);
3408
        }
3409
        if ($socid > 0) {
3410
            $sql .= " AND c.fk_soc = " . ((int) $socid);
3411
        }
3412
        if ($filtrestatut != '') {
3413
            $sql .= " AND c.fk_statut in (" . $this->db->sanitize($filtrestatut) . ")"; // Peut valoir 0
3414
        }
3415
        if (!empty($dateofvirtualstock)) {
3416
            $sql .= " AND c.date_livraison <= '" . $this->db->idate($dateofvirtualstock) . "'";
3417
        }
3418
3419
        $result = $this->db->query($sql);
3420
        if ($result) {
3421
            $obj = $this->db->fetch_object($result);
3422
            $this->stats_commande_fournisseur['suppliers'] = $obj->nb_suppliers;
3423
            $this->stats_commande_fournisseur['nb'] = $obj->nb;
3424
            $this->stats_commande_fournisseur['rows'] = $obj->nb_rows;
3425
            $this->stats_commande_fournisseur['qty'] = $obj->qty ? $obj->qty : 0;
3426
3427
            $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3428
            $reshook = $hookmanager->executeHooks('loadStatsSupplierOrder', $parameters, $this, $action);
3429
            if ($reshook > 0) {
3430
                $this->stats_commande_fournisseur = $hookmanager->resArray['stats_commande_fournisseur'];
3431
            }
3432
3433
            return 1;
3434
        } else {
3435
            $this->error = $this->db->error() . ' sql=' . $sql;
3436
            return -1;
3437
        }
3438
    }
3439
3440
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3441
    /**
3442
     *  Charge tableau des stats expedition client pour le produit/service
3443
     *
3444
     * @param   int         $socid                  Id thirdparty to filter on a thirdparty
3445
     * @param   string      $filtrestatut           [=''] Ids order status separated by comma
3446
     * @param   int         $forVirtualStock        Ignore rights filter for virtual stock calculation.
3447
     * @param   string      $filterShipmentStatus   [=''] Ids shipment status separated by comma
3448
     * @return  int                                 Array of stats in $this->stats_expedition, <0 if ko or >0 if ok
3449
     */
3450
    public function load_stats_sending($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $filterShipmentStatus = '')
3451
    {
3452
		// phpcs:enable
3453
        global $conf, $user, $hookmanager, $action;
3454
3455
        $sql = "SELECT COUNT(DISTINCT e.fk_soc) as nb_customers, COUNT(DISTINCT e.rowid) as nb,";
3456
        $sql .= " COUNT(ed.rowid) as nb_rows, SUM(ed.qty) as qty";
3457
        $sql .= " FROM " . $this->db->prefix() . "expeditiondet as ed";
3458
        $sql .= ", " . $this->db->prefix() . "commandedet as cd";
3459
        $sql .= ", " . $this->db->prefix() . "commande as c";
3460
        $sql .= ", " . $this->db->prefix() . "expedition as e";
3461
        $sql .= ", " . $this->db->prefix() . "societe as s";
3462
        if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3463
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
3464
        }
3465
        $sql .= " WHERE e.rowid = ed.fk_expedition";
3466
        $sql .= " AND c.rowid = cd.fk_commande";
3467
        $sql .= " AND e.fk_soc = s.rowid";
3468
        $sql .= " AND e.entity IN (" . getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'expedition') . ")";
3469
        $sql .= " AND ed.fk_elementdet = cd.rowid";
3470
        $sql .= " AND cd.fk_product = " . ((int) $this->id);
3471
        if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3472
            $sql .= " AND e.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int) $user->id);
3473
        }
3474
        if ($socid > 0) {
3475
            $sql .= " AND e.fk_soc = " . ((int) $socid);
3476
        }
3477
        if ($filtrestatut != '') {
3478
            $sql .= " AND c.fk_statut IN (" . $this->db->sanitize($filtrestatut) . ")";
3479
        }
3480
        if (!empty($filterShipmentStatus)) {
3481
            $sql .= " AND e.fk_statut IN (" . $this->db->sanitize($filterShipmentStatus) . ")";
3482
        }
3483
3484
        $result = $this->db->query($sql);
3485
        if ($result) {
3486
            $obj = $this->db->fetch_object($result);
3487
            $this->stats_expedition['customers'] = $obj->nb_customers;
3488
            $this->stats_expedition['nb'] = $obj->nb;
3489
            $this->stats_expedition['rows'] = $obj->nb_rows;
3490
            $this->stats_expedition['qty'] = $obj->qty ? $obj->qty : 0;
3491
3492
            // if it's a virtual product, maybe it is in sending by extension
3493
            if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3494
                $TFather = $this->getFather();
3495
                if (is_array($TFather) && !empty($TFather)) {
3496
                    foreach ($TFather as &$fatherData) {
3497
                        $pFather = new Product($this->db);
3498
                        $pFather->id = $fatherData['id'];
3499
                        $qtyCoef = $fatherData['qty'];
3500
3501
                        if ($fatherData['incdec']) {
3502
                            $pFather->load_stats_sending($socid, $filtrestatut, $forVirtualStock);
3503
3504
                            $this->stats_expedition['customers'] += $pFather->stats_expedition['customers'];
3505
                            $this->stats_expedition['nb'] += $pFather->stats_expedition['nb'];
3506
                            $this->stats_expedition['rows'] += $pFather->stats_expedition['rows'];
3507
                            $this->stats_expedition['qty'] += $pFather->stats_expedition['qty'] * $qtyCoef;
3508
                        }
3509
                    }
3510
                }
3511
            }
3512
3513
            $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock, 'filterShipmentStatus' => $filterShipmentStatus);
3514
            $reshook = $hookmanager->executeHooks('loadStatsSending', $parameters, $this, $action);
3515
            if ($reshook > 0) {
3516
                $this->stats_expedition = $hookmanager->resArray['stats_expedition'];
3517
            }
3518
3519
            return 1;
3520
        } else {
3521
            $this->error = $this->db->error();
3522
            return -1;
3523
        }
3524
    }
3525
3526
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3527
    /**
3528
     *  Charge tableau des stats réception fournisseur pour le produit/service
3529
     *
3530
     * @param  int      $socid              Id thirdparty to filter on a thirdparty
3531
     * @param  string   $filtrestatut       Id status to filter on a status
3532
     * @param  int      $forVirtualStock    Ignore rights filter for virtual stock calculation.
3533
     * @param   int     $dateofvirtualstock Date of virtual stock
3534
     * @return int                          Array of stats in $this->stats_reception, <0 if ko or >0 if ok
3535
     */
3536
    public function load_stats_reception($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3537
    {
3538
		// phpcs:enable
3539
        global $conf, $user, $hookmanager, $action;
3540
3541
        $sql = "SELECT COUNT(DISTINCT cf.fk_soc) as nb_suppliers, COUNT(DISTINCT cf.rowid) as nb,";
3542
        $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
3543
        $sql .= " FROM " . $this->db->prefix() . "receptiondet_batch as fd";
3544
        $sql .= ", " . $this->db->prefix() . "commande_fournisseur as cf";
3545
        $sql .= ", " . $this->db->prefix() . "societe as s";
3546
        if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3547
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
3548
        }
3549
        $sql .= " WHERE cf.rowid = fd.fk_element";
3550
        $sql .= " AND cf.fk_soc = s.rowid";
3551
        $sql .= " AND cf.entity IN (" . getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'supplier_order') . ")";
3552
        $sql .= " AND fd.fk_product = " . ((int) $this->id);
3553
        if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3554
            $sql .= " AND cf.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int) $user->id);
3555
        }
3556
        if ($socid > 0) {
3557
            $sql .= " AND cf.fk_soc = " . ((int) $socid);
3558
        }
3559
        if ($filtrestatut != '') {
3560
            $sql .= " AND cf.fk_statut IN (" . $this->db->sanitize($filtrestatut) . ")";
3561
        }
3562
        if (!empty($dateofvirtualstock)) {
3563
            $sql .= " AND fd.datec <= '" . $this->db->idate($dateofvirtualstock) . "'";
3564
        }
3565
3566
        $result = $this->db->query($sql);
3567
        if ($result) {
3568
            $obj = $this->db->fetch_object($result);
3569
            $this->stats_reception['suppliers'] = $obj->nb_suppliers;
3570
            $this->stats_reception['nb'] = $obj->nb;
3571
            $this->stats_reception['rows'] = $obj->nb_rows;
3572
            $this->stats_reception['qty'] = $obj->qty ? $obj->qty : 0;
3573
3574
            $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3575
            $reshook = $hookmanager->executeHooks('loadStatsReception', $parameters, $this, $action);
3576
            if ($reshook > 0) {
3577
                $this->stats_reception = $hookmanager->resArray['stats_reception'];
3578
            }
3579
3580
            return 1;
3581
        } else {
3582
            $this->error = $this->db->error();
3583
            return -1;
3584
        }
3585
    }
3586
3587
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3588
    /**
3589
     *  Charge tableau des stats production pour le produit/service
3590
     *
3591
     * @param   int     $socid              Id thirdparty to filter on a thirdparty
3592
     * @param   string  $filtrestatut       Id status to filter on a status
3593
     * @param   int     $forVirtualStock    Ignore rights filter for virtual stock calculation.
3594
     * @param   int     $dateofvirtualstock Date of virtual stock
3595
     * @param   int     $warehouseid        Filter by a warehouse. Warning: When a filter on a warehouse is set, it is not possible to calculate an accurate virtual stock because we can't know in which warehouse will be done virtual stock changes.
3596
     * @return  integer                     Array of stats in $this->stats_mrptoproduce (nb=nb of order, qty=qty ordered), <0 if ko or >0 if ok
3597
     */
3598
    public function load_stats_inproduction($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null, $warehouseid = 0)
3599
    {
3600
		// phpcs:enable
3601
        global $user, $hookmanager, $action;
3602
3603
        $serviceStockIsEnabled = isModEnabled("service") && getDolGlobalString('STOCK_SUPPORTS_SERVICES');
3604
3605
        $sql = "SELECT COUNT(DISTINCT m.fk_soc) as nb_customers, COUNT(DISTINCT m.rowid) as nb,";
3606
        $sql .= " COUNT(mp.rowid) as nb_rows, SUM(mp.qty) as qty, role";
3607
        $sql .= " FROM " . $this->db->prefix() . "mrp_production as mp";
3608
        $sql .= ", " . $this->db->prefix() . "mrp_mo as m";
3609
        $sql .= " LEFT JOIN " . $this->db->prefix() . "societe as s ON s.rowid = m.fk_soc";
3610
        if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3611
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
3612
        }
3613
        $sql .= " WHERE m.rowid = mp.fk_mo";
3614
        $sql .= " AND m.entity IN (" . getEntity(($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE')) ? 'stock' : 'mrp') . ")";
3615
        $sql .= " AND mp.fk_product = " . ((int) $this->id);
3616
        $sql .= " AND (mp.disable_stock_change IN (0) OR mp.disable_stock_change IS NULL)";
3617
        if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3618
            $sql .= " AND m.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int) $user->id);
3619
        }
3620
        if ($socid > 0) {
3621
            $sql .= " AND m.fk_soc = " . ((int) $socid);
3622
        }
3623
        if ($filtrestatut != '') {
3624
            $sql .= " AND m.status IN (" . $this->db->sanitize($filtrestatut) . ")";
3625
        }
3626
        if (!empty($dateofvirtualstock)) {
3627
            $sql .= " AND m.date_valid <= '" . $this->db->idate($dateofvirtualstock) . "'"; // better date to code ? end of production ?
3628
        }
3629
        if (!$serviceStockIsEnabled) {
3630
            $sql .= "AND EXISTS (SELECT p.rowid FROM " . $this->db->prefix() . "product AS p WHERE p.rowid = " . ((int) $this->id) . " AND p.fk_product_type IN (0))";
3631
        }
3632
        if (!empty($warehouseid)) {
3633
            $sql .= " AND m.fk_warehouse = " . ((int) $warehouseid);
3634
        }
3635
        $sql .= " GROUP BY role";
3636
3637
        if ($warehouseid) {
3638
            $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] = 0;
3639
        } else {
3640
            $this->stats_mrptoconsume['customers'] = 0;
3641
            $this->stats_mrptoconsume['nb'] = 0;
3642
            $this->stats_mrptoconsume['rows'] = 0;
3643
            $this->stats_mrptoconsume['qty'] = 0;
3644
            $this->stats_mrptoproduce['customers'] = 0;
3645
            $this->stats_mrptoproduce['nb'] = 0;
3646
            $this->stats_mrptoproduce['rows'] = 0;
3647
            $this->stats_mrptoproduce['qty'] = 0;
3648
        }
3649
3650
        $result = $this->db->query($sql);
3651
        if ($result) {
3652
            while ($obj = $this->db->fetch_object($result)) {
3653
                if ($obj->role == 'toconsume' && empty($warehouseid)) {
3654
                    $this->stats_mrptoconsume['customers'] += $obj->nb_customers;
3655
                    $this->stats_mrptoconsume['nb'] += $obj->nb;
3656
                    $this->stats_mrptoconsume['rows'] += $obj->nb_rows;
3657
                    $this->stats_mrptoconsume['qty'] += ($obj->qty ? $obj->qty : 0);
3658
                }
3659
                if ($obj->role == 'consumed' && empty($warehouseid)) {
3660
                    //$this->stats_mrptoconsume['customers'] += $obj->nb_customers;
3661
                    //$this->stats_mrptoconsume['nb'] += $obj->nb;
3662
                    //$this->stats_mrptoconsume['rows'] += $obj->nb_rows;
3663
                    $this->stats_mrptoconsume['qty'] -= ($obj->qty ? $obj->qty : 0);
3664
                }
3665
                if ($obj->role == 'toproduce') {
3666
                    if ($warehouseid) {
3667
                        $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] += ($obj->qty ? $obj->qty : 0);
3668
                    } else {
3669
                        $this->stats_mrptoproduce['customers'] += $obj->nb_customers;
3670
                        $this->stats_mrptoproduce['nb'] += $obj->nb;
3671
                        $this->stats_mrptoproduce['rows'] += $obj->nb_rows;
3672
                        $this->stats_mrptoproduce['qty'] += ($obj->qty ? $obj->qty : 0);
3673
                    }
3674
                }
3675
                if ($obj->role == 'produced') {
3676
                    //$this->stats_mrptoproduce['customers'] += $obj->nb_customers;
3677
                    //$this->stats_mrptoproduce['nb'] += $obj->nb;
3678
                    //$this->stats_mrptoproduce['rows'] += $obj->nb_rows;
3679
                    if ($warehouseid) {
3680
                        $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
3681
                    } else {
3682
                        $this->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
3683
                    }
3684
                }
3685
            }
3686
3687
            // Clean data
3688
            if ($warehouseid) {
3689
                if ($this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] < 0) {
3690
                    $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] = 0;
3691
                }
3692
            } else {
3693
                if ($this->stats_mrptoconsume['qty'] < 0) {
3694
                    $this->stats_mrptoconsume['qty'] = 0;
3695
                }
3696
                if ($this->stats_mrptoproduce['qty'] < 0) {
3697
                    $this->stats_mrptoproduce['qty'] = 0;
3698
                }
3699
            }
3700
3701
            $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3702
            $reshook = $hookmanager->executeHooks('loadStatsInProduction', $parameters, $this, $action);
3703
            if ($reshook > 0) {
3704
                $this->stats_mrptoproduce = $hookmanager->resArray['stats_mrptoproduce'];
3705
            }
3706
3707
            return 1;
3708
        } else {
3709
            $this->error = $this->db->error();
3710
            return -1;
3711
        }
3712
    }
3713
3714
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3715
    /**
3716
     *  Charge tableau des stats contrat pour le produit/service
3717
     *
3718
     * @param  int $socid Id societe
3719
     * @return int                     Array of stats in $this->stats_contrat, <0 if ko or >0 if ok
3720
     */
3721
    public function load_stats_contrat($socid = 0)
3722
    {
3723
		// phpcs:enable
3724
        global $conf, $user, $hookmanager, $action;
3725
3726
        $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3727
        $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3728
        $sql .= " FROM " . $this->db->prefix() . "contratdet as cd";
3729
        $sql .= ", " . $this->db->prefix() . "contrat as c";
3730
        $sql .= ", " . $this->db->prefix() . "societe as s";
3731
        if (!$user->hasRight('societe', 'client', 'voir')) {
3732
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
3733
        }
3734
        $sql .= " WHERE c.rowid = cd.fk_contrat";
3735
        $sql .= " AND c.fk_soc = s.rowid";
3736
        $sql .= " AND c.entity IN (" . getEntity('contract') . ")";
3737
        $sql .= " AND cd.fk_product = " . ((int) $this->id);
3738
        if (!$user->hasRight('societe', 'client', 'voir')) {
3739
            $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int) $user->id);
3740
        }
3741
        //$sql.= " AND c.statut != 0";
3742
        if ($socid > 0) {
3743
            $sql .= " AND c.fk_soc = " . ((int) $socid);
3744
        }
3745
3746
        $result = $this->db->query($sql);
3747
        if ($result) {
3748
            $obj = $this->db->fetch_object($result);
3749
            $this->stats_contrat['customers'] = $obj->nb_customers;
3750
            $this->stats_contrat['nb'] = $obj->nb;
3751
            $this->stats_contrat['rows'] = $obj->nb_rows;
3752
            $this->stats_contrat['qty'] = $obj->qty ? $obj->qty : 0;
3753
3754
            // if it's a virtual product, maybe it is in contract by extension
3755
            if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3756
                $TFather = $this->getFather();
3757
                if (is_array($TFather) && !empty($TFather)) {
3758
                    foreach ($TFather as &$fatherData) {
3759
                        $pFather = new Product($this->db);
3760
                        $pFather->id = $fatherData['id'];
3761
                        $qtyCoef = $fatherData['qty'];
3762
3763
                        if ($fatherData['incdec']) {
3764
                            $pFather->load_stats_contrat($socid);
3765
3766
                            $this->stats_contrat['customers'] += $pFather->stats_contrat['customers'];
3767
                            $this->stats_contrat['nb'] += $pFather->stats_contrat['nb'];
3768
                            $this->stats_contrat['rows'] += $pFather->stats_contrat['rows'];
3769
                            $this->stats_contrat['qty'] += $pFather->stats_contrat['qty'] * $qtyCoef;
3770
                        }
3771
                    }
3772
                }
3773
            }
3774
3775
            $parameters = array('socid' => $socid);
3776
            $reshook = $hookmanager->executeHooks('loadStatsContract', $parameters, $this, $action);
3777
            if ($reshook > 0) {
3778
                $this->stats_contrat = $hookmanager->resArray['stats_contrat'];
3779
            }
3780
3781
            return 1;
3782
        } else {
3783
            $this->error = $this->db->error() . ' sql=' . $sql;
3784
            return -1;
3785
        }
3786
    }
3787
3788
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3789
    /**
3790
     *  Charge tableau des stats facture pour le produit/service
3791
     *
3792
     * @param  int $socid Id societe
3793
     * @return int                     Array of stats in $this->stats_facture, <0 if ko or >0 if ok
3794
     */
3795
    public function load_stats_facture($socid = 0)
3796
    {
3797
		// phpcs:enable
3798
        global $conf, $user, $hookmanager, $action;
3799
3800
        $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
3801
        $sql .= " COUNT(fd.rowid) as nb_rows, SUM(" . $this->db->ifsql('f.type != 2', 'fd.qty', 'fd.qty * -1') . ") as qty";
3802
        $sql .= " FROM " . $this->db->prefix() . "facturedet as fd";
3803
        $sql .= ", " . $this->db->prefix() . "facture as f";
3804
        $sql .= ", " . $this->db->prefix() . "societe as s";
3805
        if (!$user->hasRight('societe', 'client', 'voir')) {
3806
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
3807
        }
3808
        $sql .= " WHERE f.rowid = fd.fk_facture";
3809
        $sql .= " AND f.fk_soc = s.rowid";
3810
        $sql .= " AND f.entity IN (" . getEntity('invoice') . ")";
3811
        $sql .= " AND fd.fk_product = " . ((int) $this->id);
3812
        if (!$user->hasRight('societe', 'client', 'voir')) {
3813
            $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int) $user->id);
3814
        }
3815
        //$sql.= " AND f.fk_statut != 0";
3816
        if ($socid > 0) {
3817
            $sql .= " AND f.fk_soc = " . ((int) $socid);
3818
        }
3819
3820
        $result = $this->db->query($sql);
3821
        if ($result) {
3822
            $obj = $this->db->fetch_object($result);
3823
            $this->stats_facture['customers'] = $obj->nb_customers;
3824
            $this->stats_facture['nb'] = $obj->nb;
3825
            $this->stats_facture['rows'] = $obj->nb_rows;
3826
            $this->stats_facture['qty'] = $obj->qty ? $obj->qty : 0;
3827
3828
            // if it's a virtual product, maybe it is in invoice by extension
3829
            if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3830
                $TFather = $this->getFather();
3831
                if (is_array($TFather) && !empty($TFather)) {
3832
                    foreach ($TFather as &$fatherData) {
3833
                        $pFather = new Product($this->db);
3834
                        $pFather->id = $fatherData['id'];
3835
                        $qtyCoef = $fatherData['qty'];
3836
3837
                        if ($fatherData['incdec']) {
3838
                            $pFather->load_stats_facture($socid);
3839
3840
                            $this->stats_facture['customers'] += $pFather->stats_facture['customers'];
3841
                            $this->stats_facture['nb'] += $pFather->stats_facture['nb'];
3842
                            $this->stats_facture['rows'] += $pFather->stats_facture['rows'];
3843
                            $this->stats_facture['qty'] += $pFather->stats_facture['qty'] * $qtyCoef;
3844
                        }
3845
                    }
3846
                }
3847
            }
3848
3849
            $parameters = array('socid' => $socid);
3850
            $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoice', $parameters, $this, $action);
3851
            if ($reshook > 0) {
3852
                $this->stats_facture = $hookmanager->resArray['stats_facture'];
3853
            }
3854
3855
            return 1;
3856
        } else {
3857
            $this->error = $this->db->error();
3858
            return -1;
3859
        }
3860
    }
3861
3862
3863
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3864
    /**
3865
     *  Charge tableau des stats facture recurrentes pour le produit/service
3866
     *
3867
     * @param  int $socid Id societe
3868
     * @return int                     Array of stats in $this->stats_facture, <0 if ko or >0 if ok
3869
     */
3870
    public function load_stats_facturerec($socid = 0)
3871
    {
3872
		// phpcs:enable
3873
        global $conf, $user, $hookmanager, $action;
3874
3875
        $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
3876
        $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
3877
        $sql .= " FROM " . MAIN_DB_PREFIX . "facturedet_rec as fd";
3878
        $sql .= ", " . MAIN_DB_PREFIX . "facture_rec as f";
3879
        $sql .= ", " . MAIN_DB_PREFIX . "societe as s";
3880
        if (!$user->hasRight('societe', 'client', 'voir')) {
3881
            $sql .= ", " . MAIN_DB_PREFIX . "societe_commerciaux as sc";
3882
        }
3883
        $sql .= " WHERE f.rowid = fd.fk_facture";
3884
        $sql .= " AND f.fk_soc = s.rowid";
3885
        $sql .= " AND f.entity IN (" . getEntity('invoice') . ")";
3886
        $sql .= " AND fd.fk_product = " . ((int) $this->id);
3887
        if (!$user->hasRight('societe', 'client', 'voir')) {
3888
            $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int) $user->id);
3889
        }
3890
        //$sql.= " AND f.fk_statut != 0";
3891
        if ($socid > 0) {
3892
            $sql .= " AND f.fk_soc = " . ((int) $socid);
3893
        }
3894
3895
        $result = $this->db->query($sql);
3896
        if ($result) {
3897
            $obj = $this->db->fetch_object($result);
3898
            $this->stats_facturerec['customers'] = $obj->nb_customers;
3899
            $this->stats_facturerec['nb'] = $obj->nb;
3900
            $this->stats_facturerec['rows'] = $obj->nb_rows;
3901
            $this->stats_facturerec['qty'] = $obj->qty ? $obj->qty : 0;
3902
3903
            // if it's a virtual product, maybe it is in invoice by extension
3904
            if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3905
                $TFather = $this->getFather();
3906
                if (is_array($TFather) && !empty($TFather)) {
3907
                    foreach ($TFather as &$fatherData) {
3908
                        $pFather = new Product($this->db);
3909
                        $pFather->id = $fatherData['id'];
3910
                        $qtyCoef = $fatherData['qty'];
3911
3912
                        if ($fatherData['incdec']) {
3913
                            $pFather->load_stats_facture($socid);
3914
3915
                            $this->stats_facturerec['customers'] += $pFather->stats_facturerec['customers'];
3916
                            $this->stats_facturerec['nb'] += $pFather->stats_facturerec['nb'];
3917
                            $this->stats_facturerec['rows'] += $pFather->stats_facturerec['rows'];
3918
                            $this->stats_facturerec['qty'] += $pFather->stats_facturerec['qty'] * $qtyCoef;
3919
                        }
3920
                    }
3921
                }
3922
            }
3923
3924
            $parameters = array('socid' => $socid);
3925
            $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoiceRec', $parameters, $this, $action);
3926
            if ($reshook > 0) {
3927
                $this->stats_facturerec = $hookmanager->resArray['stats_facturerec'];
3928
            }
3929
3930
            return 1;
3931
        } else {
3932
            $this->error = $this->db->error();
3933
            return -1;
3934
        }
3935
    }
3936
3937
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3938
    /**
3939
     *  Charge tableau des stats facture pour le produit/service
3940
     *
3941
     * @param  int $socid Id societe
3942
     * @return int                     Array of stats in $this->stats_facture_fournisseur, <0 if ko or >0 if ok
3943
     */
3944
    public function load_stats_facture_fournisseur($socid = 0)
3945
    {
3946
		// phpcs:enable
3947
        global $conf, $user, $hookmanager, $action;
3948
3949
        $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,";
3950
        $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
3951
        $sql .= " FROM " . $this->db->prefix() . "facture_fourn_det as fd";
3952
        $sql .= ", " . $this->db->prefix() . "facture_fourn as f";
3953
        $sql .= ", " . $this->db->prefix() . "societe as s";
3954
        if (!$user->hasRight('societe', 'client', 'voir')) {
3955
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
3956
        }
3957
        $sql .= " WHERE f.rowid = fd.fk_facture_fourn";
3958
        $sql .= " AND f.fk_soc = s.rowid";
3959
        $sql .= " AND f.entity IN (" . getEntity('facture_fourn') . ")";
3960
        $sql .= " AND fd.fk_product = " . ((int) $this->id);
3961
        if (!$user->hasRight('societe', 'client', 'voir')) {
3962
            $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int) $user->id);
3963
        }
3964
        //$sql.= " AND f.fk_statut != 0";
3965
        if ($socid > 0) {
3966
            $sql .= " AND f.fk_soc = " . ((int) $socid);
3967
        }
3968
3969
        $result = $this->db->query($sql);
3970
        if ($result) {
3971
            $obj = $this->db->fetch_object($result);
3972
            $this->stats_facture_fournisseur['suppliers'] = $obj->nb_suppliers;
3973
            $this->stats_facture_fournisseur['nb'] = $obj->nb;
3974
            $this->stats_facture_fournisseur['rows'] = $obj->nb_rows;
3975
            $this->stats_facture_fournisseur['qty'] = $obj->qty ? $obj->qty : 0;
3976
3977
            $parameters = array('socid' => $socid);
3978
            $reshook = $hookmanager->executeHooks('loadStatsSupplierInvoice', $parameters, $this, $action);
3979
            if ($reshook > 0) {
3980
                $this->stats_facture_fournisseur = $hookmanager->resArray['stats_facture_fournisseur'];
3981
            }
3982
3983
            return 1;
3984
        } else {
3985
            $this->error = $this->db->error();
3986
            return -1;
3987
        }
3988
    }
3989
3990
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3991
    /**
3992
     *  Return an array formatted for showing graphs
3993
     *
3994
     * @param  string $sql          Request to execute
3995
     * @param  string $mode         'byunit'=number of unit, 'bynumber'=nb of entities
3996
     * @param  int    $year         Year (0=current year, -1=all years)
3997
     * @return array|int            Return integer <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
3998
     */
3999
    private function _get_stats($sql, $mode, $year = 0)
4000
    {
4001
		// phpcs:enable
4002
        $tab = array();
4003
4004
        $resql = $this->db->query($sql);
4005
        if ($resql) {
4006
            $num = $this->db->num_rows($resql);
4007
            $i = 0;
4008
            while ($i < $num) {
4009
                $arr = $this->db->fetch_array($resql);
4010
                if (is_array($arr)) {
4011
                    $keyfortab = (string) $arr[1];
4012
                    if ($year == -1) {
4013
                        $keyfortab = substr($keyfortab, -2);
4014
                    }
4015
4016
                    if ($mode == 'byunit') {
4017
                        $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[0]; // 1st field
4018
                    } elseif ($mode == 'bynumber') {
4019
                        $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
4020
                    } elseif ($mode == 'byamount') {
4021
                        $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
4022
                    } else {
4023
                        // Bad value for $mode
4024
                        return -1;
4025
                    }
4026
                }
4027
                $i++;
4028
            }
4029
        } else {
4030
            $this->error = $this->db->error() . ' sql=' . $sql;
4031
            return -1;
4032
        }
4033
4034
        if (empty($year)) {
4035
            $year = dol_print_date(time(), '%Y');
4036
            $month = dol_print_date(time(), '%m');
4037
        } elseif ($year == -1) {
4038
            $year = '';
4039
            $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
4040
        } else {
4041
            $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
4042
        }
4043
4044
        $result = array();
4045
4046
        for ($j = 0; $j < 12; $j++) {
4047
            // $ids is 'D', 'N', 'O', 'S', ... (First letter of month in user language)
4048
            $idx = ucfirst(dol_trunc(dol_print_date(dol_mktime(12, 0, 0, $month, 1, 1970), "%b"), 1, 'right', 'UTF-8', 1));
4049
4050
            //print $idx.'-'.$year.'-'.$month.'<br>';
4051
            $result[$j] = array($idx, isset($tab[$year . $month]) ? $tab[$year . $month] : 0);
4052
            //            $result[$j] = array($monthnum,isset($tab[$year.$month])?$tab[$year.$month]:0);
4053
4054
            $month = "0" . ($month - 1);
4055
            if (dol_strlen($month) == 3) {
4056
                $month = substr($month, 1);
4057
            }
4058
            if ($month == 0) {
4059
                $month = 12;
4060
                $year = $year - 1;
4061
            }
4062
        }
4063
4064
        return array_reverse($result);
4065
    }
4066
4067
4068
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4069
    /**
4070
     *  Return nb of units or customers invoices in which product is included
4071
     *
4072
     * @param  int    $socid               Limit count on a particular third party id
4073
     * @param  string $mode                'byunit'=number of unit, 'bynumber'=nb of entities
4074
     * @param  int    $filteronproducttype 0=To filter on product only, 1=To filter on services only
4075
     * @param  int    $year                Year (0=last 12 month, -1=all years)
4076
     * @param  string $morefilter          More sql filters
4077
     * @return array                       Return integer <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
4078
     */
4079
    public function get_nb_vente($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4080
    {
4081
		// phpcs:enable
4082
        global $conf;
4083
        global $user;
4084
4085
        $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4086
        if ($mode == 'bynumber') {
4087
            $sql .= ", count(DISTINCT f.rowid)";
4088
        }
4089
        $sql .= ", sum(d.total_ht) as total_ht";
4090
        $sql .= " FROM " . $this->db->prefix() . "facturedet as d, " . $this->db->prefix() . "facture as f, " . $this->db->prefix() . "societe as s";
4091
        if ($filteronproducttype >= 0) {
4092
            $sql .= ", " . $this->db->prefix() . "product as p";
4093
        }
4094
        if (!$user->hasRight('societe', 'client', 'voir')) {
4095
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
4096
        }
4097
        $sql .= " WHERE f.rowid = d.fk_facture";
4098
        if ($this->id > 0) {
4099
            $sql .= " AND d.fk_product = " . ((int) $this->id);
4100
        } else {
4101
            $sql .= " AND d.fk_product > 0";
4102
        }
4103
        if ($filteronproducttype >= 0) {
4104
            $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = " . ((int) $filteronproducttype);
4105
        }
4106
        $sql .= " AND f.fk_soc = s.rowid";
4107
        $sql .= " AND f.entity IN (" . getEntity('invoice') . ")";
4108
        if (!$user->hasRight('societe', 'client', 'voir')) {
4109
            $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int) $user->id);
4110
        }
4111
        if ($socid > 0) {
4112
            $sql .= " AND f.fk_soc = $socid";
4113
        }
4114
        $sql .= $morefilter;
4115
        $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4116
        $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4117
4118
        return $this->_get_stats($sql, $mode, $year);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_get_stats($sql, $mode, $year) also could return the type integer which is incompatible with the documented return type array.
Loading history...
4119
    }
4120
4121
4122
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4123
    /**
4124
     *  Return nb of units or supplier invoices in which product is included
4125
     *
4126
     * @param  int    $socid               Limit count on a particular third party id
4127
     * @param  string $mode                'byunit'=number of unit, 'bynumber'=nb of entities
4128
     * @param  int    $filteronproducttype 0=To filter on product only, 1=To filter on services only
4129
     * @param  int    $year                Year (0=last 12 month, -1=all years)
4130
     * @param  string $morefilter          More sql filters
4131
     * @return array                       Return integer <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
4132
     */
4133
    public function get_nb_achat($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4134
    {
4135
		// phpcs:enable
4136
        global $conf;
4137
        global $user;
4138
4139
        $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4140
        if ($mode == 'bynumber') {
4141
            $sql .= ", count(DISTINCT f.rowid)";
4142
        }
4143
        $sql .= ", sum(d.total_ht) as total_ht";
4144
        $sql .= " FROM " . $this->db->prefix() . "facture_fourn_det as d, " . $this->db->prefix() . "facture_fourn as f, " . $this->db->prefix() . "societe as s";
4145
        if ($filteronproducttype >= 0) {
4146
            $sql .= ", " . $this->db->prefix() . "product as p";
4147
        }
4148
        if (!$user->hasRight('societe', 'client', 'voir')) {
4149
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
4150
        }
4151
        $sql .= " WHERE f.rowid = d.fk_facture_fourn";
4152
        if ($this->id > 0) {
4153
            $sql .= " AND d.fk_product = " . ((int) $this->id);
4154
        } else {
4155
            $sql .= " AND d.fk_product > 0";
4156
        }
4157
        if ($filteronproducttype >= 0) {
4158
            $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = " . ((int) $filteronproducttype);
4159
        }
4160
        $sql .= " AND f.fk_soc = s.rowid";
4161
        $sql .= " AND f.entity IN (" . getEntity('facture_fourn') . ")";
4162
        if (!$user->hasRight('societe', 'client', 'voir')) {
4163
            $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int) $user->id);
4164
        }
4165
        if ($socid > 0) {
4166
            $sql .= " AND f.fk_soc = $socid";
4167
        }
4168
        $sql .= $morefilter;
4169
        $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4170
        $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4171
4172
        return $this->_get_stats($sql, $mode, $year);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_get_stats($sql, $mode, $year) also could return the type integer which is incompatible with the documented return type array.
Loading history...
4173
    }
4174
4175
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4176
    /**
4177
     * Return nb of units in proposals in which product is included
4178
     *
4179
     * @param  int    $socid               Limit count on a particular third party id
4180
     * @param  string $mode                'byunit'=number of unit, 'bynumber'=nb of entities, 'byamount'=amount
4181
     * @param  int    $filteronproducttype 0=To filter on product only, 1=To filter on services only
4182
     * @param  int    $year                Year (0=last 12 month, -1=all years)
4183
     * @param  string $morefilter          More sql filters
4184
     * @return array                       Return integer <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
4185
     */
4186
    public function get_nb_propal($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4187
    {
4188
		// phpcs:enable
4189
        global $conf, $user;
4190
4191
        $sql = "SELECT sum(d.qty) as qty, date_format(p.datep, '%Y%m')";
4192
        if ($mode == 'bynumber') {
4193
            $sql .= ", count(DISTINCT p.rowid)";
4194
        }
4195
        $sql .= ", sum(d.total_ht) as total_ht";
4196
        $sql .= " FROM " . $this->db->prefix() . "propaldet as d, " . $this->db->prefix() . "propal as p, " . $this->db->prefix() . "societe as s";
4197
        if ($filteronproducttype >= 0) {
4198
            $sql .= ", " . $this->db->prefix() . "product as prod";
4199
        }
4200
        if (!$user->hasRight('societe', 'client', 'voir')) {
4201
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
4202
        }
4203
        $sql .= " WHERE p.rowid = d.fk_propal";
4204
        if ($this->id > 0) {
4205
            $sql .= " AND d.fk_product = " . ((int) $this->id);
4206
        } else {
4207
            $sql .= " AND d.fk_product > 0";
4208
        }
4209
        if ($filteronproducttype >= 0) {
4210
            $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = " . ((int) $filteronproducttype);
4211
        }
4212
        $sql .= " AND p.fk_soc = s.rowid";
4213
        $sql .= " AND p.entity IN (" . getEntity('propal') . ")";
4214
        if (!$user->hasRight('societe', 'client', 'voir')) {
4215
            $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int) $user->id);
4216
        }
4217
        if ($socid > 0) {
4218
            $sql .= " AND p.fk_soc = " . ((int) $socid);
4219
        }
4220
        $sql .= $morefilter;
4221
        $sql .= " GROUP BY date_format(p.datep,'%Y%m')";
4222
        $sql .= " ORDER BY date_format(p.datep,'%Y%m') DESC";
4223
4224
        return $this->_get_stats($sql, $mode, $year);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_get_stats($sql, $mode, $year) also could return the type integer which is incompatible with the documented return type array.
Loading history...
4225
    }
4226
4227
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4228
    /**
4229
     *  Return nb of units in proposals in which product is included
4230
     *
4231
     * @param  int    $socid               Limit count on a particular third party id
4232
     * @param  string $mode                'byunit'=number of unit, 'bynumber'=nb of entities
4233
     * @param  int    $filteronproducttype 0=To filter on product only, 1=To filter on services only
4234
     * @param  int    $year                Year (0=last 12 month, -1=all years)
4235
     * @param  string $morefilter          More sql filters
4236
     * @return array                       Return integer <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
4237
     */
4238
    public function get_nb_propalsupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4239
    {
4240
		// phpcs:enable
4241
        global $conf;
4242
        global $user;
4243
4244
        $sql = "SELECT sum(d.qty) as qty, date_format(p.date_valid, '%Y%m')";
4245
        if ($mode == 'bynumber') {
4246
            $sql .= ", count(DISTINCT p.rowid)";
4247
        }
4248
        $sql .= ", sum(d.total_ht) as total_ht";
4249
        $sql .= " FROM " . $this->db->prefix() . "supplier_proposaldet as d, " . $this->db->prefix() . "supplier_proposal as p, " . $this->db->prefix() . "societe as s";
4250
        if ($filteronproducttype >= 0) {
4251
            $sql .= ", " . $this->db->prefix() . "product as prod";
4252
        }
4253
        if (!$user->hasRight('societe', 'client', 'voir')) {
4254
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
4255
        }
4256
        $sql .= " WHERE p.rowid = d.fk_supplier_proposal";
4257
        if ($this->id > 0) {
4258
            $sql .= " AND d.fk_product = " . ((int) $this->id);
4259
        } else {
4260
            $sql .= " AND d.fk_product > 0";
4261
        }
4262
        if ($filteronproducttype >= 0) {
4263
            $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = " . ((int) $filteronproducttype);
4264
        }
4265
        $sql .= " AND p.fk_soc = s.rowid";
4266
        $sql .= " AND p.entity IN (" . getEntity('supplier_proposal') . ")";
4267
        if (!$user->hasRight('societe', 'client', 'voir')) {
4268
            $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int) $user->id);
4269
        }
4270
        if ($socid > 0) {
4271
            $sql .= " AND p.fk_soc = " . ((int) $socid);
4272
        }
4273
        $sql .= $morefilter;
4274
        $sql .= " GROUP BY date_format(p.date_valid,'%Y%m')";
4275
        $sql .= " ORDER BY date_format(p.date_valid,'%Y%m') DESC";
4276
4277
        return $this->_get_stats($sql, $mode, $year);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_get_stats($sql, $mode, $year) also could return the type integer which is incompatible with the documented return type array.
Loading history...
4278
    }
4279
4280
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4281
    /**
4282
     *  Return nb of units in orders in which product is included
4283
     *
4284
     * @param  int    $socid               Limit count on a particular third party id
4285
     * @param  string $mode                'byunit'=number of unit, 'bynumber'=nb of entities
4286
     * @param  int    $filteronproducttype 0=To filter on product only, 1=To filter on services only
4287
     * @param  int    $year                Year (0=last 12 month, -1=all years)
4288
     * @param  string $morefilter          More sql filters
4289
     * @return array                       Return integer <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
4290
     */
4291
    public function get_nb_order($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4292
    {
4293
		// phpcs:enable
4294
        global $conf, $user;
4295
4296
        $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4297
        if ($mode == 'bynumber') {
4298
            $sql .= ", count(DISTINCT c.rowid)";
4299
        }
4300
        $sql .= ", sum(d.total_ht) as total_ht";
4301
        $sql .= " FROM " . $this->db->prefix() . "commandedet as d, " . $this->db->prefix() . "commande as c, " . $this->db->prefix() . "societe as s";
4302
        if ($filteronproducttype >= 0) {
4303
            $sql .= ", " . $this->db->prefix() . "product as p";
4304
        }
4305
        if (!$user->hasRight('societe', 'client', 'voir')) {
4306
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
4307
        }
4308
        $sql .= " WHERE c.rowid = d.fk_commande";
4309
        if ($this->id > 0) {
4310
            $sql .= " AND d.fk_product = " . ((int) $this->id);
4311
        } else {
4312
            $sql .= " AND d.fk_product > 0";
4313
        }
4314
        if ($filteronproducttype >= 0) {
4315
            $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = " . ((int) $filteronproducttype);
4316
        }
4317
        $sql .= " AND c.fk_soc = s.rowid";
4318
        $sql .= " AND c.entity IN (" . getEntity('commande') . ")";
4319
        if (!$user->hasRight('societe', 'client', 'voir')) {
4320
            $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int) $user->id);
4321
        }
4322
        if ($socid > 0) {
4323
            $sql .= " AND c.fk_soc = " . ((int) $socid);
4324
        }
4325
        $sql .= $morefilter;
4326
        $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4327
        $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4328
4329
        return $this->_get_stats($sql, $mode, $year);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_get_stats($sql, $mode, $year) also could return the type integer which is incompatible with the documented return type array.
Loading history...
4330
    }
4331
4332
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4333
    /**
4334
     *  Return nb of units in orders in which product is included
4335
     *
4336
     * @param  int    $socid               Limit count on a particular third party id
4337
     * @param  string $mode                'byunit'=number of unit, 'bynumber'=nb of entities
4338
     * @param  int    $filteronproducttype 0=To filter on product only, 1=To filter on services only
4339
     * @param  int    $year                Year (0=last 12 month, -1=all years)
4340
     * @param  string $morefilter          More sql filters
4341
     * @return array                       Return integer <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
4342
     */
4343
    public function get_nb_ordersupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4344
    {
4345
		// phpcs:enable
4346
        global $conf, $user;
4347
4348
        $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4349
        if ($mode == 'bynumber') {
4350
            $sql .= ", count(DISTINCT c.rowid)";
4351
        }
4352
        $sql .= ", sum(d.total_ht) as total_ht";
4353
        $sql .= " FROM " . $this->db->prefix() . "commande_fournisseurdet as d, " . $this->db->prefix() . "commande_fournisseur as c, " . $this->db->prefix() . "societe as s";
4354
        if ($filteronproducttype >= 0) {
4355
            $sql .= ", " . $this->db->prefix() . "product as p";
4356
        }
4357
        if (!$user->hasRight('societe', 'client', 'voir')) {
4358
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
4359
        }
4360
        $sql .= " WHERE c.rowid = d.fk_commande";
4361
        if ($this->id > 0) {
4362
            $sql .= " AND d.fk_product = " . ((int) $this->id);
4363
        } else {
4364
            $sql .= " AND d.fk_product > 0";
4365
        }
4366
        if ($filteronproducttype >= 0) {
4367
            $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = " . ((int) $filteronproducttype);
4368
        }
4369
        $sql .= " AND c.fk_soc = s.rowid";
4370
        $sql .= " AND c.entity IN (" . getEntity('supplier_order') . ")";
4371
        if (!$user->hasRight('societe', 'client', 'voir')) {
4372
            $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int) $user->id);
4373
        }
4374
        if ($socid > 0) {
4375
            $sql .= " AND c.fk_soc = " . ((int) $socid);
4376
        }
4377
        $sql .= $morefilter;
4378
        $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4379
        $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4380
4381
        return $this->_get_stats($sql, $mode, $year);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_get_stats($sql, $mode, $year) also could return the type integer which is incompatible with the documented return type array.
Loading history...
4382
    }
4383
4384
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4385
    /**
4386
     *  Return nb of units in orders in which product is included
4387
     *
4388
     * @param  int    $socid               Limit count on a particular third party id
4389
     * @param  string $mode                'byunit'=number of unit, 'bynumber'=nb of entities
4390
     * @param  int    $filteronproducttype 0=To filter on product only, 1=To filter on services only
4391
     * @param  int    $year                Year (0=last 12 month, -1=all years)
4392
     * @param  string $morefilter          More sql filters
4393
     * @return array                       Return integer <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
4394
     */
4395
    public function get_nb_contract($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4396
    {
4397
		// phpcs:enable
4398
        global $conf, $user;
4399
4400
        $sql = "SELECT sum(d.qty) as qty, date_format(c.date_contrat, '%Y%m')";
4401
        if ($mode == 'bynumber') {
4402
            $sql .= ", count(DISTINCT c.rowid)";
4403
        }
4404
        $sql .= ", sum(d.total_ht) as total_ht";
4405
        $sql .= " FROM " . $this->db->prefix() . "contratdet as d, " . $this->db->prefix() . "contrat as c, " . $this->db->prefix() . "societe as s";
4406
        if ($filteronproducttype >= 0) {
4407
            $sql .= ", " . $this->db->prefix() . "product as p";
4408
        }
4409
        if (!$user->hasRight('societe', 'client', 'voir')) {
4410
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
4411
        }
4412
        $sql .= " WHERE c.entity IN (" . getEntity('contract') . ")";
4413
        $sql .= " AND c.rowid = d.fk_contrat";
4414
4415
        if ($this->id > 0) {
4416
            $sql .= " AND d.fk_product = " . ((int) $this->id);
4417
        } else {
4418
            $sql .= " AND d.fk_product > 0";
4419
        }
4420
        if ($filteronproducttype >= 0) {
4421
            $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = " . ((int) $filteronproducttype);
4422
        }
4423
        $sql .= " AND c.fk_soc = s.rowid";
4424
4425
        if (!$user->hasRight('societe', 'client', 'voir')) {
4426
            $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int) $user->id);
4427
        }
4428
        if ($socid > 0) {
4429
            $sql .= " AND c.fk_soc = " . ((int) $socid);
4430
        }
4431
        $sql .= $morefilter;
4432
        $sql .= " GROUP BY date_format(c.date_contrat,'%Y%m')";
4433
        $sql .= " ORDER BY date_format(c.date_contrat,'%Y%m') DESC";
4434
4435
        return $this->_get_stats($sql, $mode, $year);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_get_stats($sql, $mode, $year) also could return the type integer which is incompatible with the documented return type array.
Loading history...
4436
    }
4437
4438
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4439
    /**
4440
     *  Return nb of units in orders in which product is included
4441
     *
4442
     * @param  int    $socid               Limit count on a particular third party id
4443
     * @param  string $mode                'byunit'=number of unit, 'bynumber'=nb of entities
4444
     * @param  int    $filteronproducttype 0=To filter on product only, 1=To filter on services only
4445
     * @param  int    $year                Year (0=last 12 month, -1=all years)
4446
     * @param  string $morefilter          More sql filters
4447
     * @return array                       Return integer <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
4448
     */
4449
    public function get_nb_mos($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4450
    {
4451
		// phpcs:enable
4452
        global $conf, $user;
4453
4454
        $sql = "SELECT sum(d.qty), date_format(d.date_valid, '%Y%m')";
4455
        if ($mode == 'bynumber') {
4456
            $sql .= ", count(DISTINCT d.rowid)";
4457
        }
4458
        $sql .= " FROM " . $this->db->prefix() . "mrp_mo as d LEFT JOIN  " . $this->db->prefix() . "societe as s ON d.fk_soc = s.rowid";
4459
        if ($filteronproducttype >= 0) {
4460
            $sql .= ", " . $this->db->prefix() . "product as p";
4461
        }
4462
        if (!$user->hasRight('societe', 'client', 'voir')) {
4463
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
4464
        }
4465
4466
        $sql .= " WHERE d.entity IN (" . getEntity('mo') . ")";
4467
        $sql .= " AND d.status > 0";
4468
4469
        if ($this->id > 0) {
4470
            $sql .= " AND d.fk_product = " . ((int) $this->id);
4471
        } else {
4472
            $sql .= " AND d.fk_product > 0";
4473
        }
4474
        if ($filteronproducttype >= 0) {
4475
            $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = " . ((int) $filteronproducttype);
4476
        }
4477
4478
        if (!$user->hasRight('societe', 'client', 'voir')) {
4479
            $sql .= " AND d.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int) $user->id);
4480
        }
4481
        if ($socid > 0) {
4482
            $sql .= " AND d.fk_soc = " . ((int) $socid);
4483
        }
4484
        $sql .= $morefilter;
4485
        $sql .= " GROUP BY date_format(d.date_valid,'%Y%m')";
4486
        $sql .= " ORDER BY date_format(d.date_valid,'%Y%m') DESC";
4487
4488
        return $this->_get_stats($sql, $mode, $year);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_get_stats($sql, $mode, $year) also could return the type integer which is incompatible with the documented return type array.
Loading history...
4489
    }
4490
4491
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4492
    /**
4493
     *  Link a product/service to a parent product/service
4494
     *
4495
     * @param  int $id_pere Id of parent product/service
4496
     * @param  int $id_fils Id of child product/service
4497
     * @param  float $qty     Quantity
4498
     * @param  int $incdec  1=Increase/decrease stock of child when parent stock increase/decrease
4499
     * @param  int $notrigger   Disable triggers
4500
     * @return int                Return integer < 0 if KO, > 0 if OK
4501
     */
4502
    public function add_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
4503
    {
4504
        global $user;
4505
4506
		// phpcs:enable
4507
        // Clean parameters
4508
        if (!is_numeric($id_pere)) {
4509
            $id_pere = 0;
4510
        }
4511
        if (!is_numeric($id_fils)) {
4512
            $id_fils = 0;
4513
        }
4514
        if (!is_numeric($incdec)) {
4515
            $incdec = 0;
4516
        }
4517
4518
        $result = $this->del_sousproduit($id_pere, $id_fils);
4519
        if ($result < 0) {
4520
            return $result;
4521
        }
4522
4523
        // Check not already father of id_pere (to avoid father -> child -> father links)
4524
        $sql = "SELECT fk_product_pere from " . $this->db->prefix() . "product_association";
4525
        $sql .= " WHERE fk_product_pere = " . ((int) $id_fils) . " AND fk_product_fils = " . ((int) $id_pere);
4526
        if (!$this->db->query($sql)) {
4527
            dol_print_error($this->db);
4528
            return -1;
4529
        } else {
4530
            //Selection of the highest row
4531
            $sql = "SELECT MAX(rang) as max_rank FROM " . $this->db->prefix() . "product_association";
4532
            $sql .= " WHERE fk_product_pere  = " . ((int) $id_pere);
4533
            $resql = $this->db->query($sql);
4534
            if ($resql) {
4535
                $obj = $this->db->fetch_object($resql);
4536
                $rank = $obj->max_rank + 1;
4537
                //Addition of a product with the highest rank +1
4538
                $sql = "INSERT INTO " . $this->db->prefix() . "product_association(fk_product_pere,fk_product_fils,qty,incdec,rang)";
4539
                $sql .= " VALUES (" . ((int) $id_pere) . ", " . ((int) $id_fils) . ", " . price2num($qty, 'MS') . ", " . ((int) $incdec) . ", " . ((int) $rank) . ")";
4540
                if (! $this->db->query($sql)) {
4541
                    dol_print_error($this->db);
4542
                    return -1;
4543
                } else {
4544
                    if (!$notrigger) {
4545
                        // Call trigger
4546
                        $result = $this->call_trigger('PRODUCT_SUBPRODUCT_ADD', $user);
4547
                        if ($result < 0) {
4548
                            $this->error = $this->db->lasterror();
4549
                            dol_syslog(get_class($this) . '::addSubproduct error=' . $this->error, LOG_ERR);
4550
                            return -1;
4551
                        }
4552
                    }
4553
                    // End call triggers
4554
4555
                    return 1;
4556
                }
4557
            } else {
4558
                dol_print_error($this->db);
4559
                return -1;
4560
            }
4561
        }
4562
    }
4563
4564
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4565
    /**
4566
     *  Modify composed product
4567
     *
4568
     * @param  int $id_pere Id of parent product/service
4569
     * @param  int $id_fils Id of child product/service
4570
     * @param  float $qty     Quantity
4571
     * @param  int $incdec  1=Increase/decrease stock of child when parent stock increase/decrease
4572
     * @param  int $notrigger   Disable triggers
4573
     * @return int                Return integer < 0 if KO, > 0 if OK
4574
     */
4575
    public function update_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
4576
    {
4577
        global $user;
4578
4579
		// phpcs:enable
4580
        // Clean parameters
4581
        if (!is_numeric($id_pere)) {
4582
            $id_pere = 0;
4583
        }
4584
        if (!is_numeric($id_fils)) {
4585
            $id_fils = 0;
4586
        }
4587
        if (!is_numeric($incdec)) {
4588
            $incdec = 1;
4589
        }
4590
        if (!is_numeric($qty)) {
4591
            $qty = 1;
4592
        }
4593
4594
        $sql = 'UPDATE ' . $this->db->prefix() . 'product_association SET ';
4595
        $sql .= 'qty = ' . price2num($qty, 'MS');
4596
        $sql .= ',incdec = ' . ((int) $incdec);
4597
        $sql .= ' WHERE fk_product_pere = ' . ((int) $id_pere) . ' AND fk_product_fils = ' . ((int) $id_fils);
4598
4599
        if (!$this->db->query($sql)) {
4600
            dol_print_error($this->db);
4601
            return -1;
4602
        } else {
4603
            if (!$notrigger) {
4604
                // Call trigger
4605
                $result = $this->call_trigger('PRODUCT_SUBPRODUCT_UPDATE', $user);
4606
                if ($result < 0) {
4607
                    $this->error = $this->db->lasterror();
4608
                    dol_syslog(get_class($this) . '::updateSubproduct error=' . $this->error, LOG_ERR);
4609
                    return -1;
4610
                }
4611
                // End call triggers
4612
            }
4613
4614
            return 1;
4615
        }
4616
    }
4617
4618
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4619
    /**
4620
     *  Remove a link between a subproduct and a parent product/service
4621
     *
4622
     * @param  int $fk_parent Id of parent product (child will no more be linked to it)
4623
     * @param  int $fk_child  Id of child product
4624
     * @param  int $notrigger   Disable triggers
4625
     * @return int            Return integer < 0 if KO, > 0 if OK
4626
     */
4627
    public function del_sousproduit($fk_parent, $fk_child, $notrigger = 0)
4628
    {
4629
        global $user;
4630
4631
		// phpcs:enable
4632
        if (!is_numeric($fk_parent)) {
4633
            $fk_parent = 0;
4634
        }
4635
        if (!is_numeric($fk_child)) {
4636
            $fk_child = 0;
4637
        }
4638
4639
        $sql = "DELETE FROM " . $this->db->prefix() . "product_association";
4640
        $sql .= " WHERE fk_product_pere  = " . ((int) $fk_parent);
4641
        $sql .= " AND fk_product_fils = " . ((int) $fk_child);
4642
4643
        dol_syslog(get_class($this) . '::del_sousproduit', LOG_DEBUG);
4644
        if (!$this->db->query($sql)) {
4645
            dol_print_error($this->db);
4646
            return -1;
4647
        }
4648
4649
        // Updated ranks so that none are missing
4650
        $sqlrank = "SELECT rowid, rang FROM " . $this->db->prefix() . "product_association";
4651
        $sqlrank .= " WHERE fk_product_pere = " . ((int) $fk_parent);
4652
        $sqlrank .= " ORDER BY rang";
4653
        $resqlrank = $this->db->query($sqlrank);
4654
        if ($resqlrank) {
4655
            $cpt = 0;
4656
            while ($objrank = $this->db->fetch_object($resqlrank)) {
4657
                $cpt++;
4658
                $sql = "UPDATE " . $this->db->prefix() . "product_association";
4659
                $sql .= " SET rang = " . ((int) $cpt);
4660
                $sql .= " WHERE rowid = " . ((int) $objrank->rowid);
4661
                if (! $this->db->query($sql)) {
4662
                    dol_print_error($this->db);
4663
                    return -1;
4664
                }
4665
            }
4666
        }
4667
4668
        if (!$notrigger) {
4669
            // Call trigger
4670
            $result = $this->call_trigger('PRODUCT_SUBPRODUCT_DELETE', $user);
4671
            if ($result < 0) {
4672
                $this->error = $this->db->lasterror();
4673
                dol_syslog(get_class($this) . '::delSubproduct error=' . $this->error, LOG_ERR);
4674
                return -1;
4675
            }
4676
            // End call triggers
4677
        }
4678
4679
        return 1;
4680
    }
4681
4682
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4683
    /**
4684
     *  Check if it is a sub-product into a kit
4685
     *
4686
     * @param  int  $fk_parent      Id of parent kit product
4687
     * @param  int  $fk_child       Id of child product
4688
     * @return int                  Return 1 or 0; -1 if error
4689
     */
4690
    public function is_sousproduit($fk_parent, $fk_child)
4691
    {
4692
		// phpcs:enable
4693
        $sql = "SELECT fk_product_pere, qty, incdec";
4694
        $sql .= " FROM " . $this->db->prefix() . "product_association";
4695
        $sql .= " WHERE fk_product_pere  = " . ((int) $fk_parent);
4696
        $sql .= " AND fk_product_fils = " . ((int) $fk_child);
4697
4698
        $result = $this->db->query($sql);
4699
        if ($result) {
4700
            $num = $this->db->num_rows($result);
4701
4702
            if ($num > 0) {
4703
                $obj = $this->db->fetch_object($result);
4704
4705
                $this->is_sousproduit_qty = $obj->qty;
4706
                $this->is_sousproduit_incdec = $obj->incdec;
4707
4708
                return 1;
4709
            } else {
4710
                return 0;
4711
            }
4712
        } else {
4713
            dol_print_error($this->db);
4714
            return -1;
4715
        }
4716
    }
4717
4718
4719
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4720
    /**
4721
     *  Add a supplier price for the product.
4722
     *  Note: Duplicate ref is accepted for different quantity only, or for different companies.
4723
     *
4724
     * @param  User   $user      User that make link
4725
     * @param  int    $id_fourn  Supplier id
4726
     * @param  string $ref_fourn Supplier ref
4727
     * @param  float  $quantity  Quantity minimum for price
4728
     * @return int               Return integer < 0 if KO, 0 if link already exists for this product, > 0 if OK
4729
     */
4730
    public function add_fournisseur($user, $id_fourn, $ref_fourn, $quantity)
4731
    {
4732
		// phpcs:enable
4733
        global $conf;
4734
4735
        $now = dol_now();
4736
4737
        dol_syslog(get_class($this) . "::add_fournisseur id_fourn = " . $id_fourn . " ref_fourn=" . $ref_fourn . " quantity=" . $quantity, LOG_DEBUG);
4738
4739
        // Clean parameters
4740
        $quantity = price2num($quantity, 'MS');
4741
4742
        if ($ref_fourn) {
4743
            // Check if ref is not already used
4744
            $sql = "SELECT rowid, fk_product";
4745
            $sql .= " FROM " . $this->db->prefix() . "product_fournisseur_price";
4746
            $sql .= " WHERE fk_soc = " . ((int) $id_fourn);
4747
            $sql .= " AND ref_fourn = '" . $this->db->escape($ref_fourn) . "'";
4748
            $sql .= " AND fk_product <> " . ((int) $this->id);
4749
            $sql .= " AND entity IN (" . getEntity('productsupplierprice') . ")";
4750
4751
            $resql = $this->db->query($sql);
4752
            if ($resql) {
4753
                $obj = $this->db->fetch_object($resql);
4754
                if ($obj) {
4755
                    // If the supplier ref already exists but for another product (duplicate ref is accepted for different quantity only or different companies)
4756
                    $this->product_id_already_linked = $obj->fk_product;
4757
                    return -3;
4758
                }
4759
                $this->db->free($resql);
4760
            }
4761
        }
4762
4763
        $sql = "SELECT rowid";
4764
        $sql .= " FROM " . $this->db->prefix() . "product_fournisseur_price";
4765
        $sql .= " WHERE fk_soc = " . ((int) $id_fourn);
4766
        if ($ref_fourn) {
4767
            $sql .= " AND ref_fourn = '" . $this->db->escape($ref_fourn) . "'";
4768
        } else {
4769
            $sql .= " AND (ref_fourn = '' OR ref_fourn IS NULL)";
4770
        }
4771
        $sql .= " AND quantity = " . ((float) $quantity);
4772
        $sql .= " AND fk_product = " . ((int) $this->id);
4773
        $sql .= " AND entity IN (" . getEntity('productsupplierprice') . ")";
4774
4775
        $resql = $this->db->query($sql);
4776
        if ($resql) {
4777
            $obj = $this->db->fetch_object($resql);
4778
4779
            // The reference supplier does not exist, we create it for this product.
4780
            if (empty($obj)) {
4781
                $sql = "INSERT INTO " . $this->db->prefix() . "product_fournisseur_price(";
4782
                $sql .= "datec";
4783
                $sql .= ", entity";
4784
                $sql .= ", fk_product";
4785
                $sql .= ", fk_soc";
4786
                $sql .= ", ref_fourn";
4787
                $sql .= ", quantity";
4788
                $sql .= ", fk_user";
4789
                $sql .= ", tva_tx";
4790
                $sql .= ") VALUES (";
4791
                $sql .= "'" . $this->db->idate($now) . "'";
4792
                $sql .= ", " . ((int) $conf->entity);
4793
                $sql .= ", " . ((int) $this->id);
4794
                $sql .= ", " . ((int) $id_fourn);
4795
                $sql .= ", '" . $this->db->escape($ref_fourn) . "'";
4796
                $sql .= ", " . ((float) $quantity);
4797
                $sql .= ", " . ((int) $user->id);
4798
                $sql .= ", 0";
4799
                $sql .= ")";
4800
4801
                if ($this->db->query($sql)) {
4802
                    $this->product_fourn_price_id = $this->db->last_insert_id($this->db->prefix() . "product_fournisseur_price");
4803
                    return 1;
4804
                } else {
4805
                    $this->error = $this->db->lasterror();
4806
                    return -1;
4807
                }
4808
            } else {
4809
                // If the supplier price already exists for this product and quantity
4810
                $this->product_fourn_price_id = $obj->rowid;
4811
                return 0;
4812
            }
4813
        } else {
4814
            $this->error = $this->db->lasterror();
4815
            return -2;
4816
        }
4817
    }
4818
4819
4820
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4821
    /**
4822
     * Return list of suppliers providing the product or service
4823
     *
4824
     * @return array        Array of vendor ids
4825
     */
4826
    public function list_suppliers()
4827
    {
4828
		// phpcs:enable
4829
        global $conf;
4830
4831
        $list = array();
4832
4833
        $sql = "SELECT DISTINCT p.fk_soc";
4834
        $sql .= " FROM " . $this->db->prefix() . "product_fournisseur_price as p";
4835
        $sql .= " WHERE p.fk_product = " . ((int) $this->id);
4836
        $sql .= " AND p.entity = " . ((int) $conf->entity);
4837
4838
        $result = $this->db->query($sql);
4839
        if ($result) {
4840
            $num = $this->db->num_rows($result);
4841
            $i = 0;
4842
            while ($i < $num) {
4843
                $obj = $this->db->fetch_object($result);
4844
                $list[$i] = $obj->fk_soc;
4845
                $i++;
4846
            }
4847
        }
4848
4849
        return $list;
4850
    }
4851
4852
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4853
    /**
4854
     *  Recopie les prix d'un produit/service sur un autre
4855
     *
4856
     * @param  int $fromId Id product source
4857
     * @param  int $toId   Id product target
4858
     * @return int                     Return integer < 0 if KO, > 0 if OK
4859
     */
4860
    public function clone_price($fromId, $toId)
4861
    {
4862
        global $conf, $user;
4863
4864
        $now = dol_now();
4865
4866
        $this->db->begin();
4867
4868
        // prices
4869
        $sql  = "INSERT INTO " . $this->db->prefix() . "product_price (";
4870
        $sql .= " entity";
4871
        $sql .= ", fk_product";
4872
        $sql .= ", date_price";
4873
        $sql .= ", price_level";
4874
        $sql .= ", price";
4875
        $sql .= ", price_ttc";
4876
        $sql .= ", price_min";
4877
        $sql .= ", price_min_ttc";
4878
        $sql .= ", price_base_type";
4879
        $sql .= ", price_label";
4880
        $sql .= ", default_vat_code";
4881
        $sql .= ", tva_tx";
4882
        $sql .= ", recuperableonly";
4883
        $sql .= ", localtax1_tx";
4884
        $sql .= ", localtax1_type";
4885
        $sql .= ", localtax2_tx";
4886
        $sql .= ", localtax2_type";
4887
        $sql .= ", fk_user_author";
4888
        $sql .= ", tosell";
4889
        $sql .= ", price_by_qty";
4890
        $sql .= ", fk_price_expression";
4891
        $sql .= ", fk_multicurrency";
4892
        $sql .= ", multicurrency_code";
4893
        $sql .= ", multicurrency_tx";
4894
        $sql .= ", multicurrency_price";
4895
        $sql .= ", multicurrency_price_ttc";
4896
        $sql .= ")";
4897
        $sql .= " SELECT";
4898
        $sql .= " entity";
4899
        $sql .= ", " . $toId;
4900
        $sql .= ", '" . $this->db->idate($now) . "'";
4901
        $sql .= ", price_level";
4902
        $sql .= ", price";
4903
        $sql .= ", price_ttc";
4904
        $sql .= ", price_min";
4905
        $sql .= ", price_min_ttc";
4906
        $sql .= ", price_base_type";
4907
        $sql .= ", price_label";
4908
        $sql .= ", default_vat_code";
4909
        $sql .= ", tva_tx";
4910
        $sql .= ", recuperableonly";
4911
        $sql .= ", localtax1_tx";
4912
        $sql .= ", localtax1_type";
4913
        $sql .= ", localtax2_tx";
4914
        $sql .= ", localtax2_type";
4915
        $sql .= ", " . $user->id;
4916
        $sql .= ", tosell";
4917
        $sql .= ", price_by_qty";
4918
        $sql .= ", fk_price_expression";
4919
        $sql .= ", fk_multicurrency";
4920
        $sql .= ", multicurrency_code";
4921
        $sql .= ", multicurrency_tx";
4922
        $sql .= ", multicurrency_price";
4923
        $sql .= ", multicurrency_price_ttc";
4924
        $sql .= " FROM " . $this->db->prefix() . "product_price ps";
4925
        $sql .= " WHERE fk_product = " . ((int) $fromId);
4926
        $sql .= " AND date_price IN (SELECT MAX(pd.date_price) FROM " . $this->db->prefix() . "product_price pd WHERE pd.fk_product = " . ((int) $fromId) . " AND pd.price_level = ps.price_level)";
4927
        $sql .= " ORDER BY date_price DESC";
4928
4929
        dol_syslog(__METHOD__, LOG_DEBUG);
4930
        $resql = $this->db->query($sql);
4931
        if (!$resql) {
4932
            $this->db->rollback();
4933
            return -1;
4934
        }
4935
4936
        $this->db->commit();
4937
        return 1;
4938
    }
4939
4940
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4941
    /**
4942
     * Clone links between products
4943
     *
4944
     * @param  int $fromId Product id
4945
     * @param  int $toId   Product id
4946
     * @return int                  Return integer <0 if KO, >0 if OK
4947
     */
4948
    public function clone_associations($fromId, $toId)
4949
    {
4950
		// phpcs:enable
4951
        $this->db->begin();
4952
4953
        $sql = 'INSERT INTO ' . $this->db->prefix() . 'product_association (fk_product_pere, fk_product_fils, qty, incdec)';
4954
        $sql .= " SELECT " . $toId . ", fk_product_fils, qty, incdec FROM " . $this->db->prefix() . "product_association";
4955
        $sql .= " WHERE fk_product_pere = " . ((int) $fromId);
4956
4957
        dol_syslog(get_class($this) . '::clone_association', LOG_DEBUG);
4958
        if (!$this->db->query($sql)) {
4959
            $this->db->rollback();
4960
            return -1;
4961
        }
4962
4963
        $this->db->commit();
4964
        return 1;
4965
    }
4966
4967
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4968
    /**
4969
     *  Recopie les fournisseurs et prix fournisseurs d'un produit/service sur un autre
4970
     *
4971
     * @param  int $fromId Id produit source
4972
     * @param  int $toId   Id produit cible
4973
     * @return int                 Return integer < 0 si erreur, > 0 si ok
4974
     */
4975
    public function clone_fournisseurs($fromId, $toId)
4976
    {
4977
		// phpcs:enable
4978
        $this->db->begin();
4979
4980
        $now = dol_now();
4981
4982
        // les fournisseurs
4983
        /*$sql = "INSERT ".$this->db->prefix()."product_fournisseur ("
4984
         . " datec, fk_product, fk_soc, ref_fourn, fk_user_author )"
4985
         . " SELECT '".$this->db->idate($now)."', ".$toId.", fk_soc, ref_fourn, fk_user_author"
4986
         . " FROM ".$this->db->prefix()."product_fournisseur"
4987
         . " WHERE fk_product = ".((int) $fromId);
4988
4989
         if ( ! $this->db->query($sql ) )
4990
         {
4991
         $this->db->rollback();
4992
         return -1;
4993
         }*/
4994
4995
        // les prix de fournisseurs.
4996
        $sql = "INSERT " . $this->db->prefix() . "product_fournisseur_price (";
4997
        $sql .= " datec, fk_product, fk_soc, price, quantity, fk_user, tva_tx)";
4998
        $sql .= " SELECT '" . $this->db->idate($now) . "', " . ((int) $toId) . ", fk_soc, price, quantity, fk_user, tva_tx";
4999
        $sql .= " FROM " . $this->db->prefix() . "product_fournisseur_price";
5000
        $sql .= " WHERE fk_product = " . ((int) $fromId);
5001
5002
        dol_syslog(get_class($this) . '::clone_fournisseurs', LOG_DEBUG);
5003
        $resql = $this->db->query($sql);
5004
        if (!$resql) {
5005
            $this->db->rollback();
5006
            return -1;
5007
        } else {
5008
            $this->db->commit();
5009
            return 1;
5010
        }
5011
    }
5012
5013
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5014
    /**
5015
     *  Function recursive, used only by get_arbo_each_prod(), to build tree of subproducts into ->res
5016
     *  Define value of this->res
5017
     *
5018
     * @param  array  $prod                 Products array
5019
     * @param  string $compl_path           Directory path of parents to add before
5020
     * @param  int    $multiply             Because each sublevel must be multiplicated by parent nb
5021
     * @param  int    $level                Init level
5022
     * @param  int    $id_parent            Id parent
5023
     * @param  int    $ignore_stock_load    Ignore stock load
5024
     * @return void
5025
     */
5026
    public function fetch_prod_arbo($prod, $compl_path = '', $multiply = 1, $level = 1, $id_parent = 0, $ignore_stock_load = 0)
5027
    {
5028
		// phpcs:enable
5029
        global $conf, $langs;
5030
5031
        $tmpproduct = null;
5032
        //var_dump($prod);
5033
        foreach ($prod as $id_product => $desc_pere) {    // $id_product is 0 (first call starting with root top) or an id of a sub_product
5034
            if (is_array($desc_pere)) {    // If desc_pere is an array, this means it's a child
5035
                $id = (!empty($desc_pere[0]) ? $desc_pere[0] : '');
5036
                $nb = (!empty($desc_pere[1]) ? $desc_pere[1] : '');
5037
                $type = (!empty($desc_pere[2]) ? $desc_pere[2] : '');
5038
                $label = (!empty($desc_pere[3]) ? $desc_pere[3] : '');
5039
                $incdec = (!empty($desc_pere[4]) ? $desc_pere[4] : 0);
5040
5041
                if ($multiply < 1) {
5042
                    $multiply = 1;
5043
                }
5044
5045
                //print "XXX We add id=".$id." - label=".$label." - nb=".$nb." - multiply=".$multiply." fullpath=".$compl_path.$label."\n";
5046
                if (is_null($tmpproduct)) {
5047
                    $tmpproduct = new Product($this->db); // So we initialize tmpproduct only once for all loop.
5048
                }
5049
                $tmpproduct->fetch($id); // Load product to get ->ref
5050
5051
                if (empty($ignore_stock_load) && ($tmpproduct->isProduct() || getDolGlobalString('STOCK_SUPPORTS_SERVICES'))) {
5052
                    $tmpproduct->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
5053
                }
5054
5055
                $this->res[] = array(
5056
                    'id' => $id, // Id product
5057
                    'id_parent' => $id_parent,
5058
                    'ref' => $tmpproduct->ref, // Ref product
5059
                    'nb' => $nb, // Nb of units that compose parent product
5060
                    'nb_total' => $nb * $multiply, // Nb of units for all nb of product
5061
                    'stock' => $tmpproduct->stock_reel, // Stock
5062
                    'stock_alert' => $tmpproduct->seuil_stock_alerte, // Stock alert
5063
                    'label' => $label,
5064
                    'fullpath' => $compl_path . $label, // Label
5065
                    'type' => $type, // Nb of units that compose parent product
5066
                    'desiredstock' => $tmpproduct->desiredstock,
5067
                    'level' => $level,
5068
                    'incdec' => $incdec,
5069
                    'entity' => $tmpproduct->entity
5070
                );
5071
5072
                // Recursive call if there child has children of its own
5073
                if (isset($desc_pere['childs']) && is_array($desc_pere['childs'])) {
5074
                    //print 'YYY We go down for '.$desc_pere[3]." -> \n";
5075
                    $this->fetch_prod_arbo($desc_pere['childs'], $compl_path . $desc_pere[3] . " -> ", $desc_pere[1] * $multiply, $level + 1, $id, $ignore_stock_load);
5076
                }
5077
            }
5078
        }
5079
    }
5080
5081
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5082
    /**
5083
     *  Build the tree of subproducts and return it.
5084
     *  this->sousprods must have been loaded by this->get_sousproduits_arbo()
5085
     *
5086
     * @param  int      $multiply           Because each sublevel must be multiplicated by parent nb
5087
     * @param  int      $ignore_stock_load  Ignore stock load
5088
     * @return array                        Array with tree
5089
     */
5090
    public function get_arbo_each_prod($multiply = 1, $ignore_stock_load = 0)
5091
    {
5092
		// phpcs:enable
5093
        $this->res = array();
5094
        if (isset($this->sousprods) && is_array($this->sousprods)) {
5095
            foreach ($this->sousprods as $prod_name => $desc_product) {
5096
                if (is_array($desc_product)) {
5097
                    $this->fetch_prod_arbo($desc_product, "", $multiply, 1, $this->id, $ignore_stock_load); // This set $this->res
5098
                }
5099
            }
5100
        }
5101
        //var_dump($res);
5102
        return $this->res;
5103
    }
5104
5105
    /**
5106
     * Count all parent and children products for current product (first level only)
5107
     *
5108
     * @param   int     $mode   0=Both parent and child, -1=Parents only, 1=Children only
5109
     * @return  int             Nb of father + child
5110
     * @see getFather(), get_sousproduits_arbo()
5111
     */
5112
    public function hasFatherOrChild($mode = 0)
5113
    {
5114
        $nb = 0;
5115
5116
        $sql = "SELECT COUNT(pa.rowid) as nb";
5117
        $sql .= " FROM " . $this->db->prefix() . "product_association as pa";
5118
        if ($mode == 0) {
5119
            $sql .= " WHERE pa.fk_product_fils = " . ((int) $this->id) . " OR pa.fk_product_pere = " . ((int) $this->id);
5120
        } elseif ($mode == -1) {
5121
            $sql .= " WHERE pa.fk_product_fils = " . ((int) $this->id); // We are a child, so we found lines that link to parents (can have several parents)
5122
        } elseif ($mode == 1) {
5123
            $sql .= " WHERE pa.fk_product_pere = " . ((int) $this->id); // We are a parent, so we found lines that link to children (can have several children)
5124
        }
5125
5126
        $resql = $this->db->query($sql);
5127
        if ($resql) {
5128
            $obj = $this->db->fetch_object($resql);
5129
            if ($obj) {
5130
                $nb = $obj->nb;
5131
            }
5132
        } else {
5133
            return -1;
5134
        }
5135
5136
        return $nb;
5137
    }
5138
5139
    /**
5140
     * Return if a product has variants or not
5141
     *
5142
     * @return int        Number of variants
5143
     */
5144
    public function hasVariants()
5145
    {
5146
        $nb = 0;
5147
        $sql = "SELECT count(rowid) as nb FROM " . $this->db->prefix() . "product_attribute_combination WHERE fk_product_parent = " . ((int) $this->id);
5148
        $sql .= " AND entity IN (" . getEntity('product') . ")";
5149
5150
        $resql = $this->db->query($sql);
5151
        if ($resql) {
5152
            $obj = $this->db->fetch_object($resql);
5153
            if ($obj) {
5154
                $nb = $obj->nb;
5155
            }
5156
        }
5157
5158
        return $nb;
5159
    }
5160
5161
5162
    /**
5163
     * Return if loaded product is a variant
5164
     *
5165
     * @return bool|int     Return true if the product is a variant, false if not, -1 if error
5166
     */
5167
    public function isVariant()
5168
    {
5169
        global $conf;
5170
        if (isModEnabled('variants')) {
5171
            $sql = "SELECT rowid FROM " . $this->db->prefix() . "product_attribute_combination WHERE fk_product_child = " . ((int) $this->id) . " AND entity IN (" . getEntity('product') . ")";
5172
5173
            $query = $this->db->query($sql);
5174
5175
            if ($query) {
5176
                if (!$this->db->num_rows($query)) {
5177
                    return false;
5178
                }
5179
                return true;
5180
            } else {
5181
                dol_print_error($this->db);
5182
                return -1;
5183
            }
5184
        } else {
5185
            return false;
5186
        }
5187
    }
5188
5189
    /**
5190
     *  Return all parent products for current product (first level only)
5191
     *
5192
     * @return array|int         Array of product
5193
     * @see hasFatherOrChild()
5194
     */
5195
    public function getFather()
5196
    {
5197
        $sql = "SELECT p.rowid, p.label as label, p.ref as ref, pa.fk_product_pere as id, p.fk_product_type, pa.qty, pa.incdec, p.entity";
5198
        $sql .= ", p.tosell as status, p.tobuy as status_buy";
5199
        $sql .= " FROM " . $this->db->prefix() . "product_association as pa,";
5200
        $sql .= " " . $this->db->prefix() . "product as p";
5201
        $sql .= " WHERE p.rowid = pa.fk_product_pere";
5202
        $sql .= " AND pa.fk_product_fils = " . ((int) $this->id);
5203
5204
        $res = $this->db->query($sql);
5205
        if ($res) {
5206
            $prods = array();
5207
            while ($record = $this->db->fetch_array($res)) {
5208
                // $record['id'] = $record['rowid'] = id of father
5209
                $prods[$record['id']]['id'] = $record['rowid'];
5210
                $prods[$record['id']]['ref'] = $record['ref'];
5211
                $prods[$record['id']]['label'] = $record['label'];
5212
                $prods[$record['id']]['qty'] = $record['qty'];
5213
                $prods[$record['id']]['incdec'] = $record['incdec'];
5214
                $prods[$record['id']]['fk_product_type'] = $record['fk_product_type'];
5215
                $prods[$record['id']]['entity'] = $record['entity'];
5216
                $prods[$record['id']]['status'] = $record['status'];
5217
                $prods[$record['id']]['status_buy'] = $record['status_buy'];
5218
            }
5219
            return $prods;
5220
        } else {
5221
            dol_print_error($this->db);
5222
            return -1;
5223
        }
5224
    }
5225
5226
5227
    /**
5228
     *  Return children of product $id
5229
     *
5230
     * @param  int $id                  Id of product to search children of
5231
     * @param  int $firstlevelonly      Return only direct child
5232
     * @param  int $level               Level of recursing call (start to 1)
5233
     * @param  array $parents           Array of all parents of $id
5234
     * @return array|int                    Return array(prodid=>array(0=prodid, 1=>qty, 2=>product type, 3=>label, 4=>incdec, 5=>product ref)
5235
     */
5236
    public function getChildsArbo($id, $firstlevelonly = 0, $level = 1, $parents = array())
5237
    {
5238
        global $alreadyfound;
5239
5240
        if (empty($id)) {
5241
            return array();
5242
        }
5243
5244
        $sql = "SELECT p.rowid, p.ref, p.label as label, p.fk_product_type,";
5245
        $sql .= " pa.qty as qty, pa.fk_product_fils as id, pa.incdec,";
5246
        $sql .= " pa.rowid as fk_association, pa.rang";
5247
        $sql .= " FROM " . $this->db->prefix() . "product as p,";
5248
        $sql .= " " . $this->db->prefix() . "product_association as pa";
5249
        $sql .= " WHERE p.rowid = pa.fk_product_fils";
5250
        $sql .= " AND pa.fk_product_pere = " . ((int) $id);
5251
        $sql .= " AND pa.fk_product_fils <> " . ((int) $id); // This should not happens, it is to avoid infinite loop if it happens
5252
        $sql .= " ORDER BY pa.rang";
5253
5254
        dol_syslog(get_class($this) . '::getChildsArbo id=' . $id . ' level=' . $level . ' parents=' . (is_array($parents) ? implode(',', $parents) : $parents), LOG_DEBUG);
5255
5256
        if ($level == 1) {
5257
            $alreadyfound = array($id => 1); // We init array of found object to start of tree, so if we found it later (should not happened), we stop immediately
5258
        }
5259
        // Protection against infinite loop
5260
        if ($level > 30) {
5261
            return array();
5262
        }
5263
5264
        $res = $this->db->query($sql);
5265
        if ($res) {
5266
            $prods = array();
5267
            while ($rec = $this->db->fetch_array($res)) {
5268
                if (!empty($alreadyfound[$rec['rowid']])) {
5269
                    dol_syslog(get_class($this) . '::getChildsArbo the product id=' . $rec['rowid'] . ' was already found at a higher level in tree. We discard to avoid infinite loop', LOG_WARNING);
5270
                    if (in_array($rec['id'], $parents)) {
5271
                        continue; // We discard this child if it is already found at a higher level in tree in the same branch.
5272
                    }
5273
                }
5274
                $alreadyfound[$rec['rowid']] = 1;
5275
                $prods[$rec['rowid']] = array(
5276
                    0 => $rec['rowid'],
5277
                    1 => $rec['qty'],
5278
                    2 => $rec['fk_product_type'],
5279
                    3 => $this->db->escape($rec['label']),
5280
                    4 => $rec['incdec'],
5281
                    5 => $rec['ref'],
5282
                    6 => $rec['fk_association'],
5283
                    7 => $rec['rang']
5284
                );
5285
                //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty'],2=>$rec['fk_product_type']);
5286
                //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty']);
5287
                if (empty($firstlevelonly)) {
5288
                    $parents[] = $rec['rowid'];
5289
                    $listofchilds = $this->getChildsArbo($rec['rowid'], 0, $level + 1, $parents);
5290
                    foreach ($listofchilds as $keyChild => $valueChild) {
5291
                        $prods[$rec['rowid']]['childs'][$keyChild] = $valueChild;
5292
                    }
5293
                }
5294
            }
5295
5296
            return $prods;
5297
        } else {
5298
            dol_print_error($this->db);
5299
            return -1;
5300
        }
5301
    }
5302
5303
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5304
    /**
5305
     *     Return tree of all subproducts for product. Tree contains array of array(0=prodid, 1=>qty, 2=>product type, 3=>label, 4=>incdec, 5=>product ref)
5306
     *     Set this->sousprods
5307
     *
5308
     * @return void
5309
     */
5310
    public function get_sousproduits_arbo()
5311
    {
5312
		// phpcs:enable
5313
        $parent = array();
5314
5315
        foreach ($this->getChildsArbo($this->id) as $keyChild => $valueChild) {    // Warning. getChildsArbo can call getChildsArbo recursively. Starting point is $value[0]=id of product
5316
            $parent[$this->label][$keyChild] = $valueChild;
5317
        }
5318
        foreach ($parent as $key => $value) {        // key=label, value is array of children
5319
            $this->sousprods[$key] = $value;
5320
        }
5321
    }
5322
5323
    /**
5324
     * getTooltipContentArray
5325
     * @param array $params params to construct tooltip data
5326
     * @since v18
5327
     * @return array
5328
     */
5329
    public function getTooltipContentArray($params)
5330
    {
5331
        global $conf, $langs, $user;
5332
5333
        $langs->loadLangs(array('products', 'other'));
5334
5335
        $datas = array();
5336
        $nofetch = !empty($params['nofetch']);
5337
5338
        if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
5339
            return ['optimize' => $langs->trans("ShowProduct")];
5340
        }
5341
5342
        if (!empty($this->entity)) {
5343
            $tmpphoto = $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 80, 0, 0, 0, 0, 1);
5344
            if ($this->nbphoto > 0) {
5345
                $datas['photo'] = '<div class="photointooltip floatright">' . "\n" . $tmpphoto . '</div>';
5346
            }
5347
        }
5348
5349
        if ($this->type == Product::TYPE_PRODUCT) {
5350
            $datas['picto'] = img_picto('', 'product') . ' <u class="paddingrightonly">' . $langs->trans("Product") . '</u>';
5351
        } elseif ($this->type == Product::TYPE_SERVICE) {
5352
            $datas['picto'] = img_picto('', 'service') . ' <u class="paddingrightonly">' . $langs->trans("Service") . '</u>';
5353
        }
5354
        if (isset($this->status) && isset($this->status_buy)) {
5355
            $datas['status'] = ' ' . $this->getLibStatut(5, 0) . ' ' . $this->getLibStatut(5, 1);
5356
        }
5357
5358
        if (!empty($this->ref)) {
5359
            $datas['ref'] = '<br><b>' . $langs->trans('ProductRef') . ':</b> ' . $this->ref;
5360
        }
5361
        if (!empty($this->label)) {
5362
            $datas['label'] = '<br><b>' . $langs->trans('ProductLabel') . ':</b> ' . $this->label;
5363
        }
5364
        if (!empty($this->description)) {
5365
            $datas['description'] = '<br><b>' . $langs->trans('ProductDescription') . ':</b> ' . dolGetFirstLineOfText($this->description, 5);
5366
        }
5367
        if ($this->type == Product::TYPE_PRODUCT || getDolGlobalString('STOCK_SUPPORTS_SERVICES')) {
5368
            if (isModEnabled('productbatch')) {
5369
                $langs->load("productbatch");
5370
                $datas['batchstatus'] = "<br><b>" . $langs->trans("ManageLotSerial") . '</b>: ' . $this->getLibStatut(0, 2);
5371
            }
5372
        }
5373
        if (isModEnabled('barcode')) {
5374
            $datas['barcode'] = '<br><b>' . $langs->trans('BarCode') . ':</b> ' . $this->barcode;
5375
        }
5376
5377
        if ($this->type == Product::TYPE_PRODUCT) {
5378
            if ($this->weight) {
5379
                $datas['weight'] = "<br><b>" . $langs->trans("Weight") . '</b>: ' . $this->weight . ' ' . measuringUnitString(0, "weight", $this->weight_units);
5380
            }
5381
            $labelsize = "";
5382
            if ($this->length) {
5383
                $labelsize .= ($labelsize ? " - " : "") . "<b>" . $langs->trans("Length") . '</b>: ' . $this->length . ' ' . measuringUnitString(0, 'size', $this->length_units);
5384
            }
5385
            if ($this->width) {
5386
                $labelsize .= ($labelsize ? " - " : "") . "<b>" . $langs->trans("Width") . '</b>: ' . $this->width . ' ' . measuringUnitString(0, 'size', $this->width_units);
5387
            }
5388
            if ($this->height) {
5389
                $labelsize .= ($labelsize ? " - " : "") . "<b>" . $langs->trans("Height") . '</b>: ' . $this->height . ' ' . measuringUnitString(0, 'size', $this->height_units);
5390
            }
5391
            if ($labelsize) {
5392
                $datas['size'] = "<br>" . $labelsize;
5393
            }
5394
5395
            $labelsurfacevolume = "";
5396
            if ($this->surface) {
5397
                $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "") . "<b>" . $langs->trans("Surface") . '</b>: ' . $this->surface . ' ' . measuringUnitString(0, 'surface', $this->surface_units);
5398
            }
5399
            if ($this->volume) {
5400
                $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "") . "<b>" . $langs->trans("Volume") . '</b>: ' . $this->volume . ' ' . measuringUnitString(0, 'volume', $this->volume_units);
5401
            }
5402
            if ($labelsurfacevolume) {
5403
                $datas['surface'] = "<br>" . $labelsurfacevolume;
5404
            }
5405
        }
5406
        if ($this->type == Product::TYPE_SERVICE && !empty($this->duration_value)) {
5407
            // Duration
5408
            $datas['duration'] = '<br><b>' . $langs->trans("Duration") . ':</b> ' . $this->duration_value;
5409
            if ($this->duration_value > 1) {
5410
                $dur = array("i" => $langs->trans("Minutes"), "h" => $langs->trans("Hours"), "d" => $langs->trans("Days"), "w" => $langs->trans("Weeks"), "m" => $langs->trans("Months"), "y" => $langs->trans("Years"));
5411
            } elseif ($this->duration_value > 0) {
5412
                $dur = array("i" => $langs->trans("Minute"), "h" => $langs->trans("Hour"), "d" => $langs->trans("Day"), "w" => $langs->trans("Week"), "m" => $langs->trans("Month"), "y" => $langs->trans("Year"));
5413
            }
5414
            $datas['duration'] .= (!empty($this->duration_unit) && isset($dur[$this->duration_unit]) ? "&nbsp;" . $langs->trans($dur[$this->duration_unit]) : '');
5415
        }
5416
        if (empty($user->socid)) {
5417
            if (!empty($this->pmp) && $this->pmp) {
5418
                $datas['pmp'] = "<br><b>" . $langs->trans("PMPValue") . '</b>: ' . price($this->pmp, 0, '', 1, -1, -1, $conf->currency);
5419
            }
5420
5421
            if (isModEnabled('accounting')) {
5422
                if ($this->status && isset($this->accountancy_code_sell)) {
5423
                    include_once DOL_DOCUMENT_ROOT . '/core/lib/accounting.lib.php';
5424
                    $selllabel = '<br>';
5425
                    $selllabel .= '<br><b>' . $langs->trans('ProductAccountancySellCode') . ':</b> ' . length_accountg($this->accountancy_code_sell);
5426
                    $selllabel .= '<br><b>' . $langs->trans('ProductAccountancySellIntraCode') . ':</b> ' . length_accountg($this->accountancy_code_sell_intra);
5427
                    $selllabel .= '<br><b>' . $langs->trans('ProductAccountancySellExportCode') . ':</b> ' . length_accountg($this->accountancy_code_sell_export);
5428
                    $datas['accountancysell'] = $selllabel;
5429
                }
5430
                if ($this->status_buy && isset($this->accountancy_code_buy)) {
5431
                    include_once DOL_DOCUMENT_ROOT . '/core/lib/accounting.lib.php';
5432
                    $buylabel = '';
5433
                    if (empty($this->status)) {
5434
                        $buylabel .= '<br>';
5435
                    }
5436
                    $buylabel .= '<br><b>' . $langs->trans('ProductAccountancyBuyCode') . ':</b> ' . length_accountg($this->accountancy_code_buy);
5437
                    $buylabel .= '<br><b>' . $langs->trans('ProductAccountancyBuyIntraCode') . ':</b> ' . length_accountg($this->accountancy_code_buy_intra);
5438
                    $buylabel .= '<br><b>' . $langs->trans('ProductAccountancyBuyExportCode') . ':</b> ' . length_accountg($this->accountancy_code_buy_export);
5439
                    $datas['accountancybuy'] = $buylabel;
5440
                }
5441
            }
5442
        }
5443
        // show categories for this record only in ajax to not overload lists
5444
        if (isModEnabled('category') && !$nofetch) {
5445
            require_once constant('DOL_DOCUMENT_ROOT') . '/categories/class/categorie.class.php';
5446
            $form = new Form($this->db);
5447
            $datas['categories'] = '<br>' . $form->showCategories($this->id, Categorie::TYPE_PRODUCT, 1);
5448
        }
5449
5450
        return $datas;
5451
    }
5452
5453
    /**
5454
     *    Return clickable link of object (with eventually picto)
5455
     *
5456
     * @param   int     $withpicto              Add picto into link
5457
     * @param   string  $option                 Where point the link ('stock', 'composition', 'category', 'supplier', '')
5458
     * @param   int     $maxlength              Maxlength of ref
5459
     * @param   int     $save_lastsearch_value  -1=Auto, 0=No save of lastsearch_values when clicking, 1=Save lastsearch_values when clicking
5460
     * @param   int     $notooltip              No tooltip
5461
     * @param   string  $morecss                ''=Add more css on link
5462
     * @param   int     $add_label              0=Default, 1=Add label into string, >1=Add first chars into string
5463
     * @param   string  $sep                    ' - '=Separator between ref and label if option 'add_label' is set
5464
     * @return  string                          String with URL
5465
     */
5466
    public function getNomUrl($withpicto = 0, $option = '', $maxlength = 0, $save_lastsearch_value = -1, $notooltip = 0, $morecss = '', $add_label = 0, $sep = ' - ')
5467
    {
5468
        global $conf, $langs, $hookmanager, $user;
5469
        include_once DOL_DOCUMENT_ROOT . '/core/lib/product.lib.php';
5470
5471
        $result = '';
5472
5473
        $newref = $this->ref;
5474
        if ($maxlength) {
5475
            $newref = dol_trunc($newref, $maxlength, 'middle');
5476
        }
5477
        $params = [
5478
            'id' => $this->id,
5479
            'objecttype' => (isset($this->type) ? ($this->type == 1 ? 'service' : 'product') : $this->element),
5480
            'option' => $option,
5481
            'nofetch' => 1,
5482
        ];
5483
        $classfortooltip = 'classfortooltip';
5484
        $dataparams = '';
5485
        if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
5486
            $classfortooltip = 'classforajaxtooltip';
5487
            $dataparams = ' data-params="' . dol_escape_htmltag(json_encode($params)) . '"';
5488
            $label = '';
5489
        } else {
5490
            $label = implode($this->getTooltipContentArray($params));
5491
        }
5492
5493
        $linkclose = '';
5494
        if (empty($notooltip)) {
5495
            if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
5496
                $label = $langs->trans("ShowProduct");
5497
                $linkclose .= ' alt="' . dol_escape_htmltag($label, 1, 1) . '"';
5498
            }
5499
            $linkclose .= ($label ? ' title="' . dol_escape_htmltag($label, 1, 1) . '"' : ' title="tocomplete"');
5500
            $linkclose .= $dataparams . ' class="nowraponall ' . $classfortooltip . ($morecss ? ' ' . $morecss : '') . '"';
5501
        } else {
5502
            $linkclose = ' class="nowraponall' . ($morecss ? ' ' . $morecss : '') . '"';
5503
        }
5504
5505
        if ($option == 'supplier' || $option == 'category') {
5506
            $url = constant('BASE_URL') . '/product/price_suppliers.php?id=' . $this->id;
5507
        } elseif ($option == 'stock') {
5508
            $url = constant('BASE_URL') . '/product/stock/product.php?id=' . $this->id;
5509
        } elseif ($option == 'composition') {
5510
            $url = constant('BASE_URL') . '/product/composition/card.php?id=' . $this->id;
5511
        } else {
5512
            $url = constant('BASE_URL') . '/product/card.php?id=' . $this->id;
5513
        }
5514
5515
        if ($option !== 'nolink') {
5516
            // Add param to save lastsearch_values or not
5517
            $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
5518
            if ($save_lastsearch_value == -1 && isset($_SERVER["PHP_SELF"]) && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
5519
                $add_save_lastsearch_values = 1;
5520
            }
5521
            if ($add_save_lastsearch_values) {
5522
                $url .= '&save_lastsearch_values=1';
5523
            }
5524
        }
5525
5526
        $linkstart = '<a href="' . $url . '"';
5527
        $linkstart .= $linkclose . '>';
5528
        $linkend = '</a>';
5529
5530
        $result .= $linkstart;
5531
        if ($withpicto) {
5532
            if ($this->type == Product::TYPE_PRODUCT) {
5533
                $result .= (img_object(($notooltip ? '' : $label), 'product', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
5534
            }
5535
            if ($this->type == Product::TYPE_SERVICE) {
5536
                $result .= (img_object(($notooltip ? '' : $label), 'service', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
5537
            }
5538
        }
5539
        $result .= '<span class="aaa">' . dol_escape_htmltag($newref) . '</span>';
5540
        $result .= $linkend;
5541
        if ($withpicto != 2) {
5542
            $result .= (($add_label && $this->label) ? $sep . dol_trunc($this->label, ($add_label > 1 ? $add_label : 0)) : '');
5543
        }
5544
5545
        global $action;
5546
        $hookmanager->initHooks(array('productdao'));
5547
        $parameters = array('id' => $this->id, 'getnomurl' => &$result, 'label' => &$label);
5548
        $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
5549
        if ($reshook > 0) {
5550
            $result = $hookmanager->resPrint;
5551
        } else {
5552
            $result .= $hookmanager->resPrint;
5553
        }
5554
5555
        return $result;
5556
    }
5557
5558
5559
    /**
5560
     *  Create a document onto disk according to template module.
5561
     *
5562
     * @param  string    $modele      Force model to use ('' to not force)
5563
     * @param  Translate $outputlangs Object langs to use for output
5564
     * @param  int       $hidedetails Hide details of lines
5565
     * @param  int       $hidedesc    Hide description
5566
     * @param  int       $hideref     Hide ref
5567
     * @return int                         0 if KO, 1 if OK
5568
     */
5569
    public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0)
5570
    {
5571
        global $conf, $user, $langs;
5572
5573
        $langs->load("products");
5574
        $outputlangs->load("products");
5575
5576
        // Positionne le modele sur le nom du modele a utiliser
5577
        if (!dol_strlen($modele)) {
5578
            $modele = getDolGlobalString('PRODUCT_ADDON_PDF', 'strato');
5579
        }
5580
5581
        $modelpath = "core/modules/product/doc/";
5582
5583
        return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref);
5584
    }
5585
5586
    /**
5587
     *    Return label of status of object
5588
     *
5589
     * @param  int $mode 0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=Short label + Picto
5590
     * @param  int $type 0=Sell, 1=Buy, 2=Batch Number management
5591
     * @return string          Label of status
5592
     */
5593
    public function getLibStatut($mode = 0, $type = 0)
5594
    {
5595
        switch ($type) {
5596
            case 0:
5597
                return $this->LibStatut($this->status, $mode, $type);
5598
            case 1:
5599
                return $this->LibStatut($this->status_buy, $mode, $type);
5600
            case 2:
5601
                return $this->LibStatut($this->status_batch, $mode, $type);
5602
            default:
5603
                //Simulate previous behavior but should return an error string
5604
                return $this->LibStatut($this->status_buy, $mode, $type);
5605
        }
5606
    }
5607
5608
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5609
    /**
5610
     *    Return label of a given status
5611
     *
5612
     * @param  int      $status     Statut
5613
     * @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
5614
     * @param  int      $type       0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
5615
     * @return string               Label of status
5616
     */
5617
    public function LibStatut($status, $mode = 0, $type = 0)
5618
    {
5619
		// phpcs:enable
5620
        global $conf, $langs;
5621
5622
        $labelStatus = $labelStatusShort = '';
5623
5624
        $langs->load('products');
5625
        if (isModEnabled('productbatch')) {
5626
            $langs->load("productbatch");
5627
        }
5628
5629
        if ($type == 2) {
5630
            switch ($mode) {
5631
                case 0:
5632
                    $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatch') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial')));
5633
                    return dolGetStatus($label);
5634
                case 1:
5635
                    $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort')));
5636
                    return dolGetStatus($label);
5637
                case 2:
5638
                    return $this->LibStatut($status, 3, 2) . ' ' . $this->LibStatut($status, 1, 2);
5639
                case 3:
5640
                    return dolGetStatus($langs->transnoentitiesnoconv('ProductStatusNotOnBatch'), '', '', empty($status) ? 'status5' : 'status4', 3, 'dot');
5641
                case 4:
5642
                    return $this->LibStatut($status, 3, 2) . ' ' . $this->LibStatut($status, 0, 2);
5643
                case 5:
5644
                    return $this->LibStatut($status, 1, 2) . ' ' . $this->LibStatut($status, 3, 2);
5645
                default:
5646
                    return dolGetStatus($langs->transnoentitiesnoconv('Unknown'));
5647
            }
5648
        }
5649
5650
        $statuttrans = empty($status) ? 'status5' : 'status4';
5651
5652
        if ($status == 0) {
5653
            // $type   0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
5654
            if ($type == 0) {
5655
                $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnSellShort');
5656
                $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnSell');
5657
            } elseif ($type == 1) {
5658
                $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBuyShort');
5659
                $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBuy');
5660
            } elseif ($type == 2) {
5661
                $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBatch');
5662
                $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort');
5663
            }
5664
        } elseif ($status == 1) {
5665
            // $type   0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
5666
            if ($type == 0) {
5667
                $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSellShort');
5668
                $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSell');
5669
            } elseif ($type == 1) {
5670
                $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnBuyShort');
5671
                $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnBuy');
5672
            } elseif ($type == 2) {
5673
                $labelStatus = ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial'));
5674
                $labelStatusShort = ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort'));
5675
            }
5676
        } elseif ($type == 2 && $status == 2) {
5677
            $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSerial');
5678
            $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSerialShort');
5679
        }
5680
5681
        if ($mode > 6) {
5682
            return dolGetStatus($langs->transnoentitiesnoconv('Unknown'), '', '', 'status0', 0);
5683
        } else {
5684
            return dolGetStatus($labelStatus, $labelStatusShort, '', $statuttrans, $mode);
5685
        }
5686
    }
5687
5688
5689
    /**
5690
     *  Retour label of nature of product
5691
     *
5692
     * @return string|int        Return label or ''. -1 if error
5693
     */
5694
    public function getLibFinished()
5695
    {
5696
        global $langs;
5697
        $langs->load('products');
5698
        $label = '';
5699
5700
        if (isset($this->finished) && $this->finished >= 0) {
5701
            $sql = "SELECT label, code FROM " . $this->db->prefix() . "c_product_nature where code = " . ((int) $this->finished) . " AND active=1";
5702
            $resql = $this->db->query($sql);
5703
            if (!$resql) {
5704
                $this->error = $this->db->error() . ' sql=' . $sql;
5705
                dol_syslog(__METHOD__ . ' Error ' . $this->error, LOG_ERR);
5706
                return -1;
5707
            } elseif ($this->db->num_rows($resql) > 0 && $res = $this->db->fetch_array($resql)) {
5708
                $label = $langs->trans($res['label']);
5709
            }
5710
            $this->db->free($resql);
5711
        }
5712
5713
        return $label;
5714
    }
5715
5716
5717
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5718
    /**
5719
     *  Adjust stock in a warehouse for product
5720
     *
5721
     * @param  User   $user           user asking change
5722
     * @param  int    $id_entrepot    id of warehouse
5723
     * @param  double $nbpiece        nb of units (should be always positive, use $movement to decide if we add or remove)
5724
     * @param  int    $movement       0 = add, 1 = remove
5725
     * @param  string $label          Label of stock movement
5726
     * @param  double $price          Unit price HT of product, used to calculate average weighted price (PMP in french). If 0, average weighted price is not changed.
5727
     * @param  string $inventorycode  Inventory code
5728
     * @param  string $origin_element Origin element type
5729
     * @param  int    $origin_id      Origin id of element
5730
     * @param  int    $disablestockchangeforsubproduct  Disable stock change for sub-products of kit (useful only if product is a subproduct)
5731
     * @param  Extrafields $extrafields   Array of extrafields
5732
     * @return int                    Return integer <0 if KO, >0 if OK
5733
     */
5734
    public function correct_stock($user, $id_entrepot, $nbpiece, $movement, $label = '', $price = 0, $inventorycode = '', $origin_element = '', $origin_id = null, $disablestockchangeforsubproduct = 0, $extrafields = null)
5735
    {
5736
		// phpcs:enable
5737
        if ($id_entrepot) {
5738
            $this->db->begin();
5739
5740
            include_once DOL_DOCUMENT_ROOT . '/product/stock/class/mouvementstock.class.php';
5741
5742
            if ($nbpiece < 0) {
5743
                if (!$movement) {
5744
                    $movement = 1;
5745
                }
5746
                $nbpiece = abs($nbpiece);
5747
            }
5748
            $op = array();
5749
            $op[0] = "+" . trim((string) $nbpiece);
5750
            $op[1] = "-" . trim((string) $nbpiece);
5751
5752
            $movementstock = new MouvementStock($this->db);
5753
            $movementstock->setOrigin($origin_element, $origin_id); // Set ->origin_type and ->origin_id
5754
            $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', '', '', '', false, 0, $disablestockchangeforsubproduct);
5755
5756
            if ($result >= 0) {
5757
                if ($extrafields) {
5758
                    $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
5759
                    $movementstock->array_options = $array_options;
5760
                    $movementstock->insertExtraFields();
5761
                }
5762
                $this->db->commit();
5763
                return 1;
5764
            } else {
5765
                $this->error = $movementstock->error;
5766
                $this->errors = $movementstock->errors;
5767
5768
                $this->db->rollback();
5769
                return -1;
5770
            }
5771
        }
5772
5773
        return -1;
5774
    }
5775
5776
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5777
    /**
5778
     *  Adjust stock in a warehouse for product with batch number
5779
     *
5780
     * @param  User         $user           user asking change
5781
     * @param  int          $id_entrepot    id of warehouse
5782
     * @param  double       $nbpiece        nb of units (should be always positive, use $movement to decide if we add or remove)
5783
     * @param  int          $movement       0 = add, 1 = remove
5784
     * @param  string       $label          Label of stock movement
5785
     * @param  double       $price          Price to use for stock eval
5786
     * @param  int|string   $dlc            eat-by date
5787
     * @param  int|string   $dluo           sell-by date
5788
     * @param  string       $lot            Lot number
5789
     * @param  string       $inventorycode  Inventory code
5790
     * @param  string       $origin_element Origin element type
5791
     * @param  int          $origin_id      Origin id of element
5792
     * @param  int          $disablestockchangeforsubproduct    Disable stock change for sub-products of kit (useful only if product is a subproduct)
5793
     * @param  Extrafields  $extrafields    Array of extrafields
5794
     * @param  boolean      $force_update_batch   Force update batch
5795
     * @return int                      Return integer <0 if KO, >0 if OK
5796
     */
5797
    public function correct_stock_batch($user, $id_entrepot, $nbpiece, $movement, $label = '', $price = 0, $dlc = '', $dluo = '', $lot = '', $inventorycode = '', $origin_element = '', $origin_id = null, $disablestockchangeforsubproduct = 0, $extrafields = null, $force_update_batch = false)
5798
    {
5799
		// phpcs:enable
5800
        if ($id_entrepot) {
5801
            $this->db->begin();
5802
5803
            include_once DOL_DOCUMENT_ROOT . '/product/stock/class/mouvementstock.class.php';
5804
5805
            if ($nbpiece < 0) {
5806
                if (!$movement) {
5807
                    $movement = 1;
5808
                }
5809
                $nbpiece = abs($nbpiece);
5810
            }
5811
5812
            $op = array();
5813
            $op[0] = "+" . trim((string) $nbpiece);
5814
            $op[1] = "-" . trim((string) $nbpiece);
5815
5816
            $movementstock = new MouvementStock($this->db);
5817
            $movementstock->setOrigin($origin_element, $origin_id); // Set ->origin_type and ->fk_origin
5818
            $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', $dlc, $dluo, $lot, false, 0, $disablestockchangeforsubproduct, 0, $force_update_batch);
5819
5820
            if ($result >= 0) {
5821
                if ($extrafields) {
5822
                    $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
5823
                    $movementstock->array_options = $array_options;
5824
                    $movementstock->insertExtraFields();
5825
                }
5826
                $this->db->commit();
5827
                return 1;
5828
            } else {
5829
                $this->error = $movementstock->error;
5830
                $this->errors = $movementstock->errors;
5831
5832
                $this->db->rollback();
5833
                return -1;
5834
            }
5835
        }
5836
        return -1;
5837
    }
5838
5839
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5840
    /**
5841
     * Load information about stock of a product into ->stock_reel, ->stock_warehouse[] (including stock_warehouse[idwarehouse]->detail_batch for batch products)
5842
     * This function need a lot of load. If you use it on list, use a cache to execute it once for each product id.
5843
     * If ENTREPOT_EXTRA_STATUS is set, filtering on warehouse status is possible.
5844
     *
5845
     * @param   string  $option                     '' = Load all stock info, also from closed and internal warehouses, 'nobatch' = do not load batch detail, 'novirtual' = do no load virtual detail
5846
     *                                              You can also filter on 'warehouseclosed', 'warehouseopen', 'warehouseinternal'
5847
     * @param   int     $includedraftpoforvirtual   Include draft status of PO for virtual stock calculation
5848
     * @param   int     $dateofvirtualstock         Date of virtual stock
5849
     * @return  int                                 Return integer < 0 if KO, > 0 if OK
5850
     * @see     load_virtual_stock(), loadBatchInfo()
5851
     */
5852
    public function load_stock($option = '', $includedraftpoforvirtual = null, $dateofvirtualstock = null)
5853
    {
5854
		// phpcs:enable
5855
        global $conf;
5856
5857
        $this->stock_reel = 0;
5858
        $this->stock_warehouse = array();
5859
        $this->stock_theorique = 0;
5860
5861
        // Set filter on warehouse status
5862
        $warehouseStatus = array();
5863
        if (preg_match('/warehouseclosed/', $option)) {
5864
            $warehouseStatus[Entrepot::STATUS_CLOSED] = Entrepot::STATUS_CLOSED;
5865
        }
5866
        if (preg_match('/warehouseopen/', $option)) {
5867
            $warehouseStatus[Entrepot::STATUS_OPEN_ALL] = Entrepot::STATUS_OPEN_ALL;
5868
        }
5869
        if (preg_match('/warehouseinternal/', $option)) {
5870
            if (getDolGlobalString('ENTREPOT_EXTRA_STATUS')) {
5871
                $warehouseStatus[Entrepot::STATUS_OPEN_INTERNAL] = Entrepot::STATUS_OPEN_INTERNAL;
5872
            } else {
5873
                $warehouseStatus[Entrepot::STATUS_OPEN_ALL] = Entrepot::STATUS_OPEN_ALL;
5874
            }
5875
        }
5876
5877
        $sql = "SELECT ps.rowid, ps.reel, ps.fk_entrepot";
5878
        $sql .= " FROM " . $this->db->prefix() . "product_stock as ps";
5879
        $sql .= ", " . $this->db->prefix() . "entrepot as w";
5880
        $sql .= " WHERE w.entity IN (" . getEntity('stock') . ")";
5881
        $sql .= " AND w.rowid = ps.fk_entrepot";
5882
        $sql .= " AND ps.fk_product = " . ((int) $this->id);
5883
        if (count($warehouseStatus)) {
5884
            $sql .= " AND w.statut IN (" . $this->db->sanitize(implode(',', $warehouseStatus)) . ")";
5885
        }
5886
5887
        $sql .= " ORDER BY ps.reel " . (getDolGlobalString('DO_NOT_TRY_TO_DEFRAGMENT_STOCKS_WAREHOUSE') ? 'DESC' : 'ASC'); // Note : qty ASC is important for expedition card, to avoid stock fragmentation;
5888
5889
        dol_syslog(get_class($this) . "::load_stock", LOG_DEBUG);
5890
        $result = $this->db->query($sql);
5891
        if ($result) {
5892
            $num = $this->db->num_rows($result);
5893
            $i = 0;
5894
            if ($num > 0) {
5895
                while ($i < $num) {
5896
                    $row = $this->db->fetch_object($result);
5897
                    $this->stock_warehouse[$row->fk_entrepot] = new stdClass();
0 ignored issues
show
Bug introduced by
The type Dolibarr\Code\Product\Classes\stdClass was not found. Did you mean stdClass? If so, make sure to prefix the type with \.
Loading history...
5898
                    $this->stock_warehouse[$row->fk_entrepot]->real = $row->reel;
5899
                    $this->stock_warehouse[$row->fk_entrepot]->id = $row->rowid;
5900
                    if ((!preg_match('/nobatch/', $option)) && $this->hasbatch()) {
5901
                        $this->stock_warehouse[$row->fk_entrepot]->detail_batch = Productbatch::findAll($this->db, $row->rowid, 1, $this->id);
5902
                    }
5903
                    $this->stock_reel += $row->reel;
5904
                    $i++;
5905
                }
5906
            }
5907
            $this->db->free($result);
5908
5909
            if (!preg_match('/novirtual/', $option)) {
5910
                $this->load_virtual_stock($includedraftpoforvirtual, $dateofvirtualstock); // This load stock_theorique and also load all arrays stats_xxx...
5911
            }
5912
5913
            return 1;
5914
        } else {
5915
            $this->error = $this->db->lasterror();
5916
            return -1;
5917
        }
5918
    }
5919
5920
5921
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5922
    /**
5923
     *  Load value ->stock_theorique of a product. Property this->id must be defined.
5924
     *  This function need a lot of load. If you use it on list, use a cache to execute it one for each product id.
5925
     *
5926
     *  @param  int     $includedraftpoforvirtual   Include draft status and not yet approved Purchase Orders for virtual stock calculation
5927
     *  @param  int     $dateofvirtualstock         Date of virtual stock
5928
     *  @return int                                 Return integer < 0 if KO, > 0 if OK
5929
     *  @see    load_stock(), loadBatchInfo()
5930
     */
5931
    public function load_virtual_stock($includedraftpoforvirtual = null, $dateofvirtualstock = null)
5932
    {
5933
		// phpcs:enable
5934
        global $conf, $hookmanager, $action;
5935
5936
        $stock_commande_client = 0;
5937
        $stock_commande_fournisseur = 0;
5938
        $stock_sending_client = 0;
5939
        $stock_reception_fournisseur = 0;
5940
        $stock_inproduction = 0;
5941
5942
        //dol_syslog("load_virtual_stock");
5943
5944
        if (isModEnabled('order')) {
5945
            $result = $this->load_stats_commande(0, '1,2', 1);
5946
            if ($result < 0) {
5947
                dol_print_error($this->db, $this->error);
5948
            }
5949
            $stock_commande_client = $this->stats_commande['qty'];
5950
        }
5951
        if (isModEnabled("shipping")) {
5952
            require_once constant('DOL_DOCUMENT_ROOT') . '/expedition/class/expedition.class.php';
5953
            $filterShipmentStatus = '';
5954
            if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT')) {
5955
                $filterShipmentStatus = Expedition::STATUS_VALIDATED . ',' . Expedition::STATUS_CLOSED;
5956
            } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
5957
                $filterShipmentStatus = Expedition::STATUS_CLOSED;
5958
            }
5959
            $result = $this->load_stats_sending(0, '1,2', 1, $filterShipmentStatus);
5960
            if ($result < 0) {
5961
                dol_print_error($this->db, $this->error);
5962
            }
5963
            $stock_sending_client = $this->stats_expedition['qty'];
5964
        }
5965
        if (isModEnabled("supplier_order")) {
5966
            $filterStatus = getDolGlobalString('SUPPLIER_ORDER_STATUS_FOR_VIRTUAL_STOCK', '3,4');
5967
            if (isset($includedraftpoforvirtual)) {
5968
                $filterStatus = '0,1,2,' . $filterStatus; // 1,2 may have already been inside $filterStatus but it is better to have twice than missing $filterStatus does not include them
5969
            }
5970
            $result = $this->load_stats_commande_fournisseur(0, $filterStatus, 1, $dateofvirtualstock);
5971
            if ($result < 0) {
5972
                dol_print_error($this->db, $this->error);
5973
            }
5974
            $stock_commande_fournisseur = $this->stats_commande_fournisseur['qty'];
5975
        }
5976
        if ((isModEnabled("supplier_order") || isModEnabled("supplier_invoice")) && empty($conf->reception->enabled)) {
5977
            // Case module reception is not used
5978
            $filterStatus = '4';
5979
            if (isset($includedraftpoforvirtual)) {
5980
                $filterStatus = '0,' . $filterStatus;
5981
            }
5982
            $result = $this->load_stats_reception(0, $filterStatus, 1, $dateofvirtualstock);
5983
            if ($result < 0) {
5984
                dol_print_error($this->db, $this->error);
5985
            }
5986
            $stock_reception_fournisseur = $this->stats_reception['qty'];
5987
        }
5988
        if ((isModEnabled("supplier_order") || isModEnabled("supplier_invoice")) && isModEnabled("reception")) {
5989
            // Case module reception is used
5990
            $filterStatus = '4';
5991
            if (isset($includedraftpoforvirtual)) {
5992
                $filterStatus = '0,' . $filterStatus;
5993
            }
5994
            $result = $this->load_stats_reception(0, $filterStatus, 1, $dateofvirtualstock); // Use same tables than when module reception is not used.
5995
            if ($result < 0) {
5996
                dol_print_error($this->db, $this->error);
5997
            }
5998
            $stock_reception_fournisseur = $this->stats_reception['qty'];
5999
        }
6000
        if (isModEnabled('mrp')) {
6001
            $result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock);
6002
            if ($result < 0) {
6003
                dol_print_error($this->db, $this->error);
6004
            }
6005
            $stock_inproduction = $this->stats_mrptoproduce['qty'] - $this->stats_mrptoconsume['qty'];
6006
        }
6007
6008
        $this->stock_theorique = $this->stock_reel + $stock_inproduction;
6009
6010
        // Stock decrease mode
6011
        if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') || getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
6012
            $this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
6013
        } elseif (getDolGlobalString('STOCK_CALCULATE_ON_VALIDATE_ORDER')) {
6014
            $this->stock_theorique += 0;
6015
        } elseif (getDolGlobalString('STOCK_CALCULATE_ON_BILL')) {
6016
            $this->stock_theorique -= $stock_commande_client;
6017
        }
6018
        // Stock Increase mode
6019
        if (getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION') || getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION_CLOSE')) {
6020
            $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6021
        } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_DISPATCH_ORDER')) {
6022
            $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6023
        } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_VALIDATE_ORDER')) {
6024
            $this->stock_theorique -= $stock_reception_fournisseur;
6025
        } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_BILL')) {
6026
            $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6027
        }
6028
6029
        $parameters = array('id' => $this->id, 'includedraftpoforvirtual' => $includedraftpoforvirtual);
6030
        // Note that $action and $object may have been modified by some hooks
6031
        $reshook = $hookmanager->executeHooks('loadvirtualstock', $parameters, $this, $action);
6032
        if ($reshook > 0) {
6033
            $this->stock_theorique = $hookmanager->resArray['stock_theorique'];
6034
        } elseif ($reshook == 0 && isset($hookmanager->resArray['stock_stats_hook'])) {
6035
            $this->stock_theorique += $hookmanager->resArray['stock_stats_hook'];
6036
        }
6037
6038
        //Virtual Stock by Warehouse
6039
        if (!empty($this->stock_warehouse) && getDolGlobalString('STOCK_ALLOW_VIRTUAL_STOCK_PER_WAREHOUSE')) {
6040
            foreach ($this->stock_warehouse as $warehouseid => $stockwarehouse) {
6041
                if (isModEnabled('mrp')) {
6042
                    $result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock, $warehouseid);
6043
                    if ($result < 0) {
6044
                        dol_print_error($this->db, $this->error);
6045
                    }
6046
                }
6047
6048
                if ($this->fk_default_warehouse == $warehouseid) {
6049
                    $this->stock_warehouse[$warehouseid]->virtual = $this->stock_warehouse[$warehouseid]->real + $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] + $this->stats_commande_fournisseur['qty'] - ($this->stats_commande['qty'] + $this->stats_mrptoconsume['qty']);
6050
                } else {
6051
                    $this->stock_warehouse[$warehouseid]->virtual = $this->stock_warehouse[$warehouseid]->real + $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'];
6052
                }
6053
            }
6054
        }
6055
6056
        return 1;
6057
    }
6058
6059
6060
    /**
6061
     *  Load existing information about a serial
6062
     *
6063
     * @param  string $batch Lot/serial number
6064
     * @return array                    Array with record into product_batch
6065
     * @see    load_stock(), load_virtual_stock()
6066
     */
6067
    public function loadBatchInfo($batch)
6068
    {
6069
        $result = array();
6070
6071
        $sql = "SELECT pb.batch, pb.eatby, pb.sellby, SUM(pb.qty) AS qty FROM " . $this->db->prefix() . "product_batch as pb, " . $this->db->prefix() . "product_stock as ps";
6072
        $sql .= " WHERE pb.fk_product_stock = ps.rowid AND ps.fk_product = " . ((int) $this->id) . " AND pb.batch = '" . $this->db->escape($batch) . "'";
6073
        $sql .= " GROUP BY pb.batch, pb.eatby, pb.sellby";
6074
        dol_syslog(get_class($this) . "::loadBatchInfo load first entry found for lot/serial = " . $batch, LOG_DEBUG);
6075
        $resql = $this->db->query($sql);
6076
        if ($resql) {
6077
            $num = $this->db->num_rows($resql);
6078
            $i = 0;
6079
            while ($i < $num) {
6080
                $obj = $this->db->fetch_object($resql);
6081
                $result[] = array('batch' => $batch, 'eatby' => $this->db->jdate($obj->eatby), 'sellby' => $this->db->jdate($obj->sellby), 'qty' => $obj->qty);
6082
                $i++;
6083
            }
6084
            return $result;
6085
        } else {
6086
            dol_print_error($this->db);
6087
            $this->db->rollback();
6088
            return array();
6089
        }
6090
    }
6091
6092
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6093
    /**
6094
     *  Move an uploaded file described into $file array into target directory $sdir.
6095
     *
6096
     * @param  string $sdir Target directory
6097
     * @param  string $file Array of file info of file to upload: array('name'=>..., 'tmp_name'=>...)
6098
     * @return int                    Return integer <0 if KO, >0 if OK
6099
     */
6100
    public function add_photo($sdir, $file)
6101
    {
6102
		// phpcs:enable
6103
        global $conf;
6104
6105
        include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
6106
6107
        $result = 0;
6108
6109
        $dir = $sdir;
6110
        if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
6111
            $dir .= '/' . get_exdir($this->id, 2, 0, 0, $this, 'product') . $this->id . "/photos";
6112
        } else {
6113
            $dir .= '/' . get_exdir(0, 0, 0, 0, $this, 'product') . dol_sanitizeFileName($this->ref);
6114
        }
6115
6116
        dol_mkdir($dir);
6117
6118
        $dir_osencoded = $dir;
6119
6120
        if (is_dir($dir_osencoded)) {
6121
            $originImage = $dir . '/' . $file['name'];
6122
6123
            // Cree fichier en taille origine
6124
            $result = dol_move_uploaded_file($file['tmp_name'], $originImage, 1);
6125
6126
            if (file_exists(dol_osencode($originImage))) {
6127
                // Create thumbs
6128
                $this->addThumbs($originImage);
6129
            }
6130
        }
6131
6132
        if (is_numeric($result) && $result > 0) {
6133
            return 1;
6134
        } else {
6135
            return -1;
6136
        }
6137
    }
6138
6139
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6140
    /**
6141
     *  Return if at least one photo is available
6142
     *
6143
     * @param  string $sdir Directory to scan
6144
     * @return boolean                 True if at least one photo is available, False if not
6145
     */
6146
    public function is_photo_available($sdir)
6147
    {
6148
		// phpcs:enable
6149
        include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
6150
        include_once DOL_DOCUMENT_ROOT . '/core/lib/images.lib.php';
6151
6152
        global $conf;
6153
6154
        $dir = $sdir;
6155
        if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
6156
            $dir .= '/' . get_exdir($this->id, 2, 0, 0, $this, 'product') . $this->id . "/photos/";
6157
        } else {
6158
            $dir .= '/' . get_exdir(0, 0, 0, 0, $this, 'product');
6159
        }
6160
6161
        $nbphoto = 0;
6162
6163
        $dir_osencoded = dol_osencode($dir);
6164
        if (file_exists($dir_osencoded)) {
6165
            $handle = opendir($dir_osencoded);
6166
            if (is_resource($handle)) {
6167
                while (($file = readdir($handle)) !== false) {
6168
                    if (!utf8_check($file)) {
6169
                        $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // To be sure data is stored in UTF8 in memory
6170
                    }
6171
                    if (dol_is_file($dir . $file) && image_format_supported($file) >= 0) {
6172
                        return true;
6173
                    }
6174
                }
6175
            }
6176
        }
6177
        return false;
6178
    }
6179
6180
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6181
    /**
6182
     * Return an array with all photos of product found on disk. There is no sorting criteria.
6183
     *
6184
     * @param  string $dir      Directory to scan
6185
     * @param  int    $nbmax    Number maximum of photos (0=no maximum)
6186
     * @return array            Array of photos
6187
     */
6188
    public function liste_photos($dir, $nbmax = 0)
6189
    {
6190
		// phpcs:enable
6191
        include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
6192
        include_once DOL_DOCUMENT_ROOT . '/core/lib/images.lib.php';
6193
6194
        $nbphoto = 0;
6195
        $tabobj = array();
6196
6197
        $dir_osencoded = dol_osencode($dir);
6198
        $handle = @opendir($dir_osencoded);
6199
        if (is_resource($handle)) {
6200
            while (($file = readdir($handle)) !== false) {
6201
                if (!utf8_check($file)) {
6202
                    $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // readdir returns ISO
6203
                }
6204
                if (dol_is_file($dir . $file) && image_format_supported($file) >= 0) {
6205
                    $nbphoto++;
6206
6207
                    // We forge name of thumb.
6208
                    $photo = $file;
6209
                    $photo_vignette = '';
6210
                    $regs = array();
6211
                    if (preg_match('/(' . $this->regeximgext . ')$/i', $photo, $regs)) {
6212
                        $photo_vignette = preg_replace('/' . $regs[0] . '/i', '', $photo) . '_small' . $regs[0];
6213
                    }
6214
6215
                    $dirthumb = $dir . 'thumbs/';
6216
6217
                    // Object
6218
                    $obj = array();
6219
                    $obj['photo'] = $photo;
6220
                    if ($photo_vignette && dol_is_file($dirthumb . $photo_vignette)) {
6221
                        $obj['photo_vignette'] = 'thumbs/' . $photo_vignette;
6222
                    } else {
6223
                        $obj['photo_vignette'] = "";
6224
                    }
6225
6226
                    $tabobj[$nbphoto - 1] = $obj;
6227
6228
                    // Do we have to continue with next photo ?
6229
                    if ($nbmax && $nbphoto >= $nbmax) {
6230
                        break;
6231
                    }
6232
                }
6233
            }
6234
6235
            closedir($handle);
6236
        }
6237
6238
        return $tabobj;
6239
    }
6240
6241
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6242
    /**
6243
     *  Delete a photo and its thumbs
6244
     *
6245
     * @param  string $file     Path to image file
6246
     * @return void
6247
     */
6248
    public function delete_photo($file)
6249
    {
6250
		// phpcs:enable
6251
        include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
6252
        include_once DOL_DOCUMENT_ROOT . '/core/lib/images.lib.php';
6253
6254
        $dir = dirname($file) . '/'; // Chemin du dossier contenant l'image d'origine
6255
        $dirthumb = $dir . '/thumbs/'; // Chemin du dossier contenant la vignette
6256
        $filename = preg_replace('/' . preg_quote($dir, '/') . '/i', '', $file); // Nom du fichier
6257
6258
        // On efface l'image d'origine
6259
        dol_delete_file($file, 0, 0, 0, $this); // For triggers
6260
6261
        // Si elle existe, on efface la vignette
6262
        if (preg_match('/(' . $this->regeximgext . ')$/i', $filename, $regs)) {
6263
            $photo_vignette = preg_replace('/' . $regs[0] . '/i', '', $filename) . '_small' . $regs[0];
6264
            if (file_exists(dol_osencode($dirthumb . $photo_vignette))) {
6265
                dol_delete_file($dirthumb . $photo_vignette);
6266
            }
6267
6268
            $photo_vignette = preg_replace('/' . $regs[0] . '/i', '', $filename) . '_mini' . $regs[0];
6269
            if (file_exists(dol_osencode($dirthumb . $photo_vignette))) {
6270
                dol_delete_file($dirthumb . $photo_vignette);
6271
            }
6272
        }
6273
    }
6274
6275
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6276
    /**
6277
     *  Load size of image file
6278
     *
6279
     * @param  string $file Path to file
6280
     * @return void
6281
     */
6282
    public function get_image_size($file)
6283
    {
6284
		// phpcs:enable
6285
        $file_osencoded = dol_osencode($file);
6286
        $infoImg = getimagesize($file_osencoded); // Get information on image
6287
        $this->imgWidth = $infoImg[0]; // Largeur de l'image
6288
        $this->imgHeight = $infoImg[1]; // Hauteur de l'image
6289
    }
6290
6291
    /**
6292
     *  Load indicators this->nb for the dashboard
6293
     *
6294
     * @return int                 Return integer <0 if KO, >0 if OK
6295
     */
6296
    public function loadStateBoard()
6297
    {
6298
        global $hookmanager;
6299
6300
        $this->nb = array();
6301
6302
        $sql = "SELECT count(p.rowid) as nb, fk_product_type";
6303
        $sql .= " FROM " . $this->db->prefix() . "product as p";
6304
        $sql .= ' WHERE p.entity IN (' . getEntity($this->element, 1) . ')';
6305
        // Add where from hooks
6306
        if (is_object($hookmanager)) {
6307
            $parameters = array();
6308
            $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
6309
            $sql .= $hookmanager->resPrint;
6310
        }
6311
        $sql .= ' GROUP BY fk_product_type';
6312
6313
        $resql = $this->db->query($sql);
6314
        if ($resql) {
6315
            while ($obj = $this->db->fetch_object($resql)) {
6316
                if ($obj->fk_product_type == 1) {
6317
                    $this->nb["services"] = $obj->nb;
6318
                } else {
6319
                    $this->nb["products"] = $obj->nb;
6320
                }
6321
            }
6322
            $this->db->free($resql);
6323
            return 1;
6324
        } else {
6325
            dol_print_error($this->db);
6326
            $this->error = $this->db->error();
6327
            return -1;
6328
        }
6329
    }
6330
6331
    /**
6332
     * Return if object is a product.
6333
     *
6334
     * @return boolean     True if it's a product
6335
     */
6336
    public function isProduct()
6337
    {
6338
        return ($this->type == Product::TYPE_PRODUCT ? true : false);
6339
    }
6340
6341
    /**
6342
     * Return if object is a product
6343
     *
6344
     * @return boolean     True if it's a service
6345
     */
6346
    public function isService()
6347
    {
6348
        return ($this->type == Product::TYPE_SERVICE ? true : false);
6349
    }
6350
6351
    /**
6352
     * Return if object need to have its stock managed
6353
     *
6354
     * @return boolean     True if it's a service
6355
     */
6356
    public function isStockManaged()
6357
    {
6358
        return ($this->isProduct() || getDolGlobalString('STOCK_SUPPORTS_SERVICES'));
6359
    }
6360
6361
    /**
6362
     * Return if  object have a constraint on mandatory_period
6363
     *
6364
     * @return boolean     True if mandatory_period set to 1
6365
     */
6366
    public function isMandatoryPeriod()
6367
    {
6368
        return ($this->mandatory_period == 1 ? true : false);
6369
    }
6370
6371
    /**
6372
     * Return if object has a sell-by date or eat-by date
6373
     *
6374
     * @return boolean     True if it's has
6375
     */
6376
    public function hasbatch()
6377
    {
6378
        return ($this->status_batch > 0 ? true : false);
6379
    }
6380
6381
6382
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6383
    /**
6384
     *  Get a barcode from the module to generate barcode values.
6385
     *  Return value is stored into this->barcode
6386
     *
6387
     * @param  Product $object Object product or service
6388
     * @param  string  $type   Barcode type (ean, isbn, ...)
6389
     * @return string
6390
     */
6391
    public function get_barcode($object, $type = '')
6392
    {
6393
		// phpcs:enable
6394
        global $conf;
6395
6396
        $result = '';
6397
        if (getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
6398
            $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
6399
            foreach ($dirsociete as $dirroot) {
6400
                $res = dol_include_once($dirroot . getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM') . '.php');
6401
                if ($res) {
6402
                    break;
6403
                }
6404
            }
6405
            $var = getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM');
6406
            $mod = new $var();
6407
            '@phan-var-force ModeleNumRefBarCode $module';
6408
6409
            $result = $mod->getNextValue($object, $type);
6410
6411
            dol_syslog(get_class($this) . "::get_barcode barcode=" . $result . " module=" . $var);
6412
        }
6413
        return $result;
6414
    }
6415
6416
    /**
6417
     *  Initialise an instance with random values.
6418
     *  Used to build previews or test instances.
6419
     *    id must be 0 if object instance is a specimen.
6420
     *
6421
     * @return int
6422
     */
6423
    public function initAsSpecimen()
6424
    {
6425
        $now = dol_now();
6426
6427
        // Initialize parameters
6428
        $this->specimen = 1;
6429
        $this->id = 0;
6430
        $this->ref = 'PRODUCT_SPEC';
6431
        $this->label = 'PRODUCT SPECIMEN';
6432
        $this->description = 'This is description of this product specimen that was created the ' . dol_print_date($now, 'dayhourlog') . '.';
6433
        $this->specimen = 1;
6434
        $this->country_id = 1;
6435
        $this->status = 1;
6436
        $this->status_buy = 1;
6437
        $this->tobatch = 0;
6438
        $this->sell_or_eat_by_mandatory = 0;
6439
        $this->note_private = 'This is a comment (private)';
6440
        $this->note_public = 'This is a comment (public)';
6441
        $this->date_creation = $now;
6442
        $this->date_modification = $now;
6443
6444
        $this->weight = 4;
6445
        $this->weight_units = 3;
6446
6447
        $this->length = 5;
6448
        $this->length_units = 1;
6449
        $this->width = 6;
6450
        $this->width_units = 0;
6451
        $this->height = null;
6452
        $this->height_units = null;
6453
6454
        $this->surface = 30;
6455
        $this->surface_units = 0;
6456
        $this->volume = 300;
6457
        $this->volume_units = 0;
6458
6459
        $this->barcode = -1; // Create barcode automatically
6460
6461
        return 1;
6462
    }
6463
6464
    /**
6465
     *    Returns the text label from units dictionary
6466
     *
6467
     * @param  string $type Label type (long or short)
6468
     * @return string|int Return integer <0 if ko, label if ok
6469
     */
6470
    public function getLabelOfUnit($type = 'long')
6471
    {
6472
        global $langs;
6473
6474
        if (!$this->fk_unit) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->fk_unit of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
6475
            return '';
6476
        }
6477
6478
        $langs->load('products');
6479
        $label = '';
6480
        $label_type = 'label';
6481
        if ($type == 'short') {
6482
            $label_type = 'short_label';
6483
        }
6484
6485
        $sql = "SELECT " . $label_type . ", code from " . $this->db->prefix() . "c_units where rowid = " . ((int) $this->fk_unit);
6486
6487
        $resql = $this->db->query($sql);
6488
        if (!$resql) {
6489
            $this->error = $this->db->error();
6490
            dol_syslog(get_class($this) . "::getLabelOfUnit Error " . $this->error, LOG_ERR);
6491
            return -1;
6492
        } elseif ($this->db->num_rows($resql) > 0 && $res = $this->db->fetch_array($resql)) {
6493
            $label = ($label_type == 'short_label' ? $res[$label_type] : 'unit' . $res['code']);
6494
        }
6495
        $this->db->free($resql);
6496
6497
        return $label;
6498
    }
6499
6500
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6501
    /**
6502
     * Return minimum product recommended price
6503
     *
6504
     * @return int            Minimum recommended price that is higher price among all suppliers * PRODUCT_MINIMUM_RECOMMENDED_PRICE
6505
     */
6506
    public function min_recommended_price()
6507
    {
6508
		// phpcs:enable
6509
        global $conf;
6510
6511
        $maxpricesupplier = 0;
6512
6513
        if (getDolGlobalString('PRODUCT_MINIMUM_RECOMMENDED_PRICE')) {
6514
            include_once DOL_DOCUMENT_ROOT . '/fourn/class/fournisseur.product.class.php';
6515
            $product_fourn = new ProductFournisseur($this->db);
6516
            $product_fourn_list = $product_fourn->list_product_fournisseur_price($this->id, '', '');
6517
6518
            if (is_array($product_fourn_list) && count($product_fourn_list) > 0) {
6519
                foreach ($product_fourn_list as $productfourn) {
6520
                    if ($productfourn->fourn_unitprice > $maxpricesupplier) {
6521
                        $maxpricesupplier = $productfourn->fourn_unitprice;
6522
                    }
6523
                }
6524
6525
                $maxpricesupplier *= getDolGlobalString('PRODUCT_MINIMUM_RECOMMENDED_PRICE');
6526
            }
6527
        }
6528
6529
        return $maxpricesupplier;
6530
    }
6531
6532
6533
    /**
6534
     * Sets object to supplied categories.
6535
     *
6536
     * Deletes object from existing categories not supplied.
6537
     * Adds it to non existing supplied categories.
6538
     * Existing categories are left untouch.
6539
     *
6540
     * @param  int[]|int    $categories     Category or categories IDs
6541
     * @return int                          Return integer <0 if KO, >0 if OK
6542
     */
6543
    public function setCategories($categories)
6544
    {
6545
        require_once constant('DOL_DOCUMENT_ROOT') . '/categories/class/categorie.class.php';
6546
        return parent::setCategoriesCommon($categories, Categorie::TYPE_PRODUCT);
6547
    }
6548
6549
    /**
6550
     * Function used to replace a thirdparty id with another one.
6551
     *
6552
     * @param  DoliDB $dbs          Database handler
6553
     * @param  int    $origin_id    Old thirdparty id
6554
     * @param  int    $dest_id      New thirdparty id
6555
     * @return bool
6556
     */
6557
    public static function replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
6558
    {
6559
        $tables = array(
6560
            'product_customer_price',
6561
            'product_customer_price_log'
6562
        );
6563
6564
        return CommonObject::commonReplaceThirdparty($dbs, $origin_id, $dest_id, $tables);
6565
    }
6566
6567
    /**
6568
     * Generates prices for a product based on product multiprice generation rules
6569
     *
6570
     * @param  User   $user       User that updates the prices
6571
     * @param  float  $baseprice  Base price
6572
     * @param  string $price_type Base price type
6573
     * @param  float  $price_vat  VAT % tax
6574
     * @param  int    $npr        NPR
6575
     * @param  string $psq        ¿?
6576
     * @return int -1 KO, 1 OK
6577
     */
6578
    public function generateMultiprices(User $user, $baseprice, $price_type, $price_vat, $npr, $psq)
6579
    {
6580
        global $conf;
6581
6582
        $sql = "SELECT rowid, level, fk_level, var_percent, var_min_percent FROM " . $this->db->prefix() . "product_pricerules";
6583
        $query = $this->db->query($sql);
6584
6585
        $rules = array();
6586
6587
        while ($result = $this->db->fetch_object($query)) {
6588
            $rules[$result->level] = $result;
6589
        }
6590
6591
        //Because prices can be based on other level's prices, we temporarily store them
6592
        $prices = array(
6593
            1 => $baseprice
6594
        );
6595
6596
        $nbofproducts = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
6597
        for ($i = 1; $i <= $nbofproducts; $i++) {
6598
            $price = $baseprice;
6599
            $price_min = $baseprice;
6600
6601
            //We have to make sure it does exist and it is > 0
6602
            //First price level only allows changing min_price
6603
            if ($i > 1 && isset($rules[$i]->var_percent) && $rules[$i]->var_percent) {
6604
                $price = $prices[$rules[$i]->fk_level] * (1 + ($rules[$i]->var_percent / 100));
6605
            }
6606
6607
            $prices[$i] = $price;
6608
6609
            //We have to make sure it does exist and it is > 0
6610
            if (isset($rules[$i]->var_min_percent) && $rules[$i]->var_min_percent) {
6611
                $price_min = $price * (1 - ($rules[$i]->var_min_percent / 100));
6612
            }
6613
6614
            //Little check to make sure the price is modified before triggering generation
6615
            $check_amount = (($price == $this->multiprices[$i]) && ($price_min == $this->multiprices_min[$i]));
6616
            $check_type = ($baseprice == $this->multiprices_base_type[$i]);
6617
6618
            if ($check_amount && $check_type) {
6619
                continue;
6620
            }
6621
6622
            if ($this->updatePrice($price, $price_type, $user, $price_vat, $price_min, $i, $npr, $psq, true) < 0) {
6623
                return -1;
6624
            }
6625
        }
6626
6627
        return 1;
6628
    }
6629
6630
    /**
6631
     * Returns the rights used for this class
6632
     *
6633
     * @return Object
6634
     */
6635
    public function getRights()
6636
    {
6637
        global $user;
6638
6639
        if ($this->isProduct()) {
6640
            return $user->rights->produit;
6641
        } else {
6642
            return $user->rights->service;
6643
        }
6644
    }
6645
6646
    /**
6647
     *  Load information for tab info
6648
     *
6649
     * @param  int $id Id of thirdparty to load
6650
     * @return void
6651
     */
6652
    public function info($id)
6653
    {
6654
        $sql = "SELECT p.rowid, p.ref, p.datec as date_creation, p.tms as date_modification,";
6655
        $sql .= " p.fk_user_author, p.fk_user_modif";
6656
        $sql .= " FROM " . $this->db->prefix() . $this->table_element . " as p";
6657
        $sql .= " WHERE p.rowid = " . ((int) $id);
6658
6659
        $result = $this->db->query($sql);
6660
        if ($result) {
6661
            if ($this->db->num_rows($result)) {
6662
                $obj = $this->db->fetch_object($result);
6663
6664
                $this->id = $obj->rowid;
6665
                $this->ref = $obj->ref;
6666
6667
                $this->user_creation_id = $obj->fk_user_author;
6668
                $this->user_modification_id = $obj->fk_user_modif;
6669
6670
                $this->date_creation     = $this->db->jdate($obj->date_creation);
6671
                $this->date_modification = $this->db->jdate($obj->date_modification);
6672
            }
6673
6674
            $this->db->free($result);
6675
        } else {
6676
            dol_print_error($this->db);
6677
        }
6678
    }
6679
6680
6681
    /**
6682
     * Return the duration of a service in hours (for a service based on duration fields)
6683
     *
6684
     * @return float|-1     Duration in hours if OK, -1 if KO
0 ignored issues
show
Documentation Bug introduced by
The doc comment float|-1 at position 2 could not be parsed: Unknown type name '-1' at position 2 in float|-1.
Loading history...
6685
     */
6686
    public function getProductDurationHours()
6687
    {
6688
        global $langs;
6689
6690
        if (empty($this->duration_value)) {
6691
            $this->errors[] = 'ErrorDurationForServiceNotDefinedCantCalculateHourlyPrice';
6692
            return -1;
6693
        }
6694
6695
        if ($this->duration_unit == 'i') {
6696
            $prodDurationHours = 1. / 60;
6697
        }
6698
        if ($this->duration_unit == 'h') {
6699
            $prodDurationHours = 1.;
6700
        }
6701
        if ($this->duration_unit == 'd') {
6702
            $prodDurationHours = 24.;
6703
        }
6704
        if ($this->duration_unit == 'w') {
6705
            $prodDurationHours = 24. * 7;
6706
        }
6707
        if ($this->duration_unit == 'm') {
6708
            $prodDurationHours = 24. * 30;
6709
        }
6710
        if ($this->duration_unit == 'y') {
6711
            $prodDurationHours = 24. * 365;
6712
        }
6713
        $prodDurationHours *= $this->duration_value;
6714
6715
        return $prodDurationHours;
6716
    }
6717
6718
6719
    /**
6720
     *  Return clicable link of object (with eventually picto)
6721
     *
6722
     *  @param      string      $option                 Where point the link (0=> main card, 1,2 => shipment, 'nolink'=>No link)
6723
     *  @param      array       $arraydata              Array of data
6724
     *  @return     string                              HTML Code for Kanban thumb.
6725
     */
6726
    public function getKanbanView($option = '', $arraydata = null)
6727
    {
6728
        global $langs,$conf;
6729
6730
        $selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);
6731
6732
        $return = '<div class="box-flex-item box-flex-grow-zero">';
6733
        $return .= '<div class="info-box info-box-sm">';
6734
        $return .= '<div class="info-box-img">';
6735
        $label = '';
6736
        if ($this->is_photo_available($conf->product->multidir_output[$this->entity])) {
6737
            $label .= $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 120, 160, 0, 0, 0, '', 'photoref photokanban');
6738
            $return .= $label;
6739
        } else {
6740
            if ($this->type == Product::TYPE_PRODUCT) {
6741
                $label .= img_picto('', 'product');
6742
            } elseif ($this->type == Product::TYPE_SERVICE) {
6743
                $label .= img_picto('', 'service');
6744
            }
6745
            $return .= $label;
6746
        }
6747
        $return .= '</div>';
6748
        $return .= '<div class="info-box-content">';
6749
        $return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">' . (method_exists($this, 'getNomUrl') ? $this->getNomUrl() : $this->ref) . '</span>';
6750
        if ($selected >= 0) {
6751
            $return .= '<input id="cb' . $this->id . '" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="' . $this->id . '"' . ($selected ? ' checked="checked"' : '') . '>';
6752
        }
6753
        if (property_exists($this, 'label')) {
6754
            $return .= '<br><span class="info-box-label opacitymedium inline-block tdoverflowmax150 valignmiddle" title="' . dol_escape_htmltag($this->label) . '">' . dol_escape_htmltag($this->label) . '</span>';
6755
        }
6756
        if (property_exists($this, 'price') && property_exists($this, 'price_ttc')) {
6757
            if ($this->price_base_type == 'TTC') {
6758
                $return .= '<br><span class="info-box-status amount">' . price($this->price_ttc) . ' ' . $langs->trans("TTC") . '</span>';
6759
            } else {
6760
                if ($this->status) {
6761
                    $return .= '<br><span class="info-box-status amount">' . price($this->price) . ' ' . $langs->trans("HT") . '</span>';
6762
                }
6763
            }
6764
        }
6765
        $br = 1;
6766
        if (property_exists($this, 'stock_reel') && $this->isProduct()) {
6767
            $return .= '<br><div class="info-box-status opacitymedium inline-block valignmiddle">' . img_picto($langs->trans('PhysicalStock'), 'stock') . '</div><div class="inline-block valignmiddle paddingleft" title="' . $langs->trans('PhysicalStock') . '">' . $this->stock_reel . '</div>';
6768
            $br = 0;
6769
        }
6770
        if (method_exists($this, 'getLibStatut')) {
6771
            if ($br) {
6772
                $return .= '<br><div class="info-box-status inline-block valignmiddle">' . $this->getLibStatut(3, 1) . ' ' . $this->getLibStatut(3, 0) . '</div>';
6773
            } else {
6774
                $return .= '<div class="info-box-status inline-block valignmiddle marginleftonly paddingleft">' . $this->getLibStatut(3, 1) . ' ' . $this->getLibStatut(3, 0) . '</div>';
6775
            }
6776
        }
6777
        $return .= '</div>';
6778
        $return .= '</div>';
6779
        $return .= '</div>';
6780
        return $return;
6781
    }
6782
}
6783