Product::hasbatch()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 1
nc 2
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
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\Code\Categories\Classes\Categorie;
40
use Dolibarr\Code\Expedition\Classes\Expedition;
41
use Dolibarr\Code\Product\Model\ProductLot;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Dolibarr\Code\Product\Classes\ProductLot. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
42
use Dolibarr\Code\User\Classes\User;
43
use Dolibarr\Code\Variants\Model\ProductAttributeCombination;
44
use Dolibarr\Core\Base\CommonObject;
45
use DoliDB;
46
47
/**
48
 *    \file       htdocs/product/class/product.class.php
49
 *    \ingroup    produit
50
 *    \brief      File of class to manage the predefined products or services
51
 */
52
53
require_once constant('DOL_DOCUMENT_ROOT') . '/core/lib/product.lib.php';
54
55
/**
56
 * Class to manage products or services
57
 */
58
class Product extends CommonObject
59
{
60
    /**
61
     * Const sell or eat by mandatory id
62
     */
63
    const SELL_OR_EAT_BY_MANDATORY_ID_NONE = 0;
64
    const SELL_OR_EAT_BY_MANDATORY_ID_SELL_BY = 1;
65
    const SELL_OR_EAT_BY_MANDATORY_ID_EAT_BY = 2;
66
    const SELL_OR_EAT_BY_MANDATORY_ID_SELL_AND_EAT = 3;
67
    /**
68
     * Regular product
69
     */
70
    const TYPE_PRODUCT = 0;
71
    /**
72
     * Service
73
     */
74
    const TYPE_SERVICE = 1;
75
    /**
76
     * @var string ID to identify managed object
77
     */
78
    public $element = 'product';
79
    /**
80
     * @var string Name of table without prefix where object is stored
81
     */
82
    public $table_element = 'product';
83
    /**
84
     * @var string Field with ID of parent key if this field has a parent
85
     */
86
    public $fk_element = 'fk_product';
87
    /**
88
     * @var static
89
     */
90
    public $oldcopy;
91
    /**
92
     * @var string picto
93
     */
94
    public $picto = 'product';
95
    public $regeximgext = '\.gif|\.jpg|\.jpeg|\.png|\.bmp|\.webp|\.xpm|\.xbm'; // See also into images.lib.php
96
97
    /**
98
     * @deprecated  Use $label instead
99
     * @see $label
100
     */
101
    public $libelle;
102
103
    /**
104
     * Product label
105
     *
106
     * @var string
107
     */
108
    public $label;
109
110
    /**
111
     * Product description
112
     *
113
     * @var string
114
     */
115
    public $description;
116
117
    /**
118
     * Product other fields PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION
119
     *
120
     * @var string
121
     */
122
    public $other;
123
124
    /**
125
     * Check TYPE constants
126
     *
127
     * @var int
128
     */
129
    public $type = self::TYPE_PRODUCT;
130
131
    /**
132
     * Selling price without tax
133
     *
134
     * @var float
135
     */
136
    public $price;
137
138
    public $price_formated;         // used by takepos/ajax/ajax.php
139
140
    /**
141
     * Selling price with tax
142
     *
143
     * @var float
144
     */
145
    public $price_ttc;
146
147
    public $price_ttc_formated;     // used by takepos/ajax/ajax.php
148
149
    /**
150
     * Minimum price net
151
     *
152
     * @var float
153
     */
154
    public $price_min;
155
156
    /**
157
     * Minimum price with tax
158
     *
159
     * @var float
160
     */
161
    public $price_min_ttc;
162
163
    /**
164
     * Base price ('TTC' for price including tax or 'HT' for net price)
165
     * @var string
166
     */
167
    public $price_base_type;
168
    public $price_label;
169
170
    //! Arrays for multiprices
171
    public $multiprices = array();
172
    public $multiprices_ttc = array();
173
    public $multiprices_base_type = array();
174
    public $multiprices_default_vat_code = array();
175
    public $multiprices_min = array();
176
    public $multiprices_min_ttc = array();
177
    public $multiprices_tva_tx = array();
178
    public $multiprices_recuperableonly = array();
179
180
    //! Price by quantity arrays
181
    public $price_by_qty;
182
    public $prices_by_qty = array();
183
    public $prices_by_qty_id = array();
184
    public $prices_by_qty_list = array();
185
186
    /**
187
     * @var int price level set after updateprice for trigger
188
     */
189
    public $level;
190
191
    //! Array for multilangs
192
    public $multilangs = array();
193
194
    //! Default VAT code for product (link to code into llx_c_tva but without foreign keys)
195
    public $default_vat_code;
196
197
    //! Default VAT rate of product
198
    public $tva_tx;
199
200
    /**
201
     * int  French VAT NPR is used (0 or 1)
202
     */
203
    public $tva_npr = 0;
204
205
    //! Default discount percent
206
    public $remise_percent;
207
208
    //! Other local taxes
209
    public $localtax1_tx;
210
    public $localtax2_tx;
211
    public $localtax1_type;
212
    public $localtax2_type;
213
214
    // Properties set by get_buyprice() for return
215
216
    public $desc_supplier;
217
    public $vatrate_supplier;
218
    public $default_vat_code_supplier;
219
    public $fourn_multicurrency_price;
220
    public $fourn_multicurrency_unitprice;
221
    public $fourn_multicurrency_tx;
222
    public $fourn_multicurrency_id;
223
    public $fourn_multicurrency_code;
224
    public $packaging;
225
226
227
    /**
228
     * Lifetime (in seconds)
229
     *
230
     * @var int|null
231
     * @see ProductLot
232
     */
233
    public $lifetime;
234
235
    /**
236
     * Quality control frequency (in days ?)
237
     *
238
     * @var int|null
239
     * @see ProductLot
240
     */
241
    public $qc_frequency;
242
243
    /**
244
     * Stock real (denormalized data)
245
     *
246
     * @var int
247
     */
248
    public $stock_reel = 0;
249
250
    /**
251
     * Stock virtual
252
     *
253
     * @var int
254
     */
255
    public $stock_theorique;
256
257
    /**
258
     * Cost price
259
     *
260
     * @var float
261
     */
262
    public $cost_price;
263
264
    //! Average price value for product entry into stock (PMP)
265
    public $pmp;
266
267
    /**
268
     * Stock alert
269
     *
270
     * @var float
271
     */
272
    public $seuil_stock_alerte = 0;
273
274
    /**
275
     * Ask for replenishment when $desiredstock < $stock_reel
276
     */
277
    public $desiredstock = 0;
278
279
    /**
280
     * Service expiration
281
     */
282
    public $duration_value;
283
    /**
284
     * Service expiration unit
285
     */
286
    public $duration_unit;
287
    /**
288
     * Service expiration label (value + unit)
289
     */
290
    public $duration;
291
292
    /**
293
     * @var int Service Workstation
294
     */
295
    public $fk_default_workstation;
296
297
    /**
298
     * Status indicates whether the product is on sale '1' or not '0'
299
     *
300
     * @var int
301
     */
302
    public $status = 0;
303
304
    /**
305
     * Status indicates whether the product is on sale '1' or not '0'
306
     * @var int
307
     * @deprecated  Use $status instead
308
     * @see $status
309
     */
310
    public $tosell;
311
312
    /**
313
     * Status indicate whether the product is available for purchase '1' or not '0'
314
     *
315
     * @var int
316
     */
317
    public $status_buy = 0;
318
319
    /**
320
     * Status indicate whether the product is available for purchase '1' or not '0'
321
     * @var int
322
     * @deprecated Use $status_buy instead
323
     * @see $status_buy
324
     */
325
    public $tobuy;
326
327
    /**
328
     * Status indicates whether the product is a finished product '1' or a raw material '0'
329
     *
330
     * @var ?int
331
     */
332
    public $finished;
333
334
    /**
335
     * fk_default_bom indicates the default bom
336
     *
337
     * @var int
338
     */
339
    public $fk_default_bom;
340
341
    /**
342
     * product_fourn_price_id indicates the fourn price id
343
     *
344
     * @var int
345
     */
346
    public $product_fourn_price_id;
347
348
    /**
349
     * buyprice indicates buy price off the product
350
     *
351
     * @var float
352
     */
353
    public $buyprice;
354
355
    /**
356
     * for backward compatibility
357
     *
358
     * @var int
359
     */
360
    public $tobatch;
361
362
363
    /**
364
     * We must manage lot/batch number, sell-by date and so on : '0':no, '1':yes, '2": yes with unique serial number
365
     *
366
     * @var int
367
     */
368
    public $status_batch = 0;
369
370
    /**
371
     * Make sell-by or eat-by date mandatory
372
     *
373
     * @var int
374
     */
375
    public $sell_or_eat_by_mandatory = 0;
376
377
    /**
378
     * If allowed, we can edit batch or serial number mask for each product
379
     *
380
     * @var string
381
     */
382
    public $batch_mask = '';
383
384
    /**
385
     * Customs code
386
     *
387
     * @var string
388
     */
389
    public $customcode;
390
391
    /**
392
     * Product URL
393
     *
394
     * @var string
395
     */
396
    public $url;
397
398
    //! Metric of products
399
    public $weight;
400
    public $weight_units;   // scale -3, 0, 3, 6
401
    public $length;
402
    public $length_units;   // scale -3, 0, 3, 6
403
    public $width;
404
    public $width_units;    // scale -3, 0, 3, 6
405
    public $height;
406
    public $height_units;   // scale -3, 0, 3, 6
407
    public $surface;
408
    public $surface_units;  // scale -3, 0, 3, 6
409
    public $volume;
410
    public $volume_units;   // scale -3, 0, 3, 6
411
412
    public $net_measure;
413
    public $net_measure_units;  // scale -3, 0, 3, 6
414
415
    public $accountancy_code_sell;
416
    public $accountancy_code_sell_intra;
417
    public $accountancy_code_sell_export;
418
    public $accountancy_code_buy;
419
    public $accountancy_code_buy_intra;
420
    public $accountancy_code_buy_export;
421
422
    /**
423
     * @var string|int  Main Barcode value, -1 or 'auto' for auto code
424
     */
425
    public $barcode;
426
427
    /**
428
     * @var int     Main Barcode type ID
429
     */
430
    public $barcode_type;
431
432
    /**
433
     * @var string  Main Barcode type code
434
     */
435
    public $barcode_type_code;
436
437
    public $stats_propale = array();
438
    public $stats_commande = array();
439
    public $stats_contrat = array();
440
    public $stats_facture = array();
441
    public $stats_proposal_supplier = array();
442
    public $stats_commande_fournisseur = array();
443
    public $stats_expedition = array();
444
    public $stats_reception = array();
445
    public $stats_mo = array();
446
    public $stats_bom = array();
447
    public $stats_mrptoconsume = array();
448
    public $stats_mrptoproduce = array();
449
    public $stats_facturerec = array();
450
    public $stats_facture_fournisseur = array();
451
452
    //! Size of image
453
    public $imgWidth;
454
    public $imgHeight;
455
456
    /**
457
     * @var integer|string date_creation
458
     */
459
    public $date_creation;
460
461
    /**
462
     * @var integer|string date_modification
463
     */
464
    public $date_modification;
465
466
    //! Id du fournisseur
467
    public $product_fourn_id;
468
469
    //! Product ID already linked to a reference supplier
470
    public $product_id_already_linked;
471
472
    public $nbphoto = 0;
473
474
    //! Contains detail of stock of product into each warehouse
475
    public $stock_warehouse = array();
476
477
    /**
478
     * @var int Default warehouse Id
479
     */
480
    public $fk_default_warehouse;
481
    /**
482
     * @var int ID
483
     */
484
    public $fk_price_expression;
485
486
    /* To store supplier price found */
487
    public $fourn_qty;
488
    public $fourn_pu;
489
    public $fourn_price_base_type;
490
491
    /**
492
     * @var int ID
493
     */
494
    public $fourn_socid;
495
496
    /**
497
     * @deprecated
498
     * @see        $ref_supplier
499
     */
500
    public $ref_fourn;
501
502
    /**
503
     * @var string ref supplier
504
     */
505
    public $ref_supplier;
506
507
    /**
508
     * @var int|null                ID of the unit of measurement (rowid in llx_c_units table)
509
     * @see measuringUnitString()
510
     * @see getLabelOfUnit()
511
     */
512
    public $fk_unit;
513
514
    /**
515
     * Price is generated using multiprice rules
516
     *
517
     * @var int
518
     */
519
    public $price_autogen = 0;
520
521
    /**
522
     * Array with list of supplier prices of product
523
     *
524
     * @var array
525
     */
526
    public $supplierprices;
527
528
    /**
529
     * Array with list of sub-products for Kits
530
     *
531
     * @var array
532
     */
533
    public $sousprods;
534
535
    /**
536
     * @var array Path of subproducts. Build from ->sousprods with get_arbo_each_prod()
537
     */
538
    public $res;
539
540
541
    /**
542
     * Property set to save result of isObjectUsed(). Used for example by Product API.
543
     *
544
     * @var boolean
545
     */
546
    public $is_object_used;
547
548
    /**
549
     * If this Product is within a kit:
550
     * Quantity of this Product within this kit
551
     *
552
     * @var float
553
     * @see Product::is_sousproduit()       To set this property
554
     * @see Product::add_sousproduit()
555
     * @see Product::update_sousproduit()
556
     */
557
    public $is_sousproduit_qty;
558
559
    /**
560
     * If this Product is within a kit:
561
     * 1 = modify the stock of this child Product upon modification of the stock of its parent Product
562
     * ("incdec" stands for increase/decrease)
563
     *
564
     * @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...
565
     * @see Product::is_sousproduit()       To set this property
566
     * @see Product::add_sousproduit()
567
     * @see Product::update_sousproduit()
568
     */
569
    public $is_sousproduit_incdec;
570
571
    public $mandatory_period;
572
573
574
    /**
575
     *  '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')
576
     *         Note: Filter can be a string like "(t.ref:like:'SO-%') or (t.date_creation:<:'20160101') or (t.nature:is:NULL)"
577
     *  'label' the translation key.
578
     *  'enabled' is a condition when the field must be managed (Example: 1 or 'getDolGlobalString("MY_SETUP_PARAM")'
579
     *  'position' is the sort order of field.
580
     *  'notnull' is set to 1 if not null in database. Set to -1 if we must set data to null if empty ('' or 0).
581
     *  '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)
582
     *  'noteditable' says if field is not editable (1 or 0)
583
     *  '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.
584
     *  'index' if we want an index in database.
585
     *  'foreignkey'=>'tablename.field' if the field is a foreign key (it is recommended to name the field fk_...).
586
     *  'searchall' is 1 if we want to search in this field when making a search from the quick search button.
587
     *  '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).
588
     *  'css' is the CSS style to use on field. For example: 'maxwidth200'
589
     *  'help' is a string visible as a tooltip on field
590
     *  'showoncombobox' if value of the field must be visible into the label of the combobox that list record
591
     *  '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.
592
     *  'arrayofkeyval' to set list of value if type is a list of predefined values. For example: array("0"=>"Draft","1"=>"Active","-1"=>"Cancel")
593
     *  'autofocusoncreate' to have field having the focus on a create form. Only 1 field should have this property set to 1.
594
     *  'comment' is not used. You can store here any text of your choice. It is not used by application.
595
     *
596
     *  Note: To have value dynamic, you can set value to 0 in definition and edit the value on the fly into the constructor.
597
     */
598
599
    /**
600
     * @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...
601
     */
602
    public $fields = array(
603
        'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'index' => 1, 'position' => 1, 'comment' => 'Id'),
604
        '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'),
605
        'entity' => array('type' => 'integer', 'label' => 'Entity', 'enabled' => 1, 'visible' => 0, 'default' => '1', 'notnull' => 1, 'index' => 1, 'position' => 5),
606
        'label' => array('type' => 'varchar(255)', 'label' => 'Label', 'enabled' => 1, 'visible' => 1, 'notnull' => 1, 'showoncombobox' => 2, 'position' => 15, 'csslist' => 'tdoverflowmax250'),
607
        'barcode' => array('type' => 'varchar(255)', 'label' => 'Barcode', 'enabled' => 'isModEnabled("barcode")', 'position' => 20, 'visible' => -1, 'showoncombobox' => 3, 'cssview' => 'tdwordbreak', 'csslist' => 'tdoverflowmax125'),
608
        'fk_barcode_type' => array('type' => 'integer', 'label' => 'BarcodeType', 'enabled' => 1, 'position' => 21, 'notnull' => 0, 'visible' => -1,),
609
        'note_public' => array('type' => 'html', 'label' => 'NotePublic', 'enabled' => 1, 'visible' => 0, 'position' => 61),
610
        'note' => array('type' => 'html', 'label' => 'NotePrivate', 'enabled' => 1, 'visible' => 0, 'position' => 62),
611
        'datec' => array('type' => 'datetime', 'label' => 'DateCreation', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 500),
612
        'tms' => array('type' => 'timestamp', 'label' => 'DateModification', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 501),
613
        //'date_valid'    =>array('type'=>'datetime',     'label'=>'DateCreation',     'enabled'=>1, 'visible'=>-2, 'position'=>502),
614
        'fk_user_author' => array('type' => 'integer', 'label' => 'UserAuthor', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 510, 'foreignkey' => 'llx_user.rowid'),
615
        'fk_user_modif' => array('type' => 'integer', 'label' => 'UserModif', 'enabled' => 1, 'visible' => -2, 'notnull' => -1, 'position' => 511),
616
        //'fk_user_valid' =>array('type'=>'integer',      'label'=>'UserValidation',        'enabled'=>1, 'visible'=>-1, 'position'=>512),
617
        'localtax1_tx' => array('type' => 'double(6,3)', 'label' => 'Localtax1tx', 'enabled' => 1, 'position' => 150, 'notnull' => 0, 'visible' => -1,),
618
        'localtax1_type' => array('type' => 'varchar(10)', 'label' => 'Localtax1type', 'enabled' => 1, 'position' => 155, 'notnull' => 1, 'visible' => -1,),
619
        'localtax2_tx' => array('type' => 'double(6,3)', 'label' => 'Localtax2tx', 'enabled' => 1, 'position' => 160, 'notnull' => 0, 'visible' => -1,),
620
        'localtax2_type' => array('type' => 'varchar(10)', 'label' => 'Localtax2type', 'enabled' => 1, 'position' => 165, 'notnull' => 1, 'visible' => -1,),
621
        'last_main_doc' => array('type' => 'varchar(255)', 'label' => 'LastMainDoc', 'enabled' => 1, 'visible' => -1, 'position' => 170),
622
        'import_key' => array('type' => 'varchar(14)', 'label' => 'ImportId', 'enabled' => 1, 'visible' => -2, 'notnull' => -1, 'index' => 0, 'position' => 1000),
623
        //'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')),
624
        //'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')),
625
        'mandatory_period' => array('type' => 'integer', 'label' => 'mandatoryperiod', 'enabled' => 1, 'visible' => 1, 'notnull' => 1, 'default' => '0', 'index' => 1, 'position' => 1000),
626
    );
627
    /**
628
     * @var array<string, array<string>>    List of child tables. To test if we can delete object.
629
     */
630
    protected $childtables = array(
631
        'supplier_proposaldet' => array('name' => 'SupplierProposal', 'parent' => 'supplier_proposal', 'parentkey' => 'fk_supplier_proposal'),
632
        'propaldet' => array('name' => 'Proposal', 'parent' => 'propal', 'parentkey' => 'fk_propal'),
633
        'commandedet' => array('name' => 'Order', 'parent' => 'commande', 'parentkey' => 'fk_commande'),
634
        'facturedet' => array('name' => 'Invoice', 'parent' => 'facture', 'parentkey' => 'fk_facture'),
635
        'contratdet' => array('name' => 'Contract', 'parent' => 'contrat', 'parentkey' => 'fk_contrat'),
636
        'facture_fourn_det' => array('name' => 'SupplierInvoice', 'parent' => 'facture_fourn', 'parentkey' => 'fk_facture_fourn'),
637
        'commande_fournisseurdet' => array('name' => 'SupplierOrder', 'parent' => 'commande_fournisseur', 'parentkey' => 'fk_commande'),
638
        'mrp_production' => array('name' => 'Mo', 'parent' => 'mrp_mo', 'parentkey' => 'fk_mo')
639
    );
640
    /**
641
     * {@inheritdoc}
642
     */
643
    protected $table_ref_field = 'ref';
644
645
    /**
646
     *  Constructor
647
     *
648
     * @param DoliDB $db Database handler
649
     */
650
    public function __construct($db)
651
    {
652
        $this->db = $db;
653
654
        $this->ismultientitymanaged = 1;
655
        $this->isextrafieldmanaged = 1;
656
657
        $this->canvas = '';
658
    }
659
660
    /**
661
     * Function used to replace a thirdparty id with another one.
662
     *
663
     * @param DoliDB $dbs Database handler
664
     * @param int $origin_id Old thirdparty id
665
     * @param int $dest_id New thirdparty id
666
     * @return bool
667
     */
668
    public static function replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
669
    {
670
        $tables = array(
671
            'product_customer_price',
672
            'product_customer_price_log'
673
        );
674
675
        return CommonObject::commonReplaceThirdparty($dbs, $origin_id, $dest_id, $tables);
676
    }
677
678
    /**
679
     *    Check that ref and label are ok
680
     *
681
     * @return int         >1 if OK, <=0 if KO
682
     */
683
    public function check()
684
    {
685
        if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
686
            $this->ref = trim($this->ref);
687
        } else {
688
            $this->ref = dol_sanitizeFileName(stripslashes($this->ref));
689
        }
690
691
        $err = 0;
692
        if (dol_strlen(trim($this->ref)) == 0) {
693
            $err++;
694
        }
695
696
        if (dol_strlen(trim($this->label)) == 0) {
697
            $err++;
698
        }
699
700
        if ($err > 0) {
701
            return 0;
702
        } else {
703
            return 1;
704
        }
705
    }
706
707
    /**
708
     * Insert product into database
709
     *
710
     * @param User $user User making insert
711
     * @param int $notrigger Disable triggers
712
     * @return int                      Id of product/service if OK, < 0 if KO
713
     */
714
    public function create($user, $notrigger = 0)
715
    {
716
        global $conf, $langs;
717
718
        $error = 0;
719
720
        // Clean parameters
721
        if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
722
            $this->ref = trim($this->ref);
723
        } else {
724
            $this->ref = dol_sanitizeFileName(dol_string_nospecial(trim($this->ref)));
725
        }
726
        $this->label = trim($this->label);
727
        $this->price_ttc = (float)price2num($this->price_ttc);
728
        $this->price = (float)price2num($this->price);
729
        $this->price_min_ttc = (float)price2num($this->price_min_ttc);
730
        $this->price_min = (float)price2num($this->price_min);
731
        $this->price_label = trim($this->price_label);
732
        if (empty($this->tva_tx)) {
733
            $this->tva_tx = 0;
734
        }
735
        if (empty($this->tva_npr)) {
736
            $this->tva_npr = 0;
737
        }
738
        //Local taxes
739
        if (empty($this->localtax1_tx)) {
740
            $this->localtax1_tx = 0;
741
        }
742
        if (empty($this->localtax2_tx)) {
743
            $this->localtax2_tx = 0;
744
        }
745
        if (empty($this->localtax1_type)) {
746
            $this->localtax1_type = '0';
747
        }
748
        if (empty($this->localtax2_type)) {
749
            $this->localtax2_type = '0';
750
        }
751
        if (empty($this->price)) {
752
            $this->price = 0;
753
        }
754
        if (empty($this->price_min)) {
755
            $this->price_min = 0;
756
        }
757
        // Price by quantity
758
        if (empty($this->price_by_qty)) {
759
            $this->price_by_qty = 0;
760
        }
761
762
        if (empty($this->status)) {
763
            $this->status = 0;
764
        }
765
        if (empty($this->status_buy)) {
766
            $this->status_buy = 0;
767
        }
768
769
        $price_ht = 0;
770
        $price_ttc = 0;
771
        $price_min_ht = 0;
772
        $price_min_ttc = 0;
773
774
        //
775
        if ($this->price_base_type == 'TTC' && $this->price_ttc > 0) {
776
            $price_ttc = price2num($this->price_ttc, 'MU');
777
            $price_ht = price2num($this->price_ttc / (1 + ($this->tva_tx / 100)), 'MU');
778
        }
779
780
        //
781
        if ($this->price_base_type != 'TTC' && $this->price > 0) {
782
            $price_ht = price2num($this->price, 'MU');
783
            $price_ttc = price2num($this->price * (1 + ($this->tva_tx / 100)), 'MU');
784
        }
785
786
        //
787
        if (($this->price_min_ttc > 0) && ($this->price_base_type == 'TTC')) {
788
            $price_min_ttc = price2num($this->price_min_ttc, 'MU');
789
            $price_min_ht = price2num($this->price_min_ttc / (1 + ($this->tva_tx / 100)), 'MU');
790
        }
791
792
        //
793
        if (($this->price_min > 0) && ($this->price_base_type != 'TTC')) {
794
            $price_min_ht = price2num($this->price_min, 'MU');
795
            $price_min_ttc = price2num($this->price_min * (1 + ($this->tva_tx / 100)), 'MU');
796
        }
797
798
        $this->accountancy_code_buy = trim($this->accountancy_code_buy);
799
        $this->accountancy_code_buy_intra = trim($this->accountancy_code_buy_intra);
800
        $this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export);
801
        $this->accountancy_code_sell = trim($this->accountancy_code_sell);
802
        $this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra);
803
        $this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export);
804
805
        // Barcode value
806
        $this->barcode = trim($this->barcode);
807
        $this->mandatory_period = empty($this->mandatory_period) ? 0 : $this->mandatory_period;
808
        // Check parameters
809
        if (empty($this->label)) {
810
            $this->error = 'ErrorMandatoryParametersNotProvided';
811
            return -1;
812
        }
813
814
        if (empty($this->ref) || $this->ref == 'auto') {
815
            // Load object modCodeProduct
816
            $module = getDolGlobalString('PRODUCT_CODEPRODUCT_ADDON', 'mod_codeproduct_leopard');
817
            if ($module != 'mod_codeproduct_leopard') {    // Do not load module file for leopard
818
                if (substr($module, 0, 16) == 'mod_codeproduct_' && substr($module, -3) == 'php') {
819
                    $module = substr($module, 0, dol_strlen($module) - 4);
820
                }
821
                dol_include_once('/core/modules/product/' . $module . '.php');
822
                $modCodeProduct = new $module();
823
                '@phan-var-force ModeleProductCode $modCodeProduct';
824
                if (!empty($modCodeProduct->code_auto)) {
825
                    $this->ref = $modCodeProduct->getNextValue($this, $this->type);
826
                }
827
                unset($modCodeProduct);
828
            }
829
830
            if (empty($this->ref)) {
831
                $this->error = 'ProductModuleNotSetupForAutoRef';
832
                return -2;
833
            }
834
        }
835
836
        dol_syslog(get_only_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);
837
838
        $now = dol_now();
839
840
        if (empty($this->date_creation)) {
841
            $this->date_creation = $now;
842
        }
843
844
        $this->db->begin();
845
846
        // For automatic creation during create action (not used by Dolibarr GUI, can be used by scripts)
847
        if ($this->barcode == '-1' || $this->barcode == 'auto') {
848
            $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
849
        }
850
851
        // Check more parameters
852
        // If error, this->errors[] is filled
853
        $result = $this->verify();
854
855
        if ($result >= 0) {
856
            $sql = "SELECT count(*) as nb";
857
            $sql .= " FROM " . $this->db->prefix() . "product";
858
            $sql .= " WHERE entity IN (" . getEntity('product') . ")";
859
            $sql .= " AND ref = '" . $this->db->escape($this->ref) . "'";
860
861
            $result = $this->db->query($sql);
862
            if ($result) {
863
                $obj = $this->db->fetch_object($result);
864
                if ($obj->nb == 0) {
865
                    // Insert new product, no previous one found
866
                    $sql = "INSERT INTO " . $this->db->prefix() . "product (";
867
                    $sql .= "datec";
868
                    $sql .= ", entity";
869
                    $sql .= ", ref";
870
                    $sql .= ", ref_ext";
871
                    $sql .= ", price_min";
872
                    $sql .= ", price_min_ttc";
873
                    $sql .= ", label";
874
                    $sql .= ", fk_user_author";
875
                    $sql .= ", fk_product_type";
876
                    $sql .= ", price";
877
                    $sql .= ", price_ttc";
878
                    $sql .= ", price_base_type";
879
                    $sql .= ", price_label";
880
                    $sql .= ", tobuy";
881
                    $sql .= ", tosell";
882
                    if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
883
                        $sql .= ", accountancy_code_buy";
884
                        $sql .= ", accountancy_code_buy_intra";
885
                        $sql .= ", accountancy_code_buy_export";
886
                        $sql .= ", accountancy_code_sell";
887
                        $sql .= ", accountancy_code_sell_intra";
888
                        $sql .= ", accountancy_code_sell_export";
889
                    }
890
                    $sql .= ", canvas";
891
                    $sql .= ", finished";
892
                    $sql .= ", tobatch";
893
                    $sql .= ", sell_or_eat_by_mandatory";
894
                    $sql .= ", batch_mask";
895
                    $sql .= ", fk_unit";
896
                    $sql .= ", mandatory_period";
897
                    $sql .= ") VALUES (";
898
                    $sql .= "'" . $this->db->idate($this->date_creation) . "'";
899
                    $sql .= ", " . (!empty($this->entity) ? (int)$this->entity : (int)$conf->entity);
900
                    $sql .= ", '" . $this->db->escape($this->ref) . "'";
901
                    $sql .= ", " . (!empty($this->ref_ext) ? "'" . $this->db->escape($this->ref_ext) . "'" : "null");
902
                    $sql .= ", " . price2num($price_min_ht);
903
                    $sql .= ", " . price2num($price_min_ttc);
904
                    $sql .= ", " . (!empty($this->label) ? "'" . $this->db->escape($this->label) . "'" : "null");
905
                    $sql .= ", " . ((int)$user->id);
906
                    $sql .= ", " . ((int)$this->type);
907
                    $sql .= ", " . price2num($price_ht, 'MT');
908
                    $sql .= ", " . price2num($price_ttc, 'MT');
909
                    $sql .= ", '" . $this->db->escape($this->price_base_type) . "'";
910
                    $sql .= ", " . (!empty($this->price_label) ? "'" . $this->db->escape($this->price_label) . "'" : "null");
911
                    $sql .= ", " . ((int)$this->status);
912
                    $sql .= ", " . ((int)$this->status_buy);
913
                    if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
914
                        $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
915
                        $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
916
                        $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
917
                        $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
918
                        $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
919
                        $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
920
                    }
921
                    $sql .= ", '" . $this->db->escape($this->canvas) . "'";
922
                    $sql .= ", " . ((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? 'NULL' : (int)$this->finished);
923
                    $sql .= ", " . ((empty($this->status_batch) || $this->status_batch < 0) ? '0' : ((int)$this->status_batch));
924
                    $sql .= ", " . ((empty($this->sell_or_eat_by_mandatory) || $this->sell_or_eat_by_mandatory < 0) ? 0 : ((int)$this->sell_or_eat_by_mandatory));
925
                    $sql .= ", '" . $this->db->escape($this->batch_mask) . "'";
926
                    $sql .= ", " . ($this->fk_unit > 0 ? ((int)$this->fk_unit) : 'NULL');
927
                    $sql .= ", '" . $this->db->escape($this->mandatory_period) . "'";
928
                    $sql .= ")";
929
930
                    dol_syslog(get_only_class($this) . "::Create", LOG_DEBUG);
931
932
                    $result = $this->db->query($sql);
933
                    if ($result) {
934
                        $id = $this->db->last_insert_id($this->db->prefix() . "product");
935
936
                        if ($id > 0) {
937
                            $this->id = $id;
938
                            $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...
939
                            $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...
940
                            $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...
941
                            $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...
942
943
                            $result = $this->_log_price($user);
944
                            if ($result > 0) {
945
                                if ($this->update($id, $user, true, 'add') <= 0) {
946
                                    $error++;
947
                                }
948
                            } else {
949
                                $error++;
950
                                $this->error = $this->db->lasterror();
951
                            }
952
953
                            // update accountancy for this entity
954
                            if (!$error && getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
955
                                $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " . ((int)$this->id) . " AND entity = " . ((int)$conf->entity));
956
957
                                $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
958
                                $sql .= " fk_product";
959
                                $sql .= ", entity";
960
                                $sql .= ", accountancy_code_buy";
961
                                $sql .= ", accountancy_code_buy_intra";
962
                                $sql .= ", accountancy_code_buy_export";
963
                                $sql .= ", accountancy_code_sell";
964
                                $sql .= ", accountancy_code_sell_intra";
965
                                $sql .= ", accountancy_code_sell_export";
966
                                $sql .= ") VALUES (";
967
                                $sql .= $this->id;
968
                                $sql .= ", " . $conf->entity;
969
                                $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
970
                                $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
971
                                $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
972
                                $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
973
                                $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
974
                                $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
975
                                $sql .= ")";
976
                                $result = $this->db->query($sql);
977
                                if (!$result) {
978
                                    $error++;
979
                                    $this->error = 'ErrorFailedToInsertAccountancyForEntity';
980
                                }
981
                            }
982
                        } else {
983
                            $error++;
984
                            $this->error = 'ErrorFailedToGetInsertedId';
985
                        }
986
                    } else {
987
                        $error++;
988
                        $this->error = $this->db->lasterror();
989
                    }
990
                } else {
991
                    // Product already exists with this ref
992
                    $langs->load("products");
993
                    $error++;
994
                    $this->error = "ErrorProductAlreadyExists";
995
                    dol_syslog(get_only_class($this) . "::Create fails, ref " . $this->ref . " already exists");
996
                }
997
            } else {
998
                $error++;
999
                $this->error = $this->db->lasterror();
1000
            }
1001
1002
            if (!$error && !$notrigger) {
1003
                // Call trigger
1004
                $result = $this->call_trigger('PRODUCT_CREATE', $user);
1005
                if ($result < 0) {
1006
                    $error++;
1007
                }
1008
                // End call triggers
1009
            }
1010
1011
            if (!$error) {
1012
                $this->db->commit();
1013
                return $this->id;
1014
            } else {
1015
                $this->db->rollback();
1016
                return -$error;
1017
            }
1018
        } else {
1019
            $this->db->rollback();
1020
            dol_syslog(get_only_class($this) . "::Create fails verify " . implode(',', $this->errors), LOG_WARNING);
1021
            return -3;
1022
        }
1023
    }
1024
1025
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1026
1027
    /**
1028
     *  Get a barcode from the module to generate barcode values.
1029
     *  Return value is stored into this->barcode
1030
     *
1031
     * @param Product $object Object product or service
1032
     * @param string $type Barcode type (ean, isbn, ...)
1033
     * @return string
1034
     */
1035
    public function get_barcode($object, $type = '')
1036
    {
1037
        // phpcs:enable
1038
        global $conf;
1039
1040
        $result = '';
1041
        if (getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
1042
            $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
1043
            foreach ($dirsociete as $dirroot) {
1044
                $res = dol_include_once($dirroot . getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM') . '.php');
1045
                if ($res) {
1046
                    break;
1047
                }
1048
            }
1049
            $var = getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM');
1050
            $mod = new $var();
1051
            '@phan-var-force ModeleNumRefBarCode $module';
1052
1053
            $result = $mod->getNextValue($object, $type);
1054
1055
            dol_syslog(get_only_class($this) . "::get_barcode barcode=" . $result . " module=" . $var);
1056
        }
1057
        return $result;
1058
    }
1059
1060
    /**
1061
     *    Check properties of product are ok (like name, barcode, ...).
1062
     *    All properties must be already loaded on object (this->barcode, this->barcode_type_code, ...).
1063
     *
1064
     * @return int        0 if OK, <0 if KO
1065
     */
1066
    public function verify()
1067
    {
1068
        global $langs;
1069
1070
        $this->errors = array();
1071
1072
        $result = 0;
1073
        $this->ref = trim($this->ref);
1074
1075
        if (!$this->ref) {
1076
            $this->errors[] = 'ErrorBadRef';
1077
            $result = -2;
1078
        }
1079
1080
        $arrayofnonnegativevalue = array('weight' => 'Weight', 'width' => 'Width', 'height' => 'Height', 'length' => 'Length', 'surface' => 'Surface', 'volume' => 'Volume');
1081
        foreach ($arrayofnonnegativevalue as $key => $value) {
1082
            if (property_exists($this, $key) && !empty($this->$key) && ($this->$key < 0)) {
1083
                $langs->loadLangs(array("main", "other"));
1084
                $this->error = $langs->trans("FieldCannotBeNegative", $langs->transnoentitiesnoconv($value));
1085
                $this->errors[] = $this->error;
1086
                $result = -4;
1087
            }
1088
        }
1089
1090
        $rescode = $this->check_barcode($this->barcode, $this->barcode_type_code);
1091
        if ($rescode) {
1092
            if ($rescode == -1) {
1093
                $this->errors[] = 'ErrorBadBarCodeSyntax';
1094
            } elseif ($rescode == -2) {
1095
                $this->errors[] = 'ErrorBarCodeRequired';
1096
            } elseif ($rescode == -3) {
1097
                // Note: Common usage is to have barcode unique. For variants, we should have a different barcode.
1098
                $this->errors[] = 'ErrorBarCodeAlreadyUsed';
1099
            }
1100
1101
            $result = -3;
1102
        }
1103
1104
        return $result;
1105
    }
1106
1107
    /**
1108
     *  Check barcode
1109
     *
1110
     * @param string $valuetotest Value to test
1111
     * @param string $typefortest Type of barcode (ISBN, EAN, ...)
1112
     * @return int                        0 if OK
1113
     *                                     -1 ErrorBadBarCodeSyntax
1114
     *                                     -2 ErrorBarCodeRequired
1115
     *                                     -3 ErrorBarCodeAlreadyUsed
1116
     */
1117
    public function check_barcode($valuetotest, $typefortest)
1118
    {
1119
        // phpcs:enable
1120
        global $conf;
1121
        if (isModEnabled('barcode') && getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
1122
            $module = strtolower(getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM'));
1123
1124
            $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
1125
            foreach ($dirsociete as $dirroot) {
1126
                $res = dol_include_once($dirroot . $module . '.php');
1127
                if ($res) {
1128
                    break;
1129
                }
1130
            }
1131
1132
            $mod = new $module();
1133
            '@phan-var-force ModeleNumRefBarCode $mod';
1134
1135
            dol_syslog(get_only_class($this) . "::check_barcode value=" . $valuetotest . " type=" . $typefortest . " module=" . $module);
1136
            $result = $mod->verif($this->db, $valuetotest, $this, 0, $typefortest);
1137
            return $result;
1138
        } else {
1139
            return 0;
1140
        }
1141
    }
1142
1143
    /**
1144
     *  Insert a track that we changed a customer price
1145
     *
1146
     * @param User $user User making change
1147
     * @param int $level price level to change
1148
     * @return int                    Return integer <0 if KO, >0 if OK
1149
     */
1150
    private function _log_price($user, $level = 0)
1151
    {
1152
        // phpcs:enable
1153
        global $conf;
1154
1155
        $now = dol_now();
1156
1157
        // Clean parameters
1158
        if (empty($this->price_by_qty)) {
1159
            $this->price_by_qty = 0;
1160
        }
1161
1162
        // Add new price
1163
        $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,";
1164
        $sql .= " localtax1_tx, localtax2_tx, localtax1_type, localtax2_type, price_min,price_min_ttc,price_by_qty,entity,fk_price_expression) ";
1165
        $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) . ",";
1166
        $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');
1167
        $sql .= ")";
1168
1169
        dol_syslog(get_only_class($this) . "::_log_price", LOG_DEBUG);
1170
        $resql = $this->db->query($sql);
1171
        if (!$resql) {
1172
            $this->error = $this->db->lasterror();
1173
            dol_print_error($this->db);
1174
            return -1;
1175
        } else {
1176
            return 1;
1177
        }
1178
    }
1179
1180
    /**
1181
     *  Update a record into database.
1182
     *  If batch flag is set to on, we create records into llx_product_batch
1183
     *
1184
     * @param int $id Id of product
1185
     * @param User $user Object user making update
1186
     * @param int $notrigger Disable triggers
1187
     * @param string $action Current action for hookmanager ('add' or 'update')
1188
     * @param boolean $updatetype Update product type
1189
     * @return int                  1 if OK, -1 if ref already exists, -2 if other error
1190
     */
1191
    public function update($id, $user, $notrigger = 0, $action = 'update', $updatetype = false)
1192
    {
1193
        global $langs, $conf, $hookmanager;
1194
1195
        $error = 0;
1196
1197
        // Check parameters
1198
        if (!$this->label) {
1199
            $this->label = 'MISSING LABEL';
1200
        }
1201
1202
        // Clean parameters
1203
        if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
1204
            $this->ref = trim($this->ref);
1205
        } else {
1206
            $this->ref = dol_string_nospecial(trim($this->ref));
1207
        }
1208
        $this->label = trim($this->label);
1209
        $this->description = trim($this->description);
1210
        $this->note_private = (isset($this->note_private) ? trim($this->note_private) : null);
1211
        $this->note_public = (isset($this->note_public) ? trim($this->note_public) : null);
1212
        $this->net_measure = price2num($this->net_measure);
1213
        $this->net_measure_units = (empty($this->net_measure_units) ? '' : trim($this->net_measure_units));
1214
        $this->weight = price2num($this->weight);
1215
        $this->weight_units = (empty($this->weight_units) ? '' : trim($this->weight_units));
1216
        $this->length = price2num($this->length);
1217
        $this->length_units = (empty($this->length_units) ? '' : trim($this->length_units));
1218
        $this->width = price2num($this->width);
1219
        $this->width_units = (empty($this->width_units) ? '' : trim($this->width_units));
1220
        $this->height = price2num($this->height);
1221
        $this->height_units = (empty($this->height_units) ? '' : trim($this->height_units));
1222
        $this->surface = price2num($this->surface);
1223
        $this->surface_units = (empty($this->surface_units) ? '' : trim($this->surface_units));
1224
        $this->volume = price2num($this->volume);
1225
        $this->volume_units = (empty($this->volume_units) ? '' : trim($this->volume_units));
1226
1227
        // set unit not defined
1228
        if (is_numeric($this->length_units)) {
1229
            $this->width_units = $this->length_units; // Not used yet
1230
        }
1231
        if (is_numeric($this->length_units)) {
1232
            $this->height_units = $this->length_units; // Not used yet
1233
        }
1234
1235
        // Automated compute surface and volume if not filled
1236
        if (empty($this->surface) && !empty($this->length) && !empty($this->width) && $this->length_units == $this->width_units) {
1237
            $this->surface = (float)$this->length * (float)$this->width;
1238
            $this->surface_units = measuring_units_squared($this->length_units);
1239
        }
1240
        if (empty($this->volume) && !empty($this->surface) && !empty($this->height) && $this->length_units == $this->height_units) {
1241
            $this->volume = $this->surface * (float)$this->height;
1242
            $this->volume_units = measuring_units_cubed($this->height_units);
1243
        }
1244
1245
        if (empty($this->tva_tx)) {
1246
            $this->tva_tx = 0;
1247
        }
1248
        if (empty($this->tva_npr)) {
1249
            $this->tva_npr = 0;
1250
        }
1251
        if (empty($this->localtax1_tx)) {
1252
            $this->localtax1_tx = 0;
1253
        }
1254
        if (empty($this->localtax2_tx)) {
1255
            $this->localtax2_tx = 0;
1256
        }
1257
        if (empty($this->localtax1_type)) {
1258
            $this->localtax1_type = '0';
1259
        }
1260
        if (empty($this->localtax2_type)) {
1261
            $this->localtax2_type = '0';
1262
        }
1263
        if (empty($this->status)) {
1264
            $this->status = 0;
1265
        }
1266
        if (empty($this->status_buy)) {
1267
            $this->status_buy = 0;
1268
        }
1269
1270
        if (empty($this->country_id)) {
1271
            $this->country_id = 0;
1272
        }
1273
1274
        if (empty($this->state_id)) {
1275
            $this->state_id = 0;
1276
        }
1277
1278
        // Barcode value
1279
        $this->barcode = (empty($this->barcode) ? '' : trim($this->barcode));
1280
1281
        $this->accountancy_code_buy = trim($this->accountancy_code_buy);
1282
        $this->accountancy_code_buy_intra = (!empty($this->accountancy_code_buy_intra) ? trim($this->accountancy_code_buy_intra) : '');
1283
        $this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export);
1284
        $this->accountancy_code_sell = trim($this->accountancy_code_sell);
1285
        $this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra);
1286
        $this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export);
1287
1288
1289
        $this->db->begin();
1290
1291
        $result = 0;
1292
        // Check name is required and codes are ok or unique. If error, this->errors[] is filled
1293
        if ($action != 'add') {
1294
            $result = $this->verify(); // We don't check when update called during a create because verify was already done
1295
        } else {
1296
            // we can continue
1297
            $result = 0;
1298
        }
1299
1300
        if ($result >= 0) {
1301
            // $this->oldcopy should have been set by the caller of update (here properties were already modified)
1302
            if (is_null($this->oldcopy) || (is_object($this->oldcopy) && $this->oldcopy->isEmpty())) {
1303
                $this->oldcopy = dol_clone($this, 1);
1304
            }
1305
            // Test if batch management is activated on existing product
1306
            // If yes, we create missing entries into product_batch
1307
            if ($this->hasbatch() && !$this->oldcopy->hasbatch()) {
1308
                //$valueforundefinedlot = 'Undefined';  // In previous version, 39 and lower
1309
                $valueforundefinedlot = '000000';
1310
                if (getDolGlobalString('STOCK_DEFAULT_BATCH')) {
1311
                    $valueforundefinedlot = getDolGlobalString('STOCK_DEFAULT_BATCH');
1312
                }
1313
1314
                dol_syslog("Flag batch of product id=" . $this->id . " is set to ON, so we will create missing records into product_batch");
1315
1316
                $this->load_stock();
1317
                foreach ($this->stock_warehouse as $idW => $ObjW) {   // For each warehouse where we have stocks defined for this product (for each lines in product_stock)
1318
                    $qty_batch = 0;
1319
                    foreach ($ObjW->detail_batch as $detail) {    // Each lines of detail in product_batch of the current $ObjW = product_stock
1320
                        if ($detail->batch == $valueforundefinedlot || $detail->batch == 'Undefined') {
1321
                            // We discard this line, we will create it later
1322
                            $sqlclean = "DELETE FROM " . $this->db->prefix() . "product_batch WHERE batch in('Undefined', '" . $this->db->escape($valueforundefinedlot) . "') AND fk_product_stock = " . ((int)$ObjW->id);
1323
                            $result = $this->db->query($sqlclean);
1324
                            if (!$result) {
1325
                                dol_print_error($this->db);
1326
                                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...
1327
                            }
1328
                            continue;
1329
                        }
1330
1331
                        $qty_batch += $detail->qty;
1332
                    }
1333
                    // Quantities in batch details are not same as stock quantity,
1334
                    // so we add a default batch record to complete and get same qty in parent and child table
1335
                    if ($ObjW->real != $qty_batch) {
1336
                        $ObjBatch = new Productbatch($this->db);
1337
                        $ObjBatch->batch = $valueforundefinedlot;
1338
                        $ObjBatch->qty = ($ObjW->real - $qty_batch);
1339
                        $ObjBatch->fk_product_stock = $ObjW->id;
1340
1341
                        if ($ObjBatch->create($user, 1) < 0) {
1342
                            $error++;
1343
                            $this->errors = $ObjBatch->errors;
1344
                        } else {
1345
                            // we also add lot record if not exist
1346
                            $ObjLot = new Productlot($this->db);
1347
                            // @phan-suppress-next-line PhanPluginSuspiciousParamPosition
1348
                            if ($ObjLot->fetch(0, $this->id, $valueforundefinedlot) == 0) {
1349
                                $ObjLot->fk_product = $this->id;
1350
                                $ObjLot->entity = $this->entity;
1351
                                $ObjLot->fk_user_creat = $user->id;
1352
                                $ObjLot->batch = $valueforundefinedlot;
1353
                                if ($ObjLot->create($user, true) < 0) {
1354
                                    $error++;
1355
                                    $this->errors = $ObjLot->errors;
0 ignored issues
show
Bug introduced by
The property errors does not seem to exist on Dolibarr\Code\Product\Model\ProductLot. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
1356
                                }
1357
                            }
1358
                        }
1359
                    }
1360
                }
1361
            }
1362
1363
            // For automatic creation
1364
            if ($this->barcode == -1) {
1365
                $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
1366
            }
1367
1368
            $sql = "UPDATE " . $this->db->prefix() . "product";
1369
            $sql .= " SET label = '" . $this->db->escape($this->label) . "'";
1370
1371
            if ($updatetype && ($this->isProduct() || $this->isService())) {
1372
                $sql .= ", fk_product_type = " . ((int)$this->type);
1373
            }
1374
1375
            $sql .= ", ref = '" . $this->db->escape($this->ref) . "'";
1376
            $sql .= ", ref_ext = " . (!empty($this->ref_ext) ? "'" . $this->db->escape($this->ref_ext) . "'" : "null");
1377
            $sql .= ", default_vat_code = " . ($this->default_vat_code ? "'" . $this->db->escape($this->default_vat_code) . "'" : "null");
1378
            $sql .= ", tva_tx = " . ((float)$this->tva_tx);
1379
            $sql .= ", recuperableonly = " . ((int)$this->tva_npr);
1380
            $sql .= ", localtax1_tx = " . ((float)$this->localtax1_tx);
1381
            $sql .= ", localtax2_tx = " . ((float)$this->localtax2_tx);
1382
            $sql .= ", localtax1_type = " . ($this->localtax1_type != '' ? "'" . $this->db->escape($this->localtax1_type) . "'" : "'0'");
1383
            $sql .= ", localtax2_type = " . ($this->localtax2_type != '' ? "'" . $this->db->escape($this->localtax2_type) . "'" : "'0'");
1384
1385
            $sql .= ", barcode = " . (empty($this->barcode) ? "null" : "'" . $this->db->escape($this->barcode) . "'");
1386
            $sql .= ", fk_barcode_type = " . (empty($this->barcode_type) ? "null" : $this->db->escape($this->barcode_type));
1387
1388
            $sql .= ", tosell = " . (int)$this->status;
1389
            $sql .= ", tobuy = " . (int)$this->status_buy;
1390
            $sql .= ", tobatch = " . ((empty($this->status_batch) || $this->status_batch < 0) ? '0' : (int)$this->status_batch);
1391
            $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);
1392
            $sql .= ", batch_mask = '" . $this->db->escape($this->batch_mask) . "'";
1393
1394
            $sql .= ", finished = " . ((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? "null" : (int)$this->finished);
1395
            $sql .= ", fk_default_bom = " . ((!isset($this->fk_default_bom) || $this->fk_default_bom < 0 || $this->fk_default_bom == '') ? "null" : (int)$this->fk_default_bom);
1396
            $sql .= ", net_measure = " . ($this->net_measure != '' ? "'" . $this->db->escape($this->net_measure) . "'" : 'null');
1397
            $sql .= ", net_measure_units = " . ($this->net_measure_units != '' ? "'" . $this->db->escape($this->net_measure_units) . "'" : 'null');
1398
            $sql .= ", weight = " . ($this->weight != '' ? "'" . $this->db->escape($this->weight) . "'" : 'null');
1399
            $sql .= ", weight_units = " . ($this->weight_units != '' ? "'" . $this->db->escape($this->weight_units) . "'" : 'null');
1400
            $sql .= ", length = " . ($this->length != '' ? "'" . $this->db->escape($this->length) . "'" : 'null');
1401
            $sql .= ", length_units = " . ($this->length_units != '' ? "'" . $this->db->escape($this->length_units) . "'" : 'null');
1402
            $sql .= ", width= " . ($this->width != '' ? "'" . $this->db->escape($this->width) . "'" : 'null');
1403
            $sql .= ", width_units = " . ($this->width_units != '' ? "'" . $this->db->escape($this->width_units) . "'" : 'null');
1404
            $sql .= ", height = " . ($this->height != '' ? "'" . $this->db->escape($this->height) . "'" : 'null');
1405
            $sql .= ", height_units = " . ($this->height_units != '' ? "'" . $this->db->escape($this->height_units) . "'" : 'null');
1406
            $sql .= ", surface = " . ($this->surface != '' ? "'" . $this->db->escape($this->surface) . "'" : 'null');
1407
            $sql .= ", surface_units = " . ($this->surface_units != '' ? "'" . $this->db->escape($this->surface_units) . "'" : 'null');
1408
            $sql .= ", volume = " . ($this->volume != '' ? "'" . $this->db->escape($this->volume) . "'" : 'null');
1409
            $sql .= ", volume_units = " . ($this->volume_units != '' ? "'" . $this->db->escape($this->volume_units) . "'" : 'null');
1410
            $sql .= ", fk_default_warehouse = " . ($this->fk_default_warehouse > 0 ? ((int)$this->fk_default_warehouse) : 'null');
1411
            $sql .= ", fk_default_workstation = " . ($this->fk_default_workstation > 0 ? ((int)$this->fk_default_workstation) : 'null');
1412
            $sql .= ", seuil_stock_alerte = " . ((isset($this->seuil_stock_alerte) && is_numeric($this->seuil_stock_alerte)) ? (float)$this->seuil_stock_alerte : 'null');
1413
            $sql .= ", description = '" . $this->db->escape($this->description) . "'";
1414
            $sql .= ", url = " . ($this->url ? "'" . $this->db->escape($this->url) . "'" : 'null');
1415
            $sql .= ", customcode = '" . $this->db->escape($this->customcode) . "'";
1416
            $sql .= ", fk_country = " . ($this->country_id > 0 ? (int)$this->country_id : 'null');
1417
            $sql .= ", fk_state = " . ($this->state_id > 0 ? (int)$this->state_id : 'null');
1418
            $sql .= ", lifetime = " . ($this->lifetime > 0 ? (int)$this->lifetime : 'null');
1419
            $sql .= ", qc_frequency = " . ($this->qc_frequency > 0 ? (int)$this->qc_frequency : 'null');
1420
            $sql .= ", note = " . (isset($this->note_private) ? "'" . $this->db->escape($this->note_private) . "'" : 'null');
1421
            $sql .= ", note_public = " . (isset($this->note_public) ? "'" . $this->db->escape($this->note_public) . "'" : 'null');
1422
            $sql .= ", duration = '" . $this->db->escape($this->duration_value . $this->duration_unit) . "'";
1423
            if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1424
                $sql .= ", accountancy_code_buy = '" . $this->db->escape($this->accountancy_code_buy) . "'";
1425
                $sql .= ", accountancy_code_buy_intra = '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1426
                $sql .= ", accountancy_code_buy_export = '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1427
                $sql .= ", accountancy_code_sell= '" . $this->db->escape($this->accountancy_code_sell) . "'";
1428
                $sql .= ", accountancy_code_sell_intra= '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1429
                $sql .= ", accountancy_code_sell_export= '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1430
            }
1431
            $sql .= ", desiredstock = " . ((isset($this->desiredstock) && is_numeric($this->desiredstock)) ? (float)$this->desiredstock : "null");
1432
            $sql .= ", cost_price = " . ($this->cost_price != '' ? $this->db->escape($this->cost_price) : 'null');
1433
            $sql .= ", fk_unit= " . (!$this->fk_unit ? 'NULL' : (int)$this->fk_unit);
1434
            $sql .= ", price_autogen = " . (!$this->price_autogen ? 0 : 1);
1435
            $sql .= ", fk_price_expression = " . ($this->fk_price_expression != 0 ? (int)$this->fk_price_expression : 'NULL');
1436
            $sql .= ", fk_user_modif = " . ($user->id > 0 ? $user->id : 'NULL');
1437
            $sql .= ", mandatory_period = " . ($this->mandatory_period);
1438
            // stock field is not here because it is a denormalized value from product_stock.
1439
            $sql .= " WHERE rowid = " . ((int)$id);
1440
1441
            dol_syslog(get_only_class($this) . "::update", LOG_DEBUG);
1442
1443
            $resql = $this->db->query($sql);
1444
            if ($resql) {
1445
                $this->id = $id;
1446
1447
                // Multilangs
1448
                if (getDolGlobalInt('MAIN_MULTILANGS')) {
1449
                    if ($this->setMultiLangs($user) < 0) {
1450
                        $this->error = $langs->trans("Error") . " : " . $this->db->error() . " - " . $sql;
1451
                        $this->db->rollback();
1452
                        return -2;
1453
                    }
1454
                }
1455
1456
                $action = 'update';
1457
1458
                // update accountancy for this entity
1459
                if (!$error && getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1460
                    $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " . ((int)$this->id) . " AND entity = " . ((int)$conf->entity));
1461
1462
                    $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
1463
                    $sql .= " fk_product";
1464
                    $sql .= ", entity";
1465
                    $sql .= ", accountancy_code_buy";
1466
                    $sql .= ", accountancy_code_buy_intra";
1467
                    $sql .= ", accountancy_code_buy_export";
1468
                    $sql .= ", accountancy_code_sell";
1469
                    $sql .= ", accountancy_code_sell_intra";
1470
                    $sql .= ", accountancy_code_sell_export";
1471
                    $sql .= ") VALUES (";
1472
                    $sql .= $this->id;
1473
                    $sql .= ", " . $conf->entity;
1474
                    $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
1475
                    $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1476
                    $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1477
                    $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
1478
                    $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1479
                    $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1480
                    $sql .= ")";
1481
                    $result = $this->db->query($sql);
1482
                    if (!$result) {
1483
                        $error++;
1484
                        $this->error = 'ErrorFailedToUpdateAccountancyForEntity';
1485
                    }
1486
                }
1487
1488
                if (!$this->hasbatch() && $this->oldcopy->hasbatch()) {
1489
                    // Selection of all product stock movements that contains batchs
1490
                    $sql = 'SELECT pb.qty, ps.fk_entrepot, pb.batch FROM ' . MAIN_DB_PREFIX . 'product_batch as pb';
1491
                    $sql .= ' INNER JOIN ' . MAIN_DB_PREFIX . 'product_stock as ps ON (ps.rowid = pb.fk_product_stock)';
1492
                    $sql .= ' WHERE ps.fk_product = ' . (int)$this->id;
1493
1494
                    $resql = $this->db->query($sql);
1495
                    if ($resql) {
1496
                        $inventorycode = dol_print_date(dol_now(), '%Y%m%d%H%M%S');
1497
1498
                        while ($obj = $this->db->fetch_object($resql)) {
1499
                            $value = $obj->qty;
1500
                            $fk_entrepot = $obj->fk_entrepot;
1501
                            $price = 0;
1502
                            $dlc = '';
1503
                            $dluo = '';
1504
                            $batch = $obj->batch;
1505
1506
                            // To know how to revert stockMouvement (add or remove)
1507
                            $addOremove = $value > 0 ? 1 : 0; // 1 if remove, 0 if add
1508
                            $label = $langs->trans('BatchStockMouvementAddInGlobal');
1509
                            $res = $this->correct_stock_batch($user, $fk_entrepot, abs($value), $addOremove, $label, $price, $dlc, $dluo, $batch, $inventorycode, '', null, 0, null, true);
1510
1511
                            if ($res > 0) {
1512
                                $label = $langs->trans('BatchStockMouvementAddInGlobal');
1513
                                $res = $this->correct_stock($user, $fk_entrepot, abs($value), (int)empty($addOremove), $label, $price, $inventorycode, '', null, 0);
1514
                                if ($res < 0) {
1515
                                    $error++;
1516
                                }
1517
                            } else {
1518
                                $error++;
1519
                            }
1520
                        }
1521
                    }
1522
                }
1523
1524
                // Actions on extra fields
1525
                if (!$error) {
1526
                    $result = $this->insertExtraFields();
1527
                    if ($result < 0) {
1528
                        $error++;
1529
                    }
1530
                }
1531
1532
                if (!$error && !$notrigger) {
1533
                    // Call trigger
1534
                    $result = $this->call_trigger('PRODUCT_MODIFY', $user);
1535
                    if ($result < 0) {
1536
                        $error++;
1537
                    }
1538
                    // End call triggers
1539
                }
1540
1541
                if (!$error && (is_object($this->oldcopy) && $this->oldcopy->ref !== $this->ref)) {
1542
                    // We remove directory
1543
                    if ($conf->product->dir_output) {
1544
                        $olddir = $conf->product->dir_output . "/" . dol_sanitizeFileName($this->oldcopy->ref);
1545
                        $newdir = $conf->product->dir_output . "/" . dol_sanitizeFileName($this->ref);
1546
                        if (file_exists($olddir)) {
1547
                            //include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
1548
                            //$res = dol_move($olddir, $newdir);
1549
                            // do not use dol_move with directory
1550
                            $res = @rename($olddir, $newdir);
1551
                            if (!$res) {
1552
                                $langs->load("errors");
1553
                                $this->error = $langs->trans('ErrorFailToRenameDir', $olddir, $newdir);
1554
                                $error++;
1555
                            }
1556
                        }
1557
                    }
1558
                }
1559
1560
                if (!$error) {
1561
                    if (isModEnabled('variants')) {
1562
                        $comb = new ProductAttributeCombination();
1563
1564
                        foreach ($comb->fetchAllByFkProductParent($this->id) as $currcomb) {
1565
                            $currcomb->updateProperties($this, $user);
1566
                        }
1567
                    }
1568
1569
                    $this->db->commit();
1570
                    return 1;
1571
                } else {
1572
                    $this->db->rollback();
1573
                    return -$error;
1574
                }
1575
            } else {
1576
                if ($this->db->errno() == 'DB_ERROR_RECORD_ALREADY_EXISTS') {
1577
                    $langs->load("errors");
1578
                    if (empty($conf->barcode->enabled) || empty($this->barcode)) {
1579
                        $this->error = $langs->trans("Error") . " : " . $langs->trans("ErrorProductAlreadyExists", $this->ref);
1580
                    } else {
1581
                        $this->error = $langs->trans("Error") . " : " . $langs->trans("ErrorProductBarCodeAlreadyExists", $this->barcode);
1582
                    }
1583
                    $this->errors[] = $this->error;
1584
                    $this->db->rollback();
1585
                    return -1;
1586
                } else {
1587
                    $this->error = $langs->trans("Error") . " : " . $this->db->error() . " - " . $sql;
1588
                    $this->errors[] = $this->error;
1589
                    $this->db->rollback();
1590
                    return -2;
1591
                }
1592
            }
1593
        } else {
1594
            $this->db->rollback();
1595
            dol_syslog(get_only_class($this) . "::Update fails verify " . implode(',', $this->errors), LOG_WARNING);
1596
            return -3;
1597
        }
1598
    }
1599
1600
    /**
1601
     * Return if object has a sell-by date or eat-by date
1602
     *
1603
     * @return boolean     True if it's has
1604
     */
1605
    public function hasbatch()
1606
    {
1607
        return ($this->status_batch > 0 ? true : false);
1608
    }
1609
1610
    /**
1611
     * Load information about stock of a product into ->stock_reel, ->stock_warehouse[] (including stock_warehouse[idwarehouse]->detail_batch for batch products)
1612
     * This function need a lot of load. If you use it on list, use a cache to execute it once for each product id.
1613
     * If ENTREPOT_EXTRA_STATUS is set, filtering on warehouse status is possible.
1614
     *
1615
     * @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
1616
     *                                              You can also filter on 'warehouseclosed', 'warehouseopen', 'warehouseinternal'
1617
     * @param int $includedraftpoforvirtual Include draft status of PO for virtual stock calculation
1618
     * @param int $dateofvirtualstock Date of virtual stock
1619
     * @return  int                                 Return integer < 0 if KO, > 0 if OK
1620
     * @see     load_virtual_stock(), loadBatchInfo()
1621
     */
1622
    public function load_stock($option = '', $includedraftpoforvirtual = null, $dateofvirtualstock = null)
1623
    {
1624
        // phpcs:enable
1625
        global $conf;
1626
1627
        $this->stock_reel = 0;
1628
        $this->stock_warehouse = array();
1629
        $this->stock_theorique = 0;
1630
1631
        // Set filter on warehouse status
1632
        $warehouseStatus = array();
1633
        if (preg_match('/warehouseclosed/', $option)) {
1634
            $warehouseStatus[Entrepot::STATUS_CLOSED] = Entrepot::STATUS_CLOSED;
1635
        }
1636
        if (preg_match('/warehouseopen/', $option)) {
1637
            $warehouseStatus[Entrepot::STATUS_OPEN_ALL] = Entrepot::STATUS_OPEN_ALL;
1638
        }
1639
        if (preg_match('/warehouseinternal/', $option)) {
1640
            if (getDolGlobalString('ENTREPOT_EXTRA_STATUS')) {
1641
                $warehouseStatus[Entrepot::STATUS_OPEN_INTERNAL] = Entrepot::STATUS_OPEN_INTERNAL;
1642
            } else {
1643
                $warehouseStatus[Entrepot::STATUS_OPEN_ALL] = Entrepot::STATUS_OPEN_ALL;
1644
            }
1645
        }
1646
1647
        $sql = "SELECT ps.rowid, ps.reel, ps.fk_entrepot";
1648
        $sql .= " FROM " . $this->db->prefix() . "product_stock as ps";
1649
        $sql .= ", " . $this->db->prefix() . "entrepot as w";
1650
        $sql .= " WHERE w.entity IN (" . getEntity('stock') . ")";
1651
        $sql .= " AND w.rowid = ps.fk_entrepot";
1652
        $sql .= " AND ps.fk_product = " . ((int)$this->id);
1653
        if (count($warehouseStatus)) {
1654
            $sql .= " AND w.statut IN (" . $this->db->sanitize(implode(',', $warehouseStatus)) . ")";
1655
        }
1656
1657
        $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;
1658
1659
        dol_syslog(get_only_class($this) . "::load_stock", LOG_DEBUG);
1660
        $result = $this->db->query($sql);
1661
        if ($result) {
1662
            $num = $this->db->num_rows($result);
1663
            $i = 0;
1664
            if ($num > 0) {
1665
                while ($i < $num) {
1666
                    $row = $this->db->fetch_object($result);
1667
                    $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...
1668
                    $this->stock_warehouse[$row->fk_entrepot]->real = $row->reel;
1669
                    $this->stock_warehouse[$row->fk_entrepot]->id = $row->rowid;
1670
                    if ((!preg_match('/nobatch/', $option)) && $this->hasbatch()) {
1671
                        $this->stock_warehouse[$row->fk_entrepot]->detail_batch = Productbatch::findAll($this->db, $row->rowid, 1, $this->id);
1672
                    }
1673
                    $this->stock_reel += $row->reel;
1674
                    $i++;
1675
                }
1676
            }
1677
            $this->db->free($result);
1678
1679
            if (!preg_match('/novirtual/', $option)) {
1680
                $this->load_virtual_stock($includedraftpoforvirtual, $dateofvirtualstock); // This load stock_theorique and also load all arrays stats_xxx...
1681
            }
1682
1683
            return 1;
1684
        } else {
1685
            $this->error = $this->db->lasterror();
1686
            return -1;
1687
        }
1688
    }
1689
1690
    /**
1691
     *  Load value ->stock_theorique of a product. Property this->id must be defined.
1692
     *  This function need a lot of load. If you use it on list, use a cache to execute it one for each product id.
1693
     *
1694
     * @param int $includedraftpoforvirtual Include draft status and not yet approved Purchase Orders for virtual stock calculation
1695
     * @param int $dateofvirtualstock Date of virtual stock
1696
     * @return int                                 Return integer < 0 if KO, > 0 if OK
1697
     * @see    load_stock(), loadBatchInfo()
1698
     */
1699
    public function load_virtual_stock($includedraftpoforvirtual = null, $dateofvirtualstock = null)
1700
    {
1701
        // phpcs:enable
1702
        global $conf, $hookmanager, $action;
1703
1704
        $stock_commande_client = 0;
1705
        $stock_commande_fournisseur = 0;
1706
        $stock_sending_client = 0;
1707
        $stock_reception_fournisseur = 0;
1708
        $stock_inproduction = 0;
1709
1710
        //dol_syslog("load_virtual_stock");
1711
1712
        if (isModEnabled('order')) {
1713
            $result = $this->load_stats_commande(0, '1,2', 1);
1714
            if ($result < 0) {
1715
                dol_print_error($this->db, $this->error);
1716
            }
1717
            $stock_commande_client = $this->stats_commande['qty'];
1718
        }
1719
        if (isModEnabled("shipping")) {
1720
            $filterShipmentStatus = '';
1721
            if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT')) {
1722
                $filterShipmentStatus = Expedition::STATUS_VALIDATED . ',' . Expedition::STATUS_CLOSED;
1723
            } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
1724
                $filterShipmentStatus = Expedition::STATUS_CLOSED;
1725
            }
1726
            $result = $this->load_stats_sending(0, '1,2', 1, $filterShipmentStatus);
1727
            if ($result < 0) {
1728
                dol_print_error($this->db, $this->error);
1729
            }
1730
            $stock_sending_client = $this->stats_expedition['qty'];
1731
        }
1732
        if (isModEnabled("supplier_order")) {
1733
            $filterStatus = getDolGlobalString('SUPPLIER_ORDER_STATUS_FOR_VIRTUAL_STOCK', '3,4');
1734
            if (isset($includedraftpoforvirtual)) {
1735
                $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
1736
            }
1737
            $result = $this->load_stats_commande_fournisseur(0, $filterStatus, 1, $dateofvirtualstock);
1738
            if ($result < 0) {
1739
                dol_print_error($this->db, $this->error);
1740
            }
1741
            $stock_commande_fournisseur = $this->stats_commande_fournisseur['qty'];
1742
        }
1743
        if ((isModEnabled("supplier_order") || isModEnabled("supplier_invoice")) && empty($conf->reception->enabled)) {
1744
            // Case module reception is not used
1745
            $filterStatus = '4';
1746
            if (isset($includedraftpoforvirtual)) {
1747
                $filterStatus = '0,' . $filterStatus;
1748
            }
1749
            $result = $this->load_stats_reception(0, $filterStatus, 1, $dateofvirtualstock);
1750
            if ($result < 0) {
1751
                dol_print_error($this->db, $this->error);
1752
            }
1753
            $stock_reception_fournisseur = $this->stats_reception['qty'];
1754
        }
1755
        if ((isModEnabled("supplier_order") || isModEnabled("supplier_invoice")) && isModEnabled("reception")) {
1756
            // Case module reception is used
1757
            $filterStatus = '4';
1758
            if (isset($includedraftpoforvirtual)) {
1759
                $filterStatus = '0,' . $filterStatus;
1760
            }
1761
            $result = $this->load_stats_reception(0, $filterStatus, 1, $dateofvirtualstock); // Use same tables than when module reception is not used.
1762
            if ($result < 0) {
1763
                dol_print_error($this->db, $this->error);
1764
            }
1765
            $stock_reception_fournisseur = $this->stats_reception['qty'];
1766
        }
1767
        if (isModEnabled('mrp')) {
1768
            $result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock);
1769
            if ($result < 0) {
1770
                dol_print_error($this->db, $this->error);
1771
            }
1772
            $stock_inproduction = $this->stats_mrptoproduce['qty'] - $this->stats_mrptoconsume['qty'];
1773
        }
1774
1775
        $this->stock_theorique = $this->stock_reel + $stock_inproduction;
1776
1777
        // Stock decrease mode
1778
        if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') || getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
1779
            $this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
1780
        } elseif (getDolGlobalString('STOCK_CALCULATE_ON_VALIDATE_ORDER')) {
1781
            $this->stock_theorique += 0;
1782
        } elseif (getDolGlobalString('STOCK_CALCULATE_ON_BILL')) {
1783
            $this->stock_theorique -= $stock_commande_client;
1784
        }
1785
        // Stock Increase mode
1786
        if (getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION') || getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION_CLOSE')) {
1787
            $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
1788
        } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_DISPATCH_ORDER')) {
1789
            $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
1790
        } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_VALIDATE_ORDER')) {
1791
            $this->stock_theorique -= $stock_reception_fournisseur;
1792
        } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_BILL')) {
1793
            $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
1794
        }
1795
1796
        $parameters = array('id' => $this->id, 'includedraftpoforvirtual' => $includedraftpoforvirtual);
1797
        // Note that $action and $object may have been modified by some hooks
1798
        $reshook = $hookmanager->executeHooks('loadvirtualstock', $parameters, $this, $action);
1799
        if ($reshook > 0) {
1800
            $this->stock_theorique = $hookmanager->resArray['stock_theorique'];
1801
        } elseif ($reshook == 0 && isset($hookmanager->resArray['stock_stats_hook'])) {
1802
            $this->stock_theorique += $hookmanager->resArray['stock_stats_hook'];
1803
        }
1804
1805
        //Virtual Stock by Warehouse
1806
        if (!empty($this->stock_warehouse) && getDolGlobalString('STOCK_ALLOW_VIRTUAL_STOCK_PER_WAREHOUSE')) {
1807
            foreach ($this->stock_warehouse as $warehouseid => $stockwarehouse) {
1808
                if (isModEnabled('mrp')) {
1809
                    $result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock, $warehouseid);
1810
                    if ($result < 0) {
1811
                        dol_print_error($this->db, $this->error);
1812
                    }
1813
                }
1814
1815
                if ($this->fk_default_warehouse == $warehouseid) {
1816
                    $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']);
1817
                } else {
1818
                    $this->stock_warehouse[$warehouseid]->virtual = $this->stock_warehouse[$warehouseid]->real + $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'];
1819
                }
1820
            }
1821
        }
1822
1823
        return 1;
1824
    }
1825
1826
    /**
1827
     *  Charge tableau des stats commande client pour le produit/service
1828
     *
1829
     * @param int $socid Id thirdparty to filter on a thirdparty
1830
     * @param string $filtrestatut Id status to filter on a status
1831
     * @param int $forVirtualStock Ignore rights filter for virtual stock calculation. Set when load_stats_commande is used for virtual stock calculation.
1832
     * @return integer                 Array of stats in $this->stats_commande (nb=nb of order, qty=qty ordered), <0 if ko or >0 if ok
1833
     */
1834
    public function load_stats_commande($socid = 0, $filtrestatut = '', $forVirtualStock = 0)
1835
    {
1836
        // phpcs:enable
1837
        global $conf, $user, $hookmanager, $action;
1838
1839
        $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
1840
        $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
1841
        $sql .= " FROM " . $this->db->prefix() . "commandedet as cd";
1842
        $sql .= ", " . $this->db->prefix() . "commande as c";
1843
        $sql .= ", " . $this->db->prefix() . "societe as s";
1844
        if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
1845
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
1846
        }
1847
        $sql .= " WHERE c.rowid = cd.fk_commande";
1848
        $sql .= " AND c.fk_soc = s.rowid";
1849
        $sql .= " AND c.entity IN (" . getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'commande') . ")";
1850
        $sql .= " AND cd.fk_product = " . ((int)$this->id);
1851
        if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
1852
            $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int)$user->id);
1853
        }
1854
        if ($socid > 0) {
1855
            $sql .= " AND c.fk_soc = " . ((int)$socid);
1856
        }
1857
        if ($filtrestatut != '') {
1858
            $sql .= " AND c.fk_statut in (" . $this->db->sanitize($filtrestatut) . ")";
1859
        }
1860
1861
        $result = $this->db->query($sql);
1862
        if ($result) {
1863
            $obj = $this->db->fetch_object($result);
1864
            $this->stats_commande['customers'] = $obj->nb_customers;
1865
            $this->stats_commande['nb'] = $obj->nb;
1866
            $this->stats_commande['rows'] = $obj->nb_rows;
1867
            $this->stats_commande['qty'] = $obj->qty ? $obj->qty : 0;
1868
1869
            // if it's a virtual product, maybe it is in order by extension
1870
            if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
1871
                $TFather = $this->getFather();
1872
                if (is_array($TFather) && !empty($TFather)) {
1873
                    foreach ($TFather as &$fatherData) {
1874
                        $pFather = new Product($this->db);
1875
                        $pFather->id = $fatherData['id'];
1876
                        $qtyCoef = $fatherData['qty'];
1877
1878
                        if ($fatherData['incdec']) {
1879
                            $pFather->load_stats_commande($socid, $filtrestatut);
1880
1881
                            $this->stats_commande['customers'] += $pFather->stats_commande['customers'];
1882
                            $this->stats_commande['nb'] += $pFather->stats_commande['nb'];
1883
                            $this->stats_commande['rows'] += $pFather->stats_commande['rows'];
1884
                            $this->stats_commande['qty'] += $pFather->stats_commande['qty'] * $qtyCoef;
1885
                        }
1886
                    }
1887
                }
1888
            }
1889
1890
            // If stock decrease is on invoice validation, the theoretical stock continue to
1891
            // count the orders to ship in theoretical stock when some are already removed by invoice validation.
1892
            if ($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_ON_BILL')) {
1893
                if (getDolGlobalString('DECREASE_ONLY_UNINVOICEDPRODUCTS')) {
1894
                    // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is on, we make a compensation but only if order not yet invoice.
1895
                    $adeduire = 0;
1896
                    $sql = "SELECT SUM(" . $this->db->ifsql('f.type=2', -1, 1) . " * fd.qty) as count FROM " . $this->db->prefix() . "facturedet as fd ";
1897
                    $sql .= " JOIN " . $this->db->prefix() . "facture as f ON fd.fk_facture = f.rowid";
1898
                    $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'))";
1899
                    $sql .= " JOIN " . $this->db->prefix() . "commande as c ON el.fk_source = c.rowid";
1900
                    $sql .= " WHERE c.fk_statut IN (" . $this->db->sanitize($filtrestatut) . ") AND c.facture = 0 AND fd.fk_product = " . ((int)$this->id);
1901
1902
                    dol_syslog(__METHOD__ . ":: sql $sql", LOG_NOTICE);
1903
                    $resql = $this->db->query($sql);
1904
                    if ($resql) {
1905
                        if ($this->db->num_rows($resql) > 0) {
1906
                            $obj = $this->db->fetch_object($resql);
1907
                            $adeduire += $obj->count;
1908
                        }
1909
                    }
1910
1911
                    $this->stats_commande['qty'] -= $adeduire;
1912
                } else {
1913
                    // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is off, we make a compensation with lines of invoices linked to the order
1914
1915
                    // For every order having invoice already validated we need to decrease stock cause it's in physical stock
1916
                    $adeduire = 0;
1917
                    $sql = "SELECT sum(" . $this->db->ifsql('f.type=2', -1, 1) . " * fd.qty) as count FROM " . MAIN_DB_PREFIX . "facturedet as fd ";
1918
                    $sql .= " JOIN " . MAIN_DB_PREFIX . "facture as f ON fd.fk_facture = f.rowid";
1919
                    $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'))";
1920
                    $sql .= " JOIN " . MAIN_DB_PREFIX . "commande as c ON el.fk_source = c.rowid";
1921
                    $sql .= " WHERE c.fk_statut IN (" . $this->db->sanitize($filtrestatut) . ") AND f.fk_statut > " . Facture::STATUS_DRAFT . " AND fd.fk_product = " . ((int)$this->id);
1922
1923
                    dol_syslog(__METHOD__ . ":: sql $sql", LOG_NOTICE);
1924
                    $resql = $this->db->query($sql);
1925
                    if ($resql) {
1926
                        if ($this->db->num_rows($resql) > 0) {
1927
                            $obj = $this->db->fetch_object($resql);
1928
                            $adeduire += $obj->count;
1929
                        }
1930
                    } else {
1931
                        $this->error = $this->db->error();
1932
                        return -1;
1933
                    }
1934
1935
                    $this->stats_commande['qty'] -= $adeduire;
1936
                }
1937
            }
1938
1939
            $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
1940
            $reshook = $hookmanager->executeHooks('loadStatsCustomerOrder', $parameters, $this, $action);
1941
            if ($reshook > 0) {
1942
                $this->stats_commande = $hookmanager->resArray['stats_commande'];
1943
            }
1944
            return 1;
1945
        } else {
1946
            $this->error = $this->db->error();
1947
            return -1;
1948
        }
1949
    }
1950
1951
    /**
1952
     *  Return all parent products for current product (first level only)
1953
     *
1954
     * @return array|int         Array of product
1955
     * @see hasFatherOrChild()
1956
     */
1957
    public function getFather()
1958
    {
1959
        $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";
1960
        $sql .= ", p.tosell as status, p.tobuy as status_buy";
1961
        $sql .= " FROM " . $this->db->prefix() . "product_association as pa,";
1962
        $sql .= " " . $this->db->prefix() . "product as p";
1963
        $sql .= " WHERE p.rowid = pa.fk_product_pere";
1964
        $sql .= " AND pa.fk_product_fils = " . ((int)$this->id);
1965
1966
        $res = $this->db->query($sql);
1967
        if ($res) {
1968
            $prods = array();
1969
            while ($record = $this->db->fetch_array($res)) {
1970
                // $record['id'] = $record['rowid'] = id of father
1971
                $prods[$record['id']]['id'] = $record['rowid'];
1972
                $prods[$record['id']]['ref'] = $record['ref'];
1973
                $prods[$record['id']]['label'] = $record['label'];
1974
                $prods[$record['id']]['qty'] = $record['qty'];
1975
                $prods[$record['id']]['incdec'] = $record['incdec'];
1976
                $prods[$record['id']]['fk_product_type'] = $record['fk_product_type'];
1977
                $prods[$record['id']]['entity'] = $record['entity'];
1978
                $prods[$record['id']]['status'] = $record['status'];
1979
                $prods[$record['id']]['status_buy'] = $record['status_buy'];
1980
            }
1981
            return $prods;
1982
        } else {
1983
            dol_print_error($this->db);
1984
            return -1;
1985
        }
1986
    }
1987
1988
1989
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1990
1991
    /**
1992
     *  Charge tableau des stats expedition client pour le produit/service
1993
     *
1994
     * @param int $socid Id thirdparty to filter on a thirdparty
1995
     * @param string $filtrestatut [=''] Ids order status separated by comma
1996
     * @param int $forVirtualStock Ignore rights filter for virtual stock calculation.
1997
     * @param string $filterShipmentStatus [=''] Ids shipment status separated by comma
1998
     * @return  int                                 Array of stats in $this->stats_expedition, <0 if ko or >0 if ok
1999
     */
2000
    public function load_stats_sending($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $filterShipmentStatus = '')
2001
    {
2002
        // phpcs:enable
2003
        global $conf, $user, $hookmanager, $action;
2004
2005
        $sql = "SELECT COUNT(DISTINCT e.fk_soc) as nb_customers, COUNT(DISTINCT e.rowid) as nb,";
2006
        $sql .= " COUNT(ed.rowid) as nb_rows, SUM(ed.qty) as qty";
2007
        $sql .= " FROM " . $this->db->prefix() . "expeditiondet as ed";
2008
        $sql .= ", " . $this->db->prefix() . "commandedet as cd";
2009
        $sql .= ", " . $this->db->prefix() . "commande as c";
2010
        $sql .= ", " . $this->db->prefix() . "expedition as e";
2011
        $sql .= ", " . $this->db->prefix() . "societe as s";
2012
        if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
2013
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
2014
        }
2015
        $sql .= " WHERE e.rowid = ed.fk_expedition";
2016
        $sql .= " AND c.rowid = cd.fk_commande";
2017
        $sql .= " AND e.fk_soc = s.rowid";
2018
        $sql .= " AND e.entity IN (" . getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'expedition') . ")";
2019
        $sql .= " AND ed.fk_elementdet = cd.rowid";
2020
        $sql .= " AND cd.fk_product = " . ((int)$this->id);
2021
        if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
2022
            $sql .= " AND e.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int)$user->id);
2023
        }
2024
        if ($socid > 0) {
2025
            $sql .= " AND e.fk_soc = " . ((int)$socid);
2026
        }
2027
        if ($filtrestatut != '') {
2028
            $sql .= " AND c.fk_statut IN (" . $this->db->sanitize($filtrestatut) . ")";
2029
        }
2030
        if (!empty($filterShipmentStatus)) {
2031
            $sql .= " AND e.fk_statut IN (" . $this->db->sanitize($filterShipmentStatus) . ")";
2032
        }
2033
2034
        $result = $this->db->query($sql);
2035
        if ($result) {
2036
            $obj = $this->db->fetch_object($result);
2037
            $this->stats_expedition['customers'] = $obj->nb_customers;
2038
            $this->stats_expedition['nb'] = $obj->nb;
2039
            $this->stats_expedition['rows'] = $obj->nb_rows;
2040
            $this->stats_expedition['qty'] = $obj->qty ? $obj->qty : 0;
2041
2042
            // if it's a virtual product, maybe it is in sending by extension
2043
            if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
2044
                $TFather = $this->getFather();
2045
                if (is_array($TFather) && !empty($TFather)) {
2046
                    foreach ($TFather as &$fatherData) {
2047
                        $pFather = new Product($this->db);
2048
                        $pFather->id = $fatherData['id'];
2049
                        $qtyCoef = $fatherData['qty'];
2050
2051
                        if ($fatherData['incdec']) {
2052
                            $pFather->load_stats_sending($socid, $filtrestatut, $forVirtualStock);
2053
2054
                            $this->stats_expedition['customers'] += $pFather->stats_expedition['customers'];
2055
                            $this->stats_expedition['nb'] += $pFather->stats_expedition['nb'];
2056
                            $this->stats_expedition['rows'] += $pFather->stats_expedition['rows'];
2057
                            $this->stats_expedition['qty'] += $pFather->stats_expedition['qty'] * $qtyCoef;
2058
                        }
2059
                    }
2060
                }
2061
            }
2062
2063
            $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock, 'filterShipmentStatus' => $filterShipmentStatus);
2064
            $reshook = $hookmanager->executeHooks('loadStatsSending', $parameters, $this, $action);
2065
            if ($reshook > 0) {
2066
                $this->stats_expedition = $hookmanager->resArray['stats_expedition'];
2067
            }
2068
2069
            return 1;
2070
        } else {
2071
            $this->error = $this->db->error();
2072
            return -1;
2073
        }
2074
    }
2075
2076
2077
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2078
2079
    /**
2080
     *  Charge tableau des stats commande fournisseur pour le produit/service
2081
     *
2082
     * @param int $socid Id thirdparty to filter on a thirdparty
2083
     * @param string $filtrestatut Id of status to filter on status
2084
     * @param int $forVirtualStock Ignore rights filter for virtual stock calculation.
2085
     * @param int $dateofvirtualstock Date of virtual stock
2086
     * @return  int                         Array of stats in $this->stats_commande_fournisseur, <0 if ko or >0 if ok
2087
     */
2088
    public function load_stats_commande_fournisseur($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
2089
    {
2090
        // phpcs:enable
2091
        global $conf, $user, $hookmanager, $action;
2092
2093
        $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_suppliers, COUNT(DISTINCT c.rowid) as nb,";
2094
        $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
2095
        $sql .= " FROM " . $this->db->prefix() . "commande_fournisseurdet as cd";
2096
        $sql .= ", " . $this->db->prefix() . "commande_fournisseur as c";
2097
        $sql .= ", " . $this->db->prefix() . "societe as s";
2098
        if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
2099
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
2100
        }
2101
        $sql .= " WHERE c.rowid = cd.fk_commande";
2102
        $sql .= " AND c.fk_soc = s.rowid";
2103
        $sql .= " AND c.entity IN (" . getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'supplier_order') . ")";
2104
        $sql .= " AND cd.fk_product = " . ((int)$this->id);
2105
        if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
2106
            $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int)$user->id);
2107
        }
2108
        if ($socid > 0) {
2109
            $sql .= " AND c.fk_soc = " . ((int)$socid);
2110
        }
2111
        if ($filtrestatut != '') {
2112
            $sql .= " AND c.fk_statut in (" . $this->db->sanitize($filtrestatut) . ")"; // Peut valoir 0
2113
        }
2114
        if (!empty($dateofvirtualstock)) {
2115
            $sql .= " AND c.date_livraison <= '" . $this->db->idate($dateofvirtualstock) . "'";
2116
        }
2117
2118
        $result = $this->db->query($sql);
2119
        if ($result) {
2120
            $obj = $this->db->fetch_object($result);
2121
            $this->stats_commande_fournisseur['suppliers'] = $obj->nb_suppliers;
2122
            $this->stats_commande_fournisseur['nb'] = $obj->nb;
2123
            $this->stats_commande_fournisseur['rows'] = $obj->nb_rows;
2124
            $this->stats_commande_fournisseur['qty'] = $obj->qty ? $obj->qty : 0;
2125
2126
            $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
2127
            $reshook = $hookmanager->executeHooks('loadStatsSupplierOrder', $parameters, $this, $action);
2128
            if ($reshook > 0) {
2129
                $this->stats_commande_fournisseur = $hookmanager->resArray['stats_commande_fournisseur'];
2130
            }
2131
2132
            return 1;
2133
        } else {
2134
            $this->error = $this->db->error() . ' sql=' . $sql;
2135
            return -1;
2136
        }
2137
    }
2138
2139
    /**
2140
     *  Charge tableau des stats réception fournisseur pour le produit/service
2141
     *
2142
     * @param int $socid Id thirdparty to filter on a thirdparty
2143
     * @param string $filtrestatut Id status to filter on a status
2144
     * @param int $forVirtualStock Ignore rights filter for virtual stock calculation.
2145
     * @param int $dateofvirtualstock Date of virtual stock
2146
     * @return int                          Array of stats in $this->stats_reception, <0 if ko or >0 if ok
2147
     */
2148
    public function load_stats_reception($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
2149
    {
2150
        // phpcs:enable
2151
        global $conf, $user, $hookmanager, $action;
2152
2153
        $sql = "SELECT COUNT(DISTINCT cf.fk_soc) as nb_suppliers, COUNT(DISTINCT cf.rowid) as nb,";
2154
        $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
2155
        $sql .= " FROM " . $this->db->prefix() . "receptiondet_batch as fd";
2156
        $sql .= ", " . $this->db->prefix() . "commande_fournisseur as cf";
2157
        $sql .= ", " . $this->db->prefix() . "societe as s";
2158
        if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
2159
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
2160
        }
2161
        $sql .= " WHERE cf.rowid = fd.fk_element";
2162
        $sql .= " AND cf.fk_soc = s.rowid";
2163
        $sql .= " AND cf.entity IN (" . getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'supplier_order') . ")";
2164
        $sql .= " AND fd.fk_product = " . ((int)$this->id);
2165
        if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
2166
            $sql .= " AND cf.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int)$user->id);
2167
        }
2168
        if ($socid > 0) {
2169
            $sql .= " AND cf.fk_soc = " . ((int)$socid);
2170
        }
2171
        if ($filtrestatut != '') {
2172
            $sql .= " AND cf.fk_statut IN (" . $this->db->sanitize($filtrestatut) . ")";
2173
        }
2174
        if (!empty($dateofvirtualstock)) {
2175
            $sql .= " AND fd.datec <= '" . $this->db->idate($dateofvirtualstock) . "'";
2176
        }
2177
2178
        $result = $this->db->query($sql);
2179
        if ($result) {
2180
            $obj = $this->db->fetch_object($result);
2181
            $this->stats_reception['suppliers'] = $obj->nb_suppliers;
2182
            $this->stats_reception['nb'] = $obj->nb;
2183
            $this->stats_reception['rows'] = $obj->nb_rows;
2184
            $this->stats_reception['qty'] = $obj->qty ? $obj->qty : 0;
2185
2186
            $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
2187
            $reshook = $hookmanager->executeHooks('loadStatsReception', $parameters, $this, $action);
2188
            if ($reshook > 0) {
2189
                $this->stats_reception = $hookmanager->resArray['stats_reception'];
2190
            }
2191
2192
            return 1;
2193
        } else {
2194
            $this->error = $this->db->error();
2195
            return -1;
2196
        }
2197
    }
2198
2199
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2200
2201
    /**
2202
     *  Charge tableau des stats production pour le produit/service
2203
     *
2204
     * @param int $socid Id thirdparty to filter on a thirdparty
2205
     * @param string $filtrestatut Id status to filter on a status
2206
     * @param int $forVirtualStock Ignore rights filter for virtual stock calculation.
2207
     * @param int $dateofvirtualstock Date of virtual stock
2208
     * @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.
2209
     * @return  integer                     Array of stats in $this->stats_mrptoproduce (nb=nb of order, qty=qty ordered), <0 if ko or >0 if ok
2210
     */
2211
    public function load_stats_inproduction($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null, $warehouseid = 0)
2212
    {
2213
        // phpcs:enable
2214
        global $user, $hookmanager, $action;
2215
2216
        $serviceStockIsEnabled = isModEnabled("service") && getDolGlobalString('STOCK_SUPPORTS_SERVICES');
2217
2218
        $sql = "SELECT COUNT(DISTINCT m.fk_soc) as nb_customers, COUNT(DISTINCT m.rowid) as nb,";
2219
        $sql .= " COUNT(mp.rowid) as nb_rows, SUM(mp.qty) as qty, role";
2220
        $sql .= " FROM " . $this->db->prefix() . "mrp_production as mp";
2221
        $sql .= ", " . $this->db->prefix() . "mrp_mo as m";
2222
        $sql .= " LEFT JOIN " . $this->db->prefix() . "societe as s ON s.rowid = m.fk_soc";
2223
        if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
2224
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
2225
        }
2226
        $sql .= " WHERE m.rowid = mp.fk_mo";
2227
        $sql .= " AND m.entity IN (" . getEntity(($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE')) ? 'stock' : 'mrp') . ")";
2228
        $sql .= " AND mp.fk_product = " . ((int)$this->id);
2229
        $sql .= " AND (mp.disable_stock_change IN (0) OR mp.disable_stock_change IS NULL)";
2230
        if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
2231
            $sql .= " AND m.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int)$user->id);
2232
        }
2233
        if ($socid > 0) {
2234
            $sql .= " AND m.fk_soc = " . ((int)$socid);
2235
        }
2236
        if ($filtrestatut != '') {
2237
            $sql .= " AND m.status IN (" . $this->db->sanitize($filtrestatut) . ")";
2238
        }
2239
        if (!empty($dateofvirtualstock)) {
2240
            $sql .= " AND m.date_valid <= '" . $this->db->idate($dateofvirtualstock) . "'"; // better date to code ? end of production ?
2241
        }
2242
        if (!$serviceStockIsEnabled) {
2243
            $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))";
2244
        }
2245
        if (!empty($warehouseid)) {
2246
            $sql .= " AND m.fk_warehouse = " . ((int)$warehouseid);
2247
        }
2248
        $sql .= " GROUP BY role";
2249
2250
        if ($warehouseid) {
2251
            $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] = 0;
2252
        } else {
2253
            $this->stats_mrptoconsume['customers'] = 0;
2254
            $this->stats_mrptoconsume['nb'] = 0;
2255
            $this->stats_mrptoconsume['rows'] = 0;
2256
            $this->stats_mrptoconsume['qty'] = 0;
2257
            $this->stats_mrptoproduce['customers'] = 0;
2258
            $this->stats_mrptoproduce['nb'] = 0;
2259
            $this->stats_mrptoproduce['rows'] = 0;
2260
            $this->stats_mrptoproduce['qty'] = 0;
2261
        }
2262
2263
        $result = $this->db->query($sql);
2264
        if ($result) {
2265
            while ($obj = $this->db->fetch_object($result)) {
2266
                if ($obj->role == 'toconsume' && empty($warehouseid)) {
2267
                    $this->stats_mrptoconsume['customers'] += $obj->nb_customers;
2268
                    $this->stats_mrptoconsume['nb'] += $obj->nb;
2269
                    $this->stats_mrptoconsume['rows'] += $obj->nb_rows;
2270
                    $this->stats_mrptoconsume['qty'] += ($obj->qty ? $obj->qty : 0);
2271
                }
2272
                if ($obj->role == 'consumed' && empty($warehouseid)) {
2273
                    //$this->stats_mrptoconsume['customers'] += $obj->nb_customers;
2274
                    //$this->stats_mrptoconsume['nb'] += $obj->nb;
2275
                    //$this->stats_mrptoconsume['rows'] += $obj->nb_rows;
2276
                    $this->stats_mrptoconsume['qty'] -= ($obj->qty ? $obj->qty : 0);
2277
                }
2278
                if ($obj->role == 'toproduce') {
2279
                    if ($warehouseid) {
2280
                        $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] += ($obj->qty ? $obj->qty : 0);
2281
                    } else {
2282
                        $this->stats_mrptoproduce['customers'] += $obj->nb_customers;
2283
                        $this->stats_mrptoproduce['nb'] += $obj->nb;
2284
                        $this->stats_mrptoproduce['rows'] += $obj->nb_rows;
2285
                        $this->stats_mrptoproduce['qty'] += ($obj->qty ? $obj->qty : 0);
2286
                    }
2287
                }
2288
                if ($obj->role == 'produced') {
2289
                    //$this->stats_mrptoproduce['customers'] += $obj->nb_customers;
2290
                    //$this->stats_mrptoproduce['nb'] += $obj->nb;
2291
                    //$this->stats_mrptoproduce['rows'] += $obj->nb_rows;
2292
                    if ($warehouseid) {
2293
                        $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
2294
                    } else {
2295
                        $this->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
2296
                    }
2297
                }
2298
            }
2299
2300
            // Clean data
2301
            if ($warehouseid) {
2302
                if ($this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] < 0) {
2303
                    $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] = 0;
2304
                }
2305
            } else {
2306
                if ($this->stats_mrptoconsume['qty'] < 0) {
2307
                    $this->stats_mrptoconsume['qty'] = 0;
2308
                }
2309
                if ($this->stats_mrptoproduce['qty'] < 0) {
2310
                    $this->stats_mrptoproduce['qty'] = 0;
2311
                }
2312
            }
2313
2314
            $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
2315
            $reshook = $hookmanager->executeHooks('loadStatsInProduction', $parameters, $this, $action);
2316
            if ($reshook > 0) {
2317
                $this->stats_mrptoproduce = $hookmanager->resArray['stats_mrptoproduce'];
2318
            }
2319
2320
            return 1;
2321
        } else {
2322
            $this->error = $this->db->error();
2323
            return -1;
2324
        }
2325
    }
2326
2327
    /**
2328
     *  Load a product in memory from database
2329
     *
2330
     * @param int $id Id of product/service to load
2331
     * @param string $ref Ref of product/service to load
2332
     * @param string $ref_ext Ref ext of product/service to load
2333
     * @param string $barcode Barcode of product/service to load
2334
     * @param int $ignore_expression When module dynamicprices is on, ignores the math expression for calculating price and uses the db value instead
2335
     * @param int $ignore_price_load Load product without loading $this->multiprices... array (when we are sure we don't need them)
2336
     * @param int $ignore_lang_load Load product without loading $this->multilangs language arrays (when we are sure we don't need them)
2337
     * @return int                       Return integer <0 if KO, 0 if not found, >0 if OK
2338
     */
2339
    public function fetch($id = 0, $ref = '', $ref_ext = '', $barcode = '', $ignore_expression = 0, $ignore_price_load = 0, $ignore_lang_load = 0)
2340
    {
2341
        include_once DOL_DOCUMENT_ROOT . '/core/lib/company.lib.php';
2342
2343
        global $langs, $conf;
2344
2345
        dol_syslog(get_only_class($this) . "::fetch id=" . $id . " ref=" . $ref . " ref_ext=" . $ref_ext);
2346
2347
        // Check parameters
2348
        if (!$id && !$ref && !$ref_ext && !$barcode) {
2349
            $this->error = 'ErrorWrongParameters';
2350
            dol_syslog(get_only_class($this) . "::fetch " . $this->error, LOG_ERR);
2351
            return -1;
2352
        }
2353
2354
        $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,";
2355
        $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,";
2356
        $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,";
2357
        $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units, p.last_main_doc,";
2358
        $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,";
2359
        if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
2360
            $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,";
2361
        } else {
2362
            $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,";
2363
        }
2364
2365
        //For MultiCompany
2366
        //PMP per entity & Stocks Sharings stock_reel includes only stocks shared with this entity
2367
        $separatedEntityPMP = false;    // Set to true to get the AWP from table llx_product_perentity instead of field 'pmp' into llx_product.
2368
        $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.
2369
        $visibleWarehousesEntities = $conf->entity;
2370
        if (getDolGlobalString('MULTICOMPANY_PRODUCT_SHARING_ENABLED')) {
2371
            if (getDolGlobalString('MULTICOMPANY_PMP_PER_ENTITY_ENABLED')) {
2372
                $checkPMPPerEntity = $this->db->query("SELECT pmp FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " . ((int)$id) . " AND entity = " . (int)$conf->entity);
2373
                if ($this->db->num_rows($checkPMPPerEntity) > 0) {
2374
                    $separatedEntityPMP = true;
2375
                }
2376
            }
2377
            global $mc;
2378
            $separatedStock = true;
2379
            if (isset($mc->sharings['stock']) && !empty($mc->sharings['stock'])) {
2380
                $visibleWarehousesEntities .= "," . implode(",", $mc->sharings['stock']);
2381
            }
2382
        }
2383
        if ($separatedEntityPMP) {
2384
            $sql .= " ppe.pmp,";
2385
        } else {
2386
            $sql .= " p.pmp,";
2387
        }
2388
        $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,";
2389
        $sql .= " p.fk_price_expression, p.price_autogen, p.model_pdf,";
2390
        $sql .= " p.price_label,";
2391
        if ($separatedStock) {
2392
            $sql .= " SUM(sp.reel) as stock";
2393
        } else {
2394
            $sql .= " p.stock";
2395
        }
2396
        $sql .= " FROM " . $this->db->prefix() . "product as p";
2397
        if (getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED') || $separatedEntityPMP) {
2398
            $sql .= " LEFT JOIN " . $this->db->prefix() . "product_perentity as ppe ON ppe.fk_product = p.rowid AND ppe.entity = " . ((int)$conf->entity);
2399
        }
2400
        if ($separatedStock) {
2401
            $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) . "))";
2402
        }
2403
2404
        if ($id) {
2405
            $sql .= " WHERE p.rowid = " . ((int)$id);
2406
        } else {
2407
            $sql .= " WHERE p.entity IN (" . getEntity($this->element) . ")";
2408
            if ($ref) {
2409
                $sql .= " AND p.ref = '" . $this->db->escape($ref) . "'";
2410
            } elseif ($ref_ext) {
2411
                $sql .= " AND p.ref_ext = '" . $this->db->escape($ref_ext) . "'";
2412
            } elseif ($barcode) {
2413
                $sql .= " AND p.barcode = '" . $this->db->escape($barcode) . "'";
2414
            }
2415
        }
2416
        if ($separatedStock) {
2417
            $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,";
2418
            $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,";
2419
            $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,";
2420
            $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units,";
2421
            $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,";
2422
            if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
2423
                $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,";
2424
            } else {
2425
                $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,";
2426
            }
2427
            if ($separatedEntityPMP) {
2428
                $sql .= " ppe.pmp,";
2429
            } else {
2430
                $sql .= " p.pmp,";
2431
            }
2432
            $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,";
2433
            $sql .= " p.fk_price_expression, p.price_autogen, p.model_pdf";
2434
            $sql .= " ,p.price_label";
2435
            if (!$separatedStock) {
2436
                $sql .= ", p.stock";
2437
            }
2438
        }
2439
2440
        $resql = $this->db->query($sql);
2441
        if ($resql) {
2442
            unset($this->oldcopy);
2443
2444
            if ($this->db->num_rows($resql) > 0) {
2445
                $obj = $this->db->fetch_object($resql);
2446
2447
                $this->id = $obj->rowid;
2448
                $this->ref = $obj->ref;
2449
                $this->ref_ext = $obj->ref_ext;
2450
                $this->label = $obj->label;
2451
                $this->description = $obj->description;
2452
                $this->url = $obj->url;
2453
                $this->note_public = $obj->note_public;
2454
                $this->note_private = $obj->note_private;
2455
                $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

2455
                /** @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...
2456
2457
                $this->type = $obj->fk_product_type;
2458
                $this->price_label = $obj->price_label;
2459
                $this->status = $obj->tosell;
2460
                $this->status_buy = $obj->tobuy;
2461
                $this->status_batch = $obj->tobatch;
2462
                $this->sell_or_eat_by_mandatory = $obj->sell_or_eat_by_mandatory;
2463
                $this->batch_mask = $obj->batch_mask;
2464
2465
                $this->customcode = $obj->customcode;
2466
                $this->country_id = $obj->fk_country;
2467
                $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...
2468
                $this->state_id = $obj->fk_state;
2469
                $this->lifetime = $obj->lifetime;
2470
                $this->qc_frequency = $obj->qc_frequency;
2471
                $this->price = $obj->price;
2472
                $this->price_ttc = $obj->price_ttc;
2473
                $this->price_min = $obj->price_min;
2474
                $this->price_min_ttc = $obj->price_min_ttc;
2475
                $this->price_base_type = $obj->price_base_type;
2476
                $this->cost_price = $obj->cost_price;
2477
                $this->default_vat_code = $obj->default_vat_code;
2478
                $this->tva_tx = $obj->tva_tx;
2479
                //! French VAT NPR
2480
                $this->tva_npr = $obj->tva_npr;
2481
                //! Local taxes
2482
                $this->localtax1_tx = $obj->localtax1_tx;
2483
                $this->localtax2_tx = $obj->localtax2_tx;
2484
                $this->localtax1_type = $obj->localtax1_type;
2485
                $this->localtax2_type = $obj->localtax2_type;
2486
2487
                $this->finished = $obj->finished;
2488
                $this->fk_default_bom = $obj->fk_default_bom;
2489
2490
                $this->duration = $obj->duration;
2491
                $this->duration_value = $obj->duration ? substr($obj->duration, 0, dol_strlen($obj->duration) - 1) : null;
2492
                $this->duration_unit = $obj->duration ? substr($obj->duration, -1) : null;
2493
                $this->canvas = $obj->canvas;
2494
                $this->net_measure = $obj->net_measure;
2495
                $this->net_measure_units = $obj->net_measure_units;
2496
                $this->weight = $obj->weight;
2497
                $this->weight_units = $obj->weight_units;
2498
                $this->length = $obj->length;
2499
                $this->length_units = $obj->length_units;
2500
                $this->width = $obj->width;
2501
                $this->width_units = $obj->width_units;
2502
                $this->height = $obj->height;
2503
                $this->height_units = $obj->height_units;
2504
2505
                $this->surface = $obj->surface;
2506
                $this->surface_units = $obj->surface_units;
2507
                $this->volume = $obj->volume;
2508
                $this->volume_units = $obj->volume_units;
2509
                $this->barcode = $obj->barcode;
2510
                $this->barcode_type = $obj->fk_barcode_type;
2511
2512
                $this->accountancy_code_buy = $obj->accountancy_code_buy;
2513
                $this->accountancy_code_buy_intra = $obj->accountancy_code_buy_intra;
2514
                $this->accountancy_code_buy_export = $obj->accountancy_code_buy_export;
2515
                $this->accountancy_code_sell = $obj->accountancy_code_sell;
2516
                $this->accountancy_code_sell_intra = $obj->accountancy_code_sell_intra;
2517
                $this->accountancy_code_sell_export = $obj->accountancy_code_sell_export;
2518
2519
                $this->fk_default_warehouse = $obj->fk_default_warehouse;
2520
                $this->fk_default_workstation = $obj->fk_default_workstation;
2521
                $this->seuil_stock_alerte = $obj->seuil_stock_alerte;
2522
                $this->desiredstock = $obj->desiredstock;
2523
                $this->stock_reel = $obj->stock;
2524
                $this->pmp = $obj->pmp;
2525
2526
                $this->date_creation = $obj->datec;
2527
                $this->date_modification = $obj->tms;
2528
                $this->import_key = $obj->import_key;
2529
                $this->entity = $obj->entity;
2530
2531
                $this->ref_ext = $obj->ref_ext;
2532
                $this->fk_price_expression = $obj->fk_price_expression;
2533
                $this->fk_unit = $obj->fk_unit;
2534
                $this->price_autogen = $obj->price_autogen;
2535
                $this->model_pdf = $obj->model_pdf;
2536
                $this->last_main_doc = $obj->last_main_doc;
2537
2538
                $this->mandatory_period = $obj->mandatory_period;
2539
2540
                $this->db->free($resql);
2541
2542
                // fetch optionals attributes and labels
2543
                $this->fetch_optionals();
2544
2545
                // Multilangs
2546
                if (getDolGlobalInt('MAIN_MULTILANGS') && empty($ignore_lang_load)) {
2547
                    $this->getMultiLangs();
2548
                }
2549
2550
                // Load multiprices array
2551
                if (getDolGlobalString('PRODUIT_MULTIPRICES') && empty($ignore_price_load)) {                // prices per segment
2552
                    $produit_multiprices_limit = getDolGlobalString('PRODUIT_MULTIPRICES_LIMIT');
2553
                    for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
2554
                        $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
2555
                        $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
2556
                        $sql .= " ,price_label";
2557
                        $sql .= " FROM " . $this->db->prefix() . "product_price";
2558
                        $sql .= " WHERE entity IN (" . getEntity('productprice') . ")";
2559
                        $sql .= " AND price_level=" . ((int)$i);
2560
                        $sql .= " AND fk_product = " . ((int)$this->id);
2561
                        $sql .= " ORDER BY date_price DESC, rowid DESC";    // Get the most recent line
2562
                        $sql .= " LIMIT 1";                                 // Only the first one
2563
                        $resql = $this->db->query($sql);
2564
                        if ($resql) {
2565
                            $result = $this->db->fetch_array($resql);
2566
2567
                            $this->multiprices[$i] = $result ? $result["price"] : null;
2568
                            $this->multiprices_ttc[$i] = $result ? $result["price_ttc"] : null;
2569
                            $this->multiprices_min[$i] = $result ? $result["price_min"] : null;
2570
                            $this->multiprices_min_ttc[$i] = $result ? $result["price_min_ttc"] : null;
2571
                            $this->multiprices_base_type[$i] = $result ? $result["price_base_type"] : null;
2572
                            // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
2573
                            $this->multiprices_tva_tx[$i] = $result ? $result["tva_tx"] . ($result ? ' (' . $result['default_vat_code'] . ')' : '') : null;
2574
                            $this->multiprices_recuperableonly[$i] = $result ? $result["recuperableonly"] : null;
2575
2576
                            // Price by quantity
2577
                            /*
2578
                             $this->prices_by_qty[$i]=$result["price_by_qty"];
2579
                             $this->prices_by_qty_id[$i]=$result["rowid"];
2580
                             // Récuperation de la liste des prix selon qty si flag positionné
2581
                             if ($this->prices_by_qty[$i] == 1)
2582
                             {
2583
                             $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
2584
                             $sql.= " FROM ".$this->db->prefix()."product_price_by_qty";
2585
                             $sql.= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
2586
                             $sql.= " ORDER BY quantity ASC";
2587
2588
                             $resql = $this->db->query($sql);
2589
                             if ($resql)
2590
                             {
2591
                             $resultat=array();
2592
                             $ii=0;
2593
                             while ($result= $this->db->fetch_array($resql)) {
2594
                             $resultat[$ii]=array();
2595
                             $resultat[$ii]["rowid"]=$result["rowid"];
2596
                             $resultat[$ii]["price"]= $result["price"];
2597
                             $resultat[$ii]["unitprice"]= $result["unitprice"];
2598
                             $resultat[$ii]["quantity"]= $result["quantity"];
2599
                             $resultat[$ii]["remise_percent"]= $result["remise_percent"];
2600
                             $resultat[$ii]["remise"]= $result["remise"];                    // deprecated
2601
                             $resultat[$ii]["price_base_type"]= $result["price_base_type"];
2602
                             $ii++;
2603
                             }
2604
                             $this->prices_by_qty_list[$i]=$resultat;
2605
                             }
2606
                             else
2607
                             {
2608
                             dol_print_error($this->db);
2609
                             return -1;
2610
                             }
2611
                             }*/
2612
                        } else {
2613
                            $this->error = $this->db->lasterror;
2614
                            return -1;
2615
                        }
2616
                    }
2617
                } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES') && empty($ignore_price_load)) {            // prices per customers
2618
                    // Nothing loaded by default. List may be very long.
2619
                } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY') && empty($ignore_price_load)) {    // prices per quantity
2620
                    $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
2621
                    $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid";
2622
                    $sql .= " FROM " . $this->db->prefix() . "product_price";
2623
                    $sql .= " WHERE fk_product = " . ((int)$this->id);
2624
                    $sql .= " ORDER BY date_price DESC, rowid DESC";
2625
                    $sql .= " LIMIT 1";
2626
2627
                    $resql = $this->db->query($sql);
2628
                    if ($resql) {
2629
                        $result = $this->db->fetch_array($resql);
2630
2631
                        if ($result) {
2632
                            // Price by quantity
2633
                            $this->prices_by_qty[0] = $result["price_by_qty"];
2634
                            $this->prices_by_qty_id[0] = $result["rowid"];
2635
                            // Récuperation de la liste des prix selon qty si flag positionné
2636
                            if ($this->prices_by_qty[0] == 1) {
2637
                                $sql = "SELECT rowid,price, unitprice, quantity, remise_percent, remise, remise, price_base_type";
2638
                                $sql .= " FROM " . $this->db->prefix() . "product_price_by_qty";
2639
                                $sql .= " WHERE fk_product_price = " . ((int)$this->prices_by_qty_id[0]);
2640
                                $sql .= " ORDER BY quantity ASC";
2641
2642
                                $resql = $this->db->query($sql);
2643
                                if ($resql) {
2644
                                    $resultat = array();
2645
                                    $ii = 0;
2646
                                    while ($result = $this->db->fetch_array($resql)) {
2647
                                        $resultat[$ii] = array();
2648
                                        $resultat[$ii]["rowid"] = $result["rowid"];
2649
                                        $resultat[$ii]["price"] = $result["price"];
2650
                                        $resultat[$ii]["unitprice"] = $result["unitprice"];
2651
                                        $resultat[$ii]["quantity"] = $result["quantity"];
2652
                                        $resultat[$ii]["remise_percent"] = $result["remise_percent"];
2653
                                        //$resultat[$ii]["remise"]= $result["remise"];                    // deprecated
2654
                                        $resultat[$ii]["price_base_type"] = $result["price_base_type"];
2655
                                        $ii++;
2656
                                    }
2657
                                    $this->prices_by_qty_list[0] = $resultat;
2658
                                } else {
2659
                                    $this->error = $this->db->lasterror;
2660
                                    return -1;
2661
                                }
2662
                            }
2663
                        }
2664
                    } else {
2665
                        $this->error = $this->db->lasterror;
2666
                        return -1;
2667
                    }
2668
                } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES') && empty($ignore_price_load)) {    // prices per customer and quantity
2669
                    $produit_multiprices_limit = getDolGlobalString('PRODUIT_MULTIPRICES_LIMIT');
2670
                    for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
2671
                        $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
2672
                        $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
2673
                        $sql .= " FROM " . $this->db->prefix() . "product_price";
2674
                        $sql .= " WHERE entity IN (" . getEntity('productprice') . ")";
2675
                        $sql .= " AND price_level=" . ((int)$i);
2676
                        $sql .= " AND fk_product = " . ((int)$this->id);
2677
                        $sql .= " ORDER BY date_price DESC, rowid DESC";
2678
                        $sql .= " LIMIT 1";
2679
                        $resql = $this->db->query($sql);
2680
                        if (!$resql) {
2681
                            $this->error = $this->db->lasterror;
2682
                            return -1;
2683
                        } elseif ($result = $this->db->fetch_array($resql)) {
2684
                            $this->multiprices[$i] = (!empty($result["price"]) ? $result["price"] : 0);
2685
                            $this->multiprices_ttc[$i] = (!empty($result["price_ttc"]) ? $result["price_ttc"] : 0);
2686
                            $this->multiprices_min[$i] = (!empty($result["price_min"]) ? $result["price_min"] : 0);
2687
                            $this->multiprices_min_ttc[$i] = (!empty($result["price_min_ttc"]) ? $result["price_min_ttc"] : 0);
2688
                            $this->multiprices_base_type[$i] = (!empty($result["price_base_type"]) ? $result["price_base_type"] : '');
2689
                            // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
2690
                            $this->multiprices_tva_tx[$i] = (!empty($result["tva_tx"]) ? $result["tva_tx"] : 0); // TODO Add ' ('.$result['default_vat_code'].')'
2691
                            $this->multiprices_recuperableonly[$i] = (!empty($result["recuperableonly"]) ? $result["recuperableonly"] : 0);
2692
2693
                            // Price by quantity
2694
                            $this->prices_by_qty[$i] = (!empty($result["price_by_qty"]) ? $result["price_by_qty"] : 0);
2695
                            $this->prices_by_qty_id[$i] = (!empty($result["rowid"]) ? $result["rowid"] : 0);
2696
                            // Récuperation de la liste des prix selon qty si flag positionné
2697
                            if ($this->prices_by_qty[$i] == 1) {
2698
                                $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
2699
                                $sql .= " FROM " . $this->db->prefix() . "product_price_by_qty";
2700
                                $sql .= " WHERE fk_product_price = " . ((int)$this->prices_by_qty_id[$i]);
2701
                                $sql .= " ORDER BY quantity ASC";
2702
2703
                                $resql = $this->db->query($sql);
2704
                                if ($resql) {
2705
                                    $resultat = array();
2706
                                    $ii = 0;
2707
                                    while ($result = $this->db->fetch_array($resql)) {
2708
                                        $resultat[$ii] = array();
2709
                                        $resultat[$ii]["rowid"] = $result["rowid"];
2710
                                        $resultat[$ii]["price"] = $result["price"];
2711
                                        $resultat[$ii]["unitprice"] = $result["unitprice"];
2712
                                        $resultat[$ii]["quantity"] = $result["quantity"];
2713
                                        $resultat[$ii]["remise_percent"] = $result["remise_percent"];
2714
                                        $resultat[$ii]["remise"] = $result["remise"]; // deprecated
2715
                                        $resultat[$ii]["price_base_type"] = $result["price_base_type"];
2716
                                        $ii++;
2717
                                    }
2718
                                    $this->prices_by_qty_list[$i] = $resultat;
2719
                                } else {
2720
                                    $this->error = $this->db->lasterror;
2721
                                    return -1;
2722
                                }
2723
                            }
2724
                        }
2725
                    }
2726
                }
2727
2728
                if (isModEnabled('dynamicprices') && !empty($this->fk_price_expression) && empty($ignore_expression)) {
2729
                    include_once DOL_DOCUMENT_ROOT . '/product/dynamic_price/class/price_parser.class.php';
2730
                    $priceparser = new PriceParser($this->db);
2731
                    $price_result = $priceparser->parseProduct($this);
2732
                    if ($price_result >= 0) {
2733
                        $this->price = $price_result;
2734
                        // Calculate the VAT
2735
                        $this->price_ttc = (float)price2num($this->price) * (1 + ($this->tva_tx / 100));
2736
                        $this->price_ttc = (float)price2num($this->price_ttc, 'MU');
2737
                    }
2738
                }
2739
2740
                // We should not load stock during the fetch. If someone need stock of product, he must call load_stock after fetching product.
2741
                // Instead we just init the stock_warehouse array
2742
                $this->stock_warehouse = array();
2743
2744
                return 1;
2745
            } else {
2746
                return 0;
2747
            }
2748
        } else {
2749
            $this->error = $this->db->lasterror();
2750
            return -1;
2751
        }
2752
    }
2753
2754
    /**
2755
     *    Load array this->multilangs
2756
     *
2757
     * @return int        Return integer <0 if KO, >0 if OK
2758
     */
2759
    public function getMultiLangs()
2760
    {
2761
        global $langs;
2762
2763
        $current_lang = $langs->getDefaultLang();
2764
2765
        $sql = "SELECT lang, label, description, note as other";
2766
        $sql .= " FROM " . $this->db->prefix() . "product_lang";
2767
        $sql .= " WHERE fk_product = " . ((int)$this->id);
2768
2769
        $result = $this->db->query($sql);
2770
        if ($result) {
2771
            while ($obj = $this->db->fetch_object($result)) {
2772
                //print 'lang='.$obj->lang.' current='.$current_lang.'<br>';
2773
                if ($obj->lang == $current_lang) {  // si on a les traduct. dans la langue courante on les charge en infos principales.
2774
                    $this->label = $obj->label;
2775
                    $this->description = $obj->description;
2776
                    $this->other = $obj->other;
2777
                }
2778
                $this->multilangs[(string)$obj->lang]["label"] = $obj->label;
2779
                $this->multilangs[(string)$obj->lang]["description"] = $obj->description;
2780
                $this->multilangs[(string)$obj->lang]["other"] = $obj->other;
2781
            }
2782
            return 1;
2783
        } else {
2784
            $this->error = "Error: " . $this->db->lasterror() . " - " . $sql;
2785
            return -1;
2786
        }
2787
    }
2788
2789
    /**
2790
     *    Update or add a translation for a product
2791
     *
2792
     * @param User $user Object user making update
2793
     * @return int        Return integer <0 if KO, >0 if OK
2794
     */
2795
    public function setMultiLangs($user)
2796
    {
2797
        global $conf, $langs;
2798
2799
        $langs_available = $langs->get_available_languages(DOL_DOCUMENT_ROOT, 0, 2);
2800
        $current_lang = $langs->getDefaultLang();
2801
2802
        foreach ($langs_available as $key => $value) {
2803
            if ($key == $current_lang) {
2804
                $sql = "SELECT rowid";
2805
                $sql .= " FROM " . $this->db->prefix() . "product_lang";
2806
                $sql .= " WHERE fk_product = " . ((int)$this->id);
2807
                $sql .= " AND lang = '" . $this->db->escape($key) . "'";
2808
2809
                $result = $this->db->query($sql);
2810
2811
                if ($this->db->num_rows($result)) { // if there is already a description line for this language
2812
                    $sql2 = "UPDATE " . $this->db->prefix() . "product_lang";
2813
                    $sql2 .= " SET ";
2814
                    $sql2 .= " label='" . $this->db->escape($this->label) . "',";
2815
                    $sql2 .= " description='" . $this->db->escape($this->description) . "'";
2816
                    if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2817
                        $sql2 .= ", note='" . $this->db->escape($this->other) . "'";
2818
                    }
2819
                    $sql2 .= " WHERE fk_product = " . ((int)$this->id) . " AND lang = '" . $this->db->escape($key) . "'";
2820
                } else {
2821
                    $sql2 = "INSERT INTO " . $this->db->prefix() . "product_lang (fk_product, lang, label, description";
2822
                    if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2823
                        $sql2 .= ", note";
2824
                    }
2825
                    $sql2 .= ")";
2826
                    $sql2 .= " VALUES(" . ((int)$this->id) . ",'" . $this->db->escape($key) . "','" . $this->db->escape($this->label) . "',";
2827
                    $sql2 .= " '" . $this->db->escape($this->description) . "'";
2828
                    if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2829
                        $sql2 .= ", '" . $this->db->escape($this->other) . "'";
2830
                    }
2831
                    $sql2 .= ")";
2832
                }
2833
                dol_syslog(get_only_class($this) . '::setMultiLangs key = current_lang = ' . $key);
2834
                if (!$this->db->query($sql2)) {
2835
                    $this->error = $this->db->lasterror();
2836
                    return -1;
2837
                }
2838
            } elseif (isset($this->multilangs[$key])) {
2839
                if (empty($this->multilangs["$key"]["label"])) {
2840
                    $this->errors[] = $key . ' : ' . $langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("Label"));
2841
                    return -1;
2842
                }
2843
2844
                $sql = "SELECT rowid";
2845
                $sql .= " FROM " . $this->db->prefix() . "product_lang";
2846
                $sql .= " WHERE fk_product = " . ((int)$this->id);
2847
                $sql .= " AND lang = '" . $this->db->escape($key) . "'";
2848
2849
                $result = $this->db->query($sql);
2850
2851
                if ($this->db->num_rows($result)) { // if there is already a description line for this language
2852
                    $sql2 = "UPDATE " . $this->db->prefix() . "product_lang";
2853
                    $sql2 .= " SET ";
2854
                    $sql2 .= " label = '" . $this->db->escape($this->multilangs["$key"]["label"]) . "',";
2855
                    $sql2 .= " description = '" . $this->db->escape($this->multilangs["$key"]["description"]) . "'";
2856
                    if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2857
                        $sql2 .= ", note = '" . $this->db->escape($this->multilangs["$key"]["other"]) . "'";
2858
                    }
2859
                    $sql2 .= " WHERE fk_product = " . ((int)$this->id) . " AND lang = '" . $this->db->escape($key) . "'";
2860
                } else {
2861
                    $sql2 = "INSERT INTO " . $this->db->prefix() . "product_lang (fk_product, lang, label, description";
2862
                    if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2863
                        $sql2 .= ", note";
2864
                    }
2865
                    $sql2 .= ")";
2866
                    $sql2 .= " VALUES(" . ((int)$this->id) . ",'" . $this->db->escape($key) . "','" . $this->db->escape($this->multilangs["$key"]["label"]) . "',";
2867
                    $sql2 .= " '" . $this->db->escape($this->multilangs["$key"]["description"]) . "'";
2868
                    if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2869
                        $sql2 .= ", '" . $this->db->escape($this->multilangs["$key"]["other"]) . "'";
2870
                    }
2871
                    $sql2 .= ")";
2872
                }
2873
2874
                // We do not save if main fields are empty
2875
                if ($this->multilangs["$key"]["label"] || $this->multilangs["$key"]["description"]) {
2876
                    if (!$this->db->query($sql2)) {
2877
                        $this->error = $this->db->lasterror();
2878
                        return -1;
2879
                    }
2880
                }
2881
            } else {
2882
                // language is not current language and we didn't provide a multilang description for this language
2883
            }
2884
        }
2885
2886
        // Call trigger
2887
        $result = $this->call_trigger('PRODUCT_SET_MULTILANGS', $user);
2888
        if ($result < 0) {
2889
            $this->error = $this->db->lasterror();
2890
            return -1;
2891
        }
2892
        // End call triggers
2893
2894
        return 1;
2895
    }
2896
2897
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2898
2899
    /**
2900
     * Return if object is a product.
2901
     *
2902
     * @return boolean     True if it's a product
2903
     */
2904
    public function isProduct()
2905
    {
2906
        return ($this->type == Product::TYPE_PRODUCT ? true : false);
2907
    }
2908
2909
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2910
2911
    /**
2912
     * Return if object is a product
2913
     *
2914
     * @return boolean     True if it's a service
2915
     */
2916
    public function isService()
2917
    {
2918
        return ($this->type == Product::TYPE_SERVICE ? true : false);
2919
    }
2920
2921
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2922
2923
    /**
2924
     *  Adjust stock in a warehouse for product with batch number
2925
     *
2926
     * @param User $user user asking change
2927
     * @param int $id_entrepot id of warehouse
2928
     * @param double $nbpiece nb of units (should be always positive, use $movement to decide if we add or remove)
2929
     * @param int $movement 0 = add, 1 = remove
2930
     * @param string $label Label of stock movement
2931
     * @param double $price Price to use for stock eval
2932
     * @param int|string $dlc eat-by date
2933
     * @param int|string $dluo sell-by date
2934
     * @param string $lot Lot number
2935
     * @param string $inventorycode Inventory code
2936
     * @param string $origin_element Origin element type
2937
     * @param int $origin_id Origin id of element
2938
     * @param int $disablestockchangeforsubproduct Disable stock change for sub-products of kit (useful only if product is a subproduct)
2939
     * @param Extrafields $extrafields Array of extrafields
2940
     * @param boolean $force_update_batch Force update batch
2941
     * @return int                      Return integer <0 if KO, >0 if OK
2942
     */
2943
    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)
2944
    {
2945
        // phpcs:enable
2946
        if ($id_entrepot) {
2947
            $this->db->begin();
2948
2949
            include_once DOL_DOCUMENT_ROOT . '/product/stock/class/mouvementstock.class.php';
2950
2951
            if ($nbpiece < 0) {
2952
                if (!$movement) {
2953
                    $movement = 1;
2954
                }
2955
                $nbpiece = abs($nbpiece);
2956
            }
2957
2958
            $op = array();
2959
            $op[0] = "+" . trim((string)$nbpiece);
2960
            $op[1] = "-" . trim((string)$nbpiece);
2961
2962
            $movementstock = new MouvementStock($this->db);
2963
            $movementstock->setOrigin($origin_element, $origin_id); // Set ->origin_type and ->fk_origin
2964
            $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', $dlc, $dluo, $lot, false, 0, $disablestockchangeforsubproduct, 0, $force_update_batch);
2965
2966
            if ($result >= 0) {
2967
                if ($extrafields) {
2968
                    $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
2969
                    $movementstock->array_options = $array_options;
2970
                    $movementstock->insertExtraFields();
2971
                }
2972
                $this->db->commit();
2973
                return 1;
2974
            } else {
2975
                $this->error = $movementstock->error;
2976
                $this->errors = $movementstock->errors;
2977
2978
                $this->db->rollback();
2979
                return -1;
2980
            }
2981
        }
2982
        return -1;
2983
    }
2984
2985
2986
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2987
2988
    /**
2989
     *  Adjust stock in a warehouse for product
2990
     *
2991
     * @param User $user user asking change
2992
     * @param int $id_entrepot id of warehouse
2993
     * @param double $nbpiece nb of units (should be always positive, use $movement to decide if we add or remove)
2994
     * @param int $movement 0 = add, 1 = remove
2995
     * @param string $label Label of stock movement
2996
     * @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.
2997
     * @param string $inventorycode Inventory code
2998
     * @param string $origin_element Origin element type
2999
     * @param int $origin_id Origin id of element
3000
     * @param int $disablestockchangeforsubproduct Disable stock change for sub-products of kit (useful only if product is a subproduct)
3001
     * @param Extrafields $extrafields Array of extrafields
3002
     * @return int                    Return integer <0 if KO, >0 if OK
3003
     */
3004
    public function correct_stock($user, $id_entrepot, $nbpiece, $movement, $label = '', $price = 0, $inventorycode = '', $origin_element = '', $origin_id = null, $disablestockchangeforsubproduct = 0, $extrafields = null)
3005
    {
3006
        // phpcs:enable
3007
        if ($id_entrepot) {
3008
            $this->db->begin();
3009
3010
            include_once DOL_DOCUMENT_ROOT . '/product/stock/class/mouvementstock.class.php';
3011
3012
            if ($nbpiece < 0) {
3013
                if (!$movement) {
3014
                    $movement = 1;
3015
                }
3016
                $nbpiece = abs($nbpiece);
3017
            }
3018
            $op = array();
3019
            $op[0] = "+" . trim((string)$nbpiece);
3020
            $op[1] = "-" . trim((string)$nbpiece);
3021
3022
            $movementstock = new MouvementStock($this->db);
3023
            $movementstock->setOrigin($origin_element, $origin_id); // Set ->origin_type and ->origin_id
3024
            $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', '', '', '', false, 0, $disablestockchangeforsubproduct);
3025
3026
            if ($result >= 0) {
3027
                if ($extrafields) {
3028
                    $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
3029
                    $movementstock->array_options = $array_options;
3030
                    $movementstock->insertExtraFields();
3031
                }
3032
                $this->db->commit();
3033
                return 1;
3034
            } else {
3035
                $this->error = $movementstock->error;
3036
                $this->errors = $movementstock->errors;
3037
3038
                $this->db->rollback();
3039
                return -1;
3040
            }
3041
        }
3042
3043
        return -1;
3044
    }
3045
3046
3047
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3048
3049
    /**
3050
     *  Delete a product from database (if not used)
3051
     *
3052
     * @param User $user User (object) deleting product
3053
     * @param int $notrigger Do not execute trigger
3054
     * @return int                    Return integer < 0 if KO, 0 = Not possible, > 0 if OK
3055
     */
3056
    public function delete(User $user, $notrigger = 0)
3057
    {
3058
        global $conf, $langs;
3059
        include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
3060
3061
        $error = 0;
3062
3063
        // Check parameters
3064
        if (empty($this->id)) {
3065
            $this->error = "Object must be fetched before calling delete";
3066
            return -1;
3067
        }
3068
        if (($this->type == Product::TYPE_PRODUCT && !$user->hasRight('produit', 'supprimer')) || ($this->type == Product::TYPE_SERVICE && !$user->hasRight('service', 'supprimer'))) {
3069
            $this->error = "ErrorForbidden";
3070
            return 0;
3071
        }
3072
3073
        $objectisused = $this->isObjectUsed($this->id);
3074
        if (empty($objectisused)) {
3075
            $this->db->begin();
3076
3077
            if (!$error && empty($notrigger)) {
3078
                // Call trigger
3079
                $result = $this->call_trigger('PRODUCT_DELETE', $user);
3080
                if ($result < 0) {
3081
                    $error++;
3082
                }
3083
                // End call triggers
3084
            }
3085
3086
            // Delete from product_batch on product delete
3087
            if (!$error) {
3088
                $sql = "DELETE FROM " . $this->db->prefix() . 'product_batch';
3089
                $sql .= " WHERE fk_product_stock IN (";
3090
                $sql .= "SELECT rowid FROM " . $this->db->prefix() . 'product_stock';
3091
                $sql .= " WHERE fk_product = " . ((int)$this->id) . ")";
3092
3093
                $result = $this->db->query($sql);
3094
                if (!$result) {
3095
                    $error++;
3096
                    $this->errors[] = $this->db->lasterror();
3097
                }
3098
            }
3099
3100
            // Delete all child tables
3101
            if (!$error) {
3102
                $elements = array('product_fournisseur_price', 'product_price', 'product_lang', 'categorie_product', 'product_stock', 'product_customer_price', 'product_lot'); // product_batch is done before
3103
                foreach ($elements as $table) {
3104
                    if (!$error) {
3105
                        $sql = "DELETE FROM " . $this->db->prefix() . $table;
3106
                        $sql .= " WHERE fk_product = " . (int)$this->id;
3107
3108
                        $result = $this->db->query($sql);
3109
                        if (!$result) {
3110
                            $error++;
3111
                            $this->errors[] = $this->db->lasterror();
3112
                        }
3113
                    }
3114
                }
3115
            }
3116
3117
            if (!$error) {
3118
                //If it is a parent product, then we remove the association with child products
3119
                $prodcomb = new ProductAttributeCombination();
3120
3121
                if ($prodcomb->deleteByFkProductParent($user, $this->id) < 0) {
0 ignored issues
show
Bug introduced by
Are you sure the usage of $prodcomb->deleteByFkPro...arent($user, $this->id) targeting Dolibarr\Code\Variants\M...leteByFkProductParent() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3122
                    $error++;
3123
                    $this->errors[] = 'Error deleting combinations';
3124
                }
3125
3126
                //We also check if it is a child product
3127
                if (!$error && ($prodcomb->fetchByFkProductChild($this->id) > 0) && ($prodcomb->delete($user) < 0)) {
3128
                    $error++;
3129
                    $this->errors[] = 'Error deleting child combination';
3130
                }
3131
            }
3132
3133
            // Delete from product_association
3134
            if (!$error) {
3135
                $sql = "DELETE FROM " . $this->db->prefix() . "product_association";
3136
                $sql .= " WHERE fk_product_pere = " . (int)$this->id . " OR fk_product_fils = " . (int)$this->id;
3137
3138
                $result = $this->db->query($sql);
3139
                if (!$result) {
3140
                    $error++;
3141
                    $this->errors[] = $this->db->lasterror();
3142
                }
3143
            }
3144
3145
            // Remove extrafields
3146
            if (!$error) {
3147
                $result = $this->deleteExtraFields();
3148
                if ($result < 0) {
3149
                    $error++;
3150
                    dol_syslog(get_only_class($this) . "::delete error -4 " . $this->error, LOG_ERR);
3151
                }
3152
            }
3153
3154
            // Delete product
3155
            if (!$error) {
3156
                $sqlz = "DELETE FROM " . $this->db->prefix() . "product";
3157
                $sqlz .= " WHERE rowid = " . (int)$this->id;
3158
3159
                $resultz = $this->db->query($sqlz);
3160
                if (!$resultz) {
3161
                    $error++;
3162
                    $this->errors[] = $this->db->lasterror();
3163
                }
3164
            }
3165
3166
            // Delete record into ECM index and physically
3167
            if (!$error) {
3168
                $res = $this->deleteEcmFiles(0); // Deleting files physically is done later with the dol_delete_dir_recursive
3169
                $res = $this->deleteEcmFiles(1); // Deleting files physically is done later with the dol_delete_dir_recursive
3170
                if (!$res) {
3171
                    $error++;
3172
                }
3173
            }
3174
3175
            if (!$error) {
3176
                // We remove directory
3177
                $ref = dol_sanitizeFileName($this->ref);
3178
                if ($conf->product->dir_output) {
3179
                    $dir = $conf->product->dir_output . "/" . $ref;
3180
                    if (file_exists($dir)) {
3181
                        $res = @dol_delete_dir_recursive($dir);
3182
                        if (!$res) {
3183
                            $this->errors[] = 'ErrorFailToDeleteDir';
3184
                            $error++;
3185
                        }
3186
                    }
3187
                }
3188
            }
3189
3190
            if (!$error) {
3191
                $this->db->commit();
3192
                return 1;
3193
            } else {
3194
                foreach ($this->errors as $errmsg) {
3195
                    dol_syslog(get_only_class($this) . "::delete " . $errmsg, LOG_ERR);
3196
                    $this->error .= ($this->error ? ', ' . $errmsg : $errmsg);
3197
                }
3198
                $this->db->rollback();
3199
                return -$error;
3200
            }
3201
        } else {
3202
            $this->error = "ErrorRecordIsUsedCantDelete";
3203
            return 0;
3204
        }
3205
    }
3206
3207
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3208
3209
    /**
3210
     * Get sell or eat by mandatory label
3211
     *
3212
     * @return  string  Sell or eat by mandatory label
3213
     */
3214
    public function getSellOrEatByMandatoryLabel()
3215
    {
3216
        $sellOrEatByMandatoryLabel = '';
3217
3218
        $sellOrEatByMandatoryList = self::getSellOrEatByMandatoryList();
3219
        if (isset($sellOrEatByMandatoryList[$this->sell_or_eat_by_mandatory])) {
3220
            $sellOrEatByMandatoryLabel = $sellOrEatByMandatoryList[$this->sell_or_eat_by_mandatory];
3221
        }
3222
3223
        return $sellOrEatByMandatoryLabel;
3224
    }
3225
3226
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3227
3228
    /**
3229
     * Get sell or eat by mandatory list
3230
     *
3231
     * @return  array   Sell or eat by mandatory list
3232
     */
3233
    public static function getSellOrEatByMandatoryList()
3234
    {
3235
        global $langs;
3236
3237
        $sellByLabel = $langs->trans('SellByDate');
3238
        $eatByLabel = $langs->trans('EatByDate');
3239
        return array(
3240
            self::SELL_OR_EAT_BY_MANDATORY_ID_NONE => $langs->trans('BatchSellOrEatByMandatoryNone'),
3241
            self::SELL_OR_EAT_BY_MANDATORY_ID_SELL_BY => $sellByLabel,
3242
            self::SELL_OR_EAT_BY_MANDATORY_ID_EAT_BY => $eatByLabel,
3243
            self::SELL_OR_EAT_BY_MANDATORY_ID_SELL_AND_EAT => $langs->trans('BatchSellOrEatByMandatoryAll', $sellByLabel, $eatByLabel),
3244
        );
3245
    }
3246
3247
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3248
3249
    /**
3250
     *    Delete a language for this product
3251
     *
3252
     * @param string $langtodelete Language code to delete
3253
     * @param User $user Object user making delete
3254
     *
3255
     * @return int                            Return integer <0 if KO, >0 if OK
3256
     */
3257
    public function delMultiLangs($langtodelete, $user)
3258
    {
3259
        $sql = "DELETE FROM " . $this->db->prefix() . "product_lang";
3260
        $sql .= " WHERE fk_product = " . ((int)$this->id) . " AND lang = '" . $this->db->escape($langtodelete) . "'";
3261
3262
        dol_syslog(get_only_class($this) . '::delMultiLangs', LOG_DEBUG);
3263
        $result = $this->db->query($sql);
3264
        if ($result) {
3265
            // Call trigger
3266
            $result = $this->call_trigger('PRODUCT_DEL_MULTILANGS', $user);
3267
            if ($result < 0) {
3268
                $this->error = $this->db->lasterror();
3269
                dol_syslog(get_only_class($this) . '::delMultiLangs error=' . $this->error, LOG_ERR);
3270
                return -1;
3271
            }
3272
            // End call triggers
3273
            return 1;
3274
        } else {
3275
            $this->error = $this->db->lasterror();
3276
            dol_syslog(get_only_class($this) . '::delMultiLangs error=' . $this->error, LOG_ERR);
3277
            return -1;
3278
        }
3279
    }
3280
3281
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3282
3283
    /**
3284
     * Sets an accountancy code for a product.
3285
     * Also calls PRODUCT_MODIFY trigger when modified
3286
     *
3287
     * @param string $type It can be 'buy', 'buy_intra', 'buy_export', 'sell', 'sell_intra' or 'sell_export'
3288
     * @param string $value Accountancy code
3289
     * @return  int             Return integer <0 KO >0 OK
3290
     */
3291
    public function setAccountancyCode($type, $value)
3292
    {
3293
        global $user, $langs, $conf;
3294
3295
        $error = 0;
3296
3297
        $this->db->begin();
3298
3299
        if ($type == 'buy') {
3300
            $field = 'accountancy_code_buy';
3301
        } elseif ($type == 'buy_intra') {
3302
            $field = 'accountancy_code_buy_intra';
3303
        } elseif ($type == 'buy_export') {
3304
            $field = 'accountancy_code_buy_export';
3305
        } elseif ($type == 'sell') {
3306
            $field = 'accountancy_code_sell';
3307
        } elseif ($type == 'sell_intra') {
3308
            $field = 'accountancy_code_sell_intra';
3309
        } elseif ($type == 'sell_export') {
3310
            $field = 'accountancy_code_sell_export';
3311
        } else {
3312
            return -1;
3313
        }
3314
3315
        $sql = "UPDATE " . $this->db->prefix() . $this->table_element . " SET ";
3316
        $sql .= "$field = '" . $this->db->escape($value) . "'";
3317
        $sql .= " WHERE rowid = " . ((int)$this->id);
3318
3319
        dol_syslog(__METHOD__, LOG_DEBUG);
3320
        $resql = $this->db->query($sql);
3321
3322
        if ($resql) {
3323
            // Call trigger
3324
            $result = $this->call_trigger('PRODUCT_MODIFY', $user);
3325
            if ($result < 0) {
3326
                $error++;
3327
            }
3328
            // End call triggers
3329
3330
            if ($error) {
3331
                $this->db->rollback();
3332
                return -1;
3333
            }
3334
3335
            $this->$field = $value;
3336
3337
            $this->db->commit();
3338
            return 1;
3339
        } else {
3340
            $this->error = $this->db->lasterror();
3341
            $this->db->rollback();
3342
            return -1;
3343
        }
3344
    }
3345
3346
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3347
3348
    /**
3349
     *  Delete a price line
3350
     *
3351
     * @param User $user Object user
3352
     * @param int $rowid Line id to delete
3353
     * @return int                Return integer <0 if KO, >0 if OK
3354
     */
3355
    public function log_price_delete($user, $rowid)
3356
    {
3357
        // phpcs:enable
3358
        $sql = "DELETE FROM " . $this->db->prefix() . "product_price_by_qty";
3359
        $sql .= " WHERE fk_product_price = " . ((int)$rowid);
3360
        $resql = $this->db->query($sql);
3361
3362
        $sql = "DELETE FROM " . $this->db->prefix() . "product_price";
3363
        $sql .= " WHERE rowid=" . ((int)$rowid);
3364
        $resql = $this->db->query($sql);
3365
        if ($resql) {
3366
            return 1;
3367
        } else {
3368
            $this->error = $this->db->lasterror();
3369
            return -1;
3370
        }
3371
    }
3372
3373
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3374
3375
    /**
3376
     * Return price of sell of a product for a seller/buyer/product.
3377
     *
3378
     * @param Societe $thirdparty_seller Seller
3379
     * @param Societe $thirdparty_buyer Buyer
3380
     * @param int $pqp Id of product price per quantity if a selection was done of such a price
3381
     * @return  array                               Array of price information array('pu_ht'=> , 'pu_ttc'=> , 'tva_tx'=>'X.Y (code)', ...), 'tva_npr'=>0, ...)
3382
     * @see get_buyprice(), find_min_price_product_fournisseur()
3383
     */
3384
    public function getSellPrice($thirdparty_seller, $thirdparty_buyer, $pqp = 0)
3385
    {
3386
        global $conf, $hookmanager, $action;
3387
3388
        // Call hook if any
3389
        if (is_object($hookmanager)) {
3390
            $parameters = array('thirdparty_seller' => $thirdparty_seller, 'thirdparty_buyer' => $thirdparty_buyer, 'pqp' => $pqp);
3391
            // Note that $action and $object may have been modified by some hooks
3392
            $reshook = $hookmanager->executeHooks('getSellPrice', $parameters, $this, $action);
3393
            if ($reshook > 0) {
3394
                return $hookmanager->resArray;
3395
            }
3396
        }
3397
3398
        // Update if prices fields are defined
3399
        $tva_tx = get_default_tva($thirdparty_seller, $thirdparty_buyer, $this->id);
3400
        $tva_npr = get_default_npr($thirdparty_seller, $thirdparty_buyer, $this->id);
3401
        if (empty($tva_tx)) {
3402
            $tva_npr = 0;
3403
        }
3404
3405
        $pu_ht = $this->price;
3406
        $pu_ttc = $this->price_ttc;
3407
        $price_min = $this->price_min;
3408
        $price_base_type = $this->price_base_type;
3409
3410
        // If price per segment
3411
        if (getDolGlobalString('PRODUIT_MULTIPRICES') && !empty($thirdparty_buyer->price_level)) {
3412
            $pu_ht = $this->multiprices[$thirdparty_buyer->price_level];
3413
            $pu_ttc = $this->multiprices_ttc[$thirdparty_buyer->price_level];
3414
            $price_min = $this->multiprices_min[$thirdparty_buyer->price_level];
3415
            $price_base_type = $this->multiprices_base_type[$thirdparty_buyer->price_level];
3416
            if (getDolGlobalString('PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL')) {  // using this option is a bug. kept for backward compatibility
3417
                if (isset($this->multiprices_tva_tx[$thirdparty_buyer->price_level])) {
3418
                    $tva_tx = $this->multiprices_tva_tx[$thirdparty_buyer->price_level];
3419
                }
3420
                if (isset($this->multiprices_recuperableonly[$thirdparty_buyer->price_level])) {
3421
                    $tva_npr = $this->multiprices_recuperableonly[$thirdparty_buyer->price_level];
3422
                }
3423
                if (empty($tva_tx)) {
3424
                    $tva_npr = 0;
3425
                }
3426
            }
3427
        } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES')) {
3428
            // If price per customer
3429
            require_once constant('DOL_DOCUMENT_ROOT') . '/product/class/productcustomerprice.class.php';
3430
3431
            $prodcustprice = new ProductCustomerPrice($this->db);
3432
3433
            $filter = array('t.fk_product' => $this->id, 't.fk_soc' => $thirdparty_buyer->id);
3434
3435
            $result = $prodcustprice->fetchAll('', '', 0, 0, $filter);
3436
            if ($result) {
3437
                if (count($prodcustprice->lines) > 0) {
3438
                    $pu_ht = price($prodcustprice->lines[0]->price);
3439
                    $price_min = price($prodcustprice->lines[0]->price_min);
3440
                    $pu_ttc = price($prodcustprice->lines[0]->price_ttc);
3441
                    $price_base_type = $prodcustprice->lines[0]->price_base_type;
3442
                    $tva_tx = $prodcustprice->lines[0]->tva_tx;
3443
                    if ($prodcustprice->lines[0]->default_vat_code && !preg_match('/\(.*\)/', $tva_tx)) {
3444
                        $tva_tx .= ' (' . $prodcustprice->lines[0]->default_vat_code . ')';
3445
                    }
3446
                    $tva_npr = $prodcustprice->lines[0]->recuperableonly;
3447
                    if (empty($tva_tx)) {
3448
                        $tva_npr = 0;
3449
                    }
3450
                }
3451
            }
3452
        } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY')) {
3453
            // If price per quantity
3454
            if ($this->prices_by_qty[0]) {
3455
                // yes, this product has some prices per quantity
3456
                // Search price into product_price_by_qty from $this->id
3457
                foreach ($this->prices_by_qty_list[0] as $priceforthequantityarray) {
3458
                    if ($priceforthequantityarray['rowid'] != $pqp) {
3459
                        continue;
3460
                    }
3461
                    // We found the price
3462
                    if ($priceforthequantityarray['price_base_type'] == 'HT') {
3463
                        $pu_ht = $priceforthequantityarray['unitprice'];
3464
                    } else {
3465
                        $pu_ttc = $priceforthequantityarray['unitprice'];
3466
                    }
3467
                    break;
3468
                }
3469
            }
3470
        } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES')) {
3471
            // If price per quantity and customer
3472
            if ($this->prices_by_qty[$thirdparty_buyer->price_level]) {
3473
                // yes, this product has some prices per quantity
3474
                // Search price into product_price_by_qty from $this->id
3475
                foreach ($this->prices_by_qty_list[$thirdparty_buyer->price_level] as $priceforthequantityarray) {
3476
                    if ($priceforthequantityarray['rowid'] != $pqp) {
3477
                        continue;
3478
                    }
3479
                    // We found the price
3480
                    if ($priceforthequantityarray['price_base_type'] == 'HT') {
3481
                        $pu_ht = $priceforthequantityarray['unitprice'];
3482
                    } else {
3483
                        $pu_ttc = $priceforthequantityarray['unitprice'];
3484
                    }
3485
                    break;
3486
                }
3487
            }
3488
        }
3489
3490
        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);
3491
    }
3492
3493
3494
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3495
3496
    /**
3497
     * Read price used by a provider.
3498
     * We enter as input couple prodfournprice/qty or triplet qty/product_id/fourn_ref.
3499
     * This also set some properties on product like ->buyprice, ->fourn_pu, ...
3500
     *
3501
     * @param int $prodfournprice Id du tarif = rowid table product_fournisseur_price
3502
     * @param double $qty Quantity asked or -1 to get first entry found
3503
     * @param int $product_id Filter on a particular product id
3504
     * @param string $fourn_ref Filter on a supplier price ref. 'none' to exclude ref in search.
3505
     * @param int $fk_soc If of supplier
3506
     * @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
3507
     * @see getSellPrice(), find_min_price_product_fournisseur()
3508
     */
3509
    public function get_buyprice($prodfournprice, $qty, $product_id = 0, $fourn_ref = '', $fk_soc = 0)
3510
    {
3511
        // phpcs:enable
3512
        global $action, $hookmanager;
3513
3514
        // Call hook if any
3515
        if (is_object($hookmanager)) {
3516
            $parameters = array(
3517
                'prodfournprice' => $prodfournprice,
3518
                'qty' => $qty,
3519
                'product_id' => $product_id,
3520
                'fourn_ref' => $fourn_ref,
3521
                'fk_soc' => $fk_soc,
3522
            );
3523
            // Note that $action and $object may have been modified by some hooks
3524
            $reshook = $hookmanager->executeHooks('getBuyPrice', $parameters, $this, $action);
3525
            if ($reshook > 0) {
3526
                return $hookmanager->resArray;
3527
            }
3528
        }
3529
3530
        $result = 0;
3531
3532
        // 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)
3533
        $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
3534
        $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,";
3535
        $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
3536
        $sql .= " pfp.packaging";
3537
        $sql .= " FROM " . $this->db->prefix() . "product_fournisseur_price as pfp";
3538
        $sql .= " WHERE pfp.rowid = " . ((int)$prodfournprice);
3539
        if ($qty > 0) {
3540
            $sql .= " AND pfp.quantity <= " . ((float)$qty);
3541
        }
3542
        $sql .= " ORDER BY pfp.quantity DESC";
3543
3544
        dol_syslog(get_only_class($this) . "::get_buyprice first search by prodfournprice/qty", LOG_DEBUG);
3545
        $resql = $this->db->query($sql);
3546
        if ($resql) {
3547
            $obj = $this->db->fetch_object($resql);
3548
            if ($obj && $obj->quantity > 0) {        // If we found a supplier prices from the id of supplier price
3549
                if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
3550
                    $prod_supplier = new ProductFournisseur($this->db);
3551
                    $prod_supplier->product_fourn_price_id = $obj->rowid;
3552
                    $prod_supplier->id = $obj->fk_product;
3553
                    $prod_supplier->fourn_qty = $obj->quantity;
3554
                    $prod_supplier->fourn_tva_tx = $obj->tva_tx;
3555
                    $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
3556
3557
                    include_once DOL_DOCUMENT_ROOT . '/product/dynamic_price/class/price_parser.class.php';
3558
                    $priceparser = new PriceParser($this->db);
3559
                    $price_result = $priceparser->parseProductSupplier($prod_supplier);
3560
                    if ($price_result >= 0) {
3561
                        $obj->price = $price_result;
3562
                    }
3563
                }
3564
                $this->product_fourn_price_id = $obj->rowid;
3565
                $this->buyprice = $obj->price; // deprecated
3566
                $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product of supplier
3567
                $this->fourn_price_base_type = 'HT'; // Price base type
3568
                $this->fourn_socid = $obj->fk_soc; // Company that offer this price
3569
                $this->ref_fourn = $obj->ref_supplier; // deprecated
3570
                $this->ref_supplier = $obj->ref_supplier; // Ref supplier
3571
                $this->desc_supplier = $obj->desc_supplier; // desc supplier
3572
                $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
3573
                $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
3574
                $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
3575
                $this->fourn_multicurrency_price = $obj->multicurrency_price;
3576
                $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
3577
                $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
3578
                $this->fourn_multicurrency_id = $obj->fk_multicurrency;
3579
                $this->fourn_multicurrency_code = $obj->multicurrency_code;
3580
                if (getDolGlobalString('PRODUCT_USE_SUPPLIER_PACKAGING')) {
3581
                    $this->packaging = $obj->packaging;
3582
                }
3583
                $result = $obj->fk_product;
3584
                return $result;
3585
            } else { // If not found
3586
                // 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.
3587
                $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
3588
                $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,";
3589
                $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
3590
                $sql .= " pfp.packaging";
3591
                $sql .= " FROM " . $this->db->prefix() . "product_fournisseur_price as pfp";
3592
                $sql .= " WHERE 1 = 1";
3593
                if ($product_id > 0) {
3594
                    $sql .= " AND pfp.fk_product = " . ((int)$product_id);
3595
                }
3596
                if ($fourn_ref != 'none') {
3597
                    $sql .= " AND pfp.ref_fourn = '" . $this->db->escape($fourn_ref) . "'";
3598
                }
3599
                if ($fk_soc > 0) {
3600
                    $sql .= " AND pfp.fk_soc = " . ((int)$fk_soc);
3601
                }
3602
                if ($qty > 0) {
3603
                    $sql .= " AND pfp.quantity <= " . ((float)$qty);
3604
                }
3605
                $sql .= " ORDER BY pfp.quantity DESC";
3606
                $sql .= " LIMIT 1";
3607
3608
                dol_syslog(get_only_class($this) . "::get_buyprice second search from qty/ref/product_id", LOG_DEBUG);
3609
                $resql = $this->db->query($sql);
3610
                if ($resql) {
3611
                    $obj = $this->db->fetch_object($resql);
3612
                    if ($obj && $obj->quantity > 0) {        // If found
3613
                        if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
3614
                            $prod_supplier = new ProductFournisseur($this->db);
3615
                            $prod_supplier->product_fourn_price_id = $obj->rowid;
3616
                            $prod_supplier->id = $obj->fk_product;
3617
                            $prod_supplier->fourn_qty = $obj->quantity;
3618
                            $prod_supplier->fourn_tva_tx = $obj->tva_tx;
3619
                            $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
3620
3621
                            include_once DOL_DOCUMENT_ROOT . '/product/dynamic_price/class/price_parser.class.php';
3622
                            $priceparser = new PriceParser($this->db);
3623
                            $price_result = $priceparser->parseProductSupplier($prod_supplier);
3624
                            if ($result >= 0) {
3625
                                $obj->price = $price_result;
3626
                            }
3627
                        }
3628
                        $this->product_fourn_price_id = $obj->rowid;
3629
                        $this->buyprice = $obj->price; // deprecated
3630
                        $this->fourn_qty = $obj->quantity; // min quantity for price for a virtual supplier
3631
                        $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product for a virtual supplier
3632
                        $this->fourn_price_base_type = 'HT'; // Price base type for a virtual supplier
3633
                        $this->fourn_socid = $obj->fk_soc; // Company that offer this price
3634
                        $this->ref_fourn = $obj->ref_supplier; // deprecated
3635
                        $this->ref_supplier = $obj->ref_supplier; // Ref supplier
3636
                        $this->desc_supplier = $obj->desc_supplier; // desc supplier
3637
                        $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
3638
                        $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
3639
                        $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
3640
                        $this->fourn_multicurrency_price = $obj->multicurrency_price;
3641
                        $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
3642
                        $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
3643
                        $this->fourn_multicurrency_id = $obj->fk_multicurrency;
3644
                        $this->fourn_multicurrency_code = $obj->multicurrency_code;
3645
                        if (getDolGlobalString('PRODUCT_USE_SUPPLIER_PACKAGING')) {
3646
                            $this->packaging = $obj->packaging;
3647
                        }
3648
                        $result = $obj->fk_product;
3649
                        return $result;
3650
                    } else {
3651
                        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é.
3652
                    }
3653
                } else {
3654
                    $this->error = $this->db->lasterror();
3655
                    return -3;
3656
                }
3657
            }
3658
        } else {
3659
            $this->error = $this->db->lasterror();
3660
            return -2;
3661
        }
3662
    }
3663
3664
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3665
3666
    /**
3667
     * Modify customer price of a product/Service for a given level
3668
     *
3669
     * @param double $newprice New price
3670
     * @param string $newpricebase HT or TTC
3671
     * @param User $user Object user that make change
3672
     * @param  ?double $newvat New VAT Rate (For example 8.5. Should not be a string)
3673
     * @param double $newminprice New price min
3674
     * @param int $level 0=standard, >0 = level if multilevel prices
3675
     * @param int $newnpr 0=Standard vat rate, 1=Special vat rate for French NPR VAT
3676
     * @param int $newpbq 1 if it has price by quantity
3677
     * @param int $ignore_autogen Used to avoid infinite loops
3678
     * @param array $localtaxes_array Array with localtaxes info array('0'=>type1,'1'=>rate1,'2'=>type2,'3'=>rate2) (loaded by getLocalTaxesFromRate(vatrate, 0, ...) function).
3679
     * @param string $newdefaultvatcode Default vat code
3680
     * @param string $price_label Price Label
3681
     * @param int $notrigger Disable triggers
3682
     * @return int                            Return integer <0 if KO, >0 if OK
3683
     */
3684
    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)
3685
    {
3686
        global $conf, $langs;
3687
3688
        $lastPriceData = $this->getArrayForPriceCompare($level); // temporary store current price before update
3689
3690
        $id = $this->id;
3691
3692
        dol_syslog(get_only_class($this) . "::update_price id=" . $id . " newprice=" . $newprice . " newpricebase=" . $newpricebase . " newminprice=" . $newminprice . " level=" . $level . " npr=" . $newnpr . " newdefaultvatcode=" . $newdefaultvatcode);
3693
3694
        // Clean parameters
3695
        if (empty($this->tva_tx)) {
3696
            $this->tva_tx = 0;
3697
        }
3698
        if (empty($newnpr)) {
3699
            $newnpr = 0;
3700
        }
3701
        if (empty($newminprice)) {
3702
            $newminprice = 0;
3703
        }
3704
3705
        // Check parameters
3706
        if ($newvat === null || $newvat == '') {  // Maintain '' for backwards compatibility
3707
            $newvat = $this->tva_tx;
3708
        }
3709
3710
        // If multiprices are enabled, then we check if the current product is subject to price autogeneration
3711
        // Price will be modified ONLY when the first one is the one that is being modified
3712
        if ((getDolGlobalString('PRODUIT_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES')) && !$ignore_autogen && $this->price_autogen && ($level == 1)) {
3713
            return $this->generateMultiprices($user, $newprice, $newpricebase, $newvat, $newnpr, $newpbq);
3714
        }
3715
3716
        if (!empty($newminprice) && ($newminprice > $newprice)) {
3717
            $this->error = 'ErrorPriceCantBeLowerThanMinPrice';
3718
            return -1;
3719
        }
3720
3721
        if ($newprice !== '' || $newprice === 0) {
3722
            if ($newpricebase == 'TTC') {
3723
                $price_ttc = price2num($newprice, 'MU');
3724
                $price = (float)price2num($newprice) / (1 + ((float)$newvat / 100));
3725
                $price = price2num($price, 'MU');
3726
3727
                if ($newminprice != '' || $newminprice == 0) {
3728
                    $price_min_ttc = price2num($newminprice, 'MU');
3729
                    $price_min = (float)price2num($newminprice) / (1 + ($newvat / 100));
3730
                    $price_min = price2num($price_min, 'MU');
3731
                } else {
3732
                    $price_min = 0;
3733
                    $price_min_ttc = 0;
3734
                }
3735
            } else {
3736
                $price = (float)price2num($newprice, 'MU');
3737
                $price_ttc = ($newnpr != 1) ? price2num($newprice) * (1 + ($newvat / 100)) : $price;
3738
                $price_ttc = (float)price2num($price_ttc, 'MU');
3739
3740
                if ($newminprice !== '' || $newminprice === 0) {
3741
                    $price_min = price2num($newminprice, 'MU');
3742
                    $price_min_ttc = (float)price2num($newminprice) * (1 + ($newvat / 100));
3743
                    $price_min_ttc = price2num($price_min_ttc, 'MU');
3744
                    //print 'X'.$newminprice.'-'.$price_min;
3745
                } else {
3746
                    $price_min = 0;
3747
                    $price_min_ttc = 0;
3748
                }
3749
            }
3750
            //print 'x'.$id.'-'.$newprice.'-'.$newpricebase.'-'.$price.'-'.$price_ttc.'-'.$price_min.'-'.$price_min_ttc;
3751
3752
            if (count($localtaxes_array) > 0) {
3753
                $localtaxtype1 = $localtaxes_array['0'];
3754
                $localtax1 = $localtaxes_array['1'];
3755
                $localtaxtype2 = $localtaxes_array['2'];
3756
                $localtax2 = $localtaxes_array['3'];
3757
            } else {
3758
                // if array empty, we try to use the vat code
3759
                if (!empty($newdefaultvatcode)) {
3760
                    global $mysoc;
3761
                    // Get record from code
3762
                    $sql = "SELECT t.rowid, t.code, t.recuperableonly as tva_npr, t.localtax1, t.localtax2, t.localtax1_type, t.localtax2_type";
3763
                    $sql .= " FROM " . MAIN_DB_PREFIX . "c_tva as t, " . MAIN_DB_PREFIX . "c_country as c";
3764
                    $sql .= " WHERE t.fk_pays = c.rowid AND c.code = '" . $this->db->escape($mysoc->country_code) . "'";
3765
                    $sql .= " AND t.taux = " . ((float)$newdefaultvatcode) . " AND t.active = 1";
3766
                    $sql .= " AND t.code = '" . $this->db->escape($newdefaultvatcode) . "'";
3767
                    $resql = $this->db->query($sql);
3768
                    if ($resql) {
3769
                        $obj = $this->db->fetch_object($resql);
3770
                        if ($obj) {
3771
                            $npr = $obj->tva_npr;
3772
                            $localtax1 = $obj->localtax1;
3773
                            $localtax2 = $obj->localtax2;
3774
                            $localtaxtype1 = $obj->localtax1_type;
3775
                            $localtaxtype2 = $obj->localtax2_type;
3776
                        }
3777
                    }
3778
                } else {
3779
                    // old method. deprecated because we can't retrieve type
3780
                    $localtaxtype1 = '0';
3781
                    $localtax1 = get_localtax($newvat, 1);
3782
                    $localtaxtype2 = '0';
3783
                    $localtax2 = get_localtax($newvat, 2);
3784
                }
3785
            }
3786
            if (empty($localtax1)) {
3787
                $localtax1 = 0; // If = '' then = 0
3788
            }
3789
            if (empty($localtax2)) {
3790
                $localtax2 = 0; // If = '' then = 0
3791
            }
3792
3793
            $this->db->begin();
3794
3795
            // Ne pas mettre de quote sur les numeriques decimaux.
3796
            // Ceci provoque des stockages avec arrondis en base au lieu des valeurs exactes.
3797
            $sql = "UPDATE " . $this->db->prefix() . "product SET";
3798
            $sql .= " price_base_type = '" . $this->db->escape($newpricebase) . "',";
3799
            $sql .= " price = " . (float)$price . ",";
3800
            $sql .= " price_ttc = " . (float)$price_ttc . ",";
3801
            $sql .= " price_min = " . (float)$price_min . ",";
3802
            $sql .= " price_min_ttc = " . (float)$price_min_ttc . ",";
3803
            $sql .= " localtax1_tx = " . ($localtax1 >= 0 ? (float)$localtax1 : 'NULL') . ",";
3804
            $sql .= " localtax2_tx = " . ($localtax2 >= 0 ? (float)$localtax2 : 'NULL') . ",";
3805
            $sql .= " localtax1_type = " . ($localtaxtype1 != '' ? "'" . $this->db->escape($localtaxtype1) . "'" : "'0'") . ",";
3806
            $sql .= " localtax2_type = " . ($localtaxtype2 != '' ? "'" . $this->db->escape($localtaxtype2) . "'" : "'0'") . ",";
3807
            $sql .= " default_vat_code = " . ($newdefaultvatcode ? "'" . $this->db->escape($newdefaultvatcode) . "'" : "null") . ",";
3808
            $sql .= " price_label = " . (!empty($price_label) ? "'" . $this->db->escape($price_label) . "'" : "null") . ",";
3809
            $sql .= " tva_tx = " . (float)price2num($newvat) . ",";
3810
            $sql .= " recuperableonly = '" . $this->db->escape($newnpr) . "'";
3811
            $sql .= " WHERE rowid = " . ((int)$id);
3812
3813
            dol_syslog(get_only_class($this) . "::update_price", LOG_DEBUG);
3814
            $resql = $this->db->query($sql);
3815
            if ($resql) {
3816
                $this->multiprices[$level] = $price;
3817
                $this->multiprices_ttc[$level] = $price_ttc;
3818
                $this->multiprices_min[$level] = $price_min;
3819
                $this->multiprices_min_ttc[$level] = $price_min_ttc;
3820
                $this->multiprices_base_type[$level] = $newpricebase;
3821
                $this->multiprices_default_vat_code[$level] = $newdefaultvatcode;
3822
                $this->multiprices_tva_tx[$level] = $newvat;
3823
                $this->multiprices_recuperableonly[$level] = $newnpr;
3824
3825
                $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...
3826
                $this->price_label = $price_label;
3827
                $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...
3828
                $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...
3829
                $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...
3830
                $this->price_base_type = $newpricebase;
3831
                $this->default_vat_code = $newdefaultvatcode;
3832
                $this->tva_tx = $newvat;
3833
                $this->tva_npr = $newnpr;
3834
3835
                //Local taxes
3836
                $this->localtax1_tx = $localtax1;
3837
                $this->localtax2_tx = $localtax2;
3838
                $this->localtax1_type = $localtaxtype1;
3839
                $this->localtax2_type = $localtaxtype2;
3840
3841
                // Price by quantity
3842
                $this->price_by_qty = $newpbq;
3843
3844
                // check if price have really change before log
3845
                $newPriceData = $this->getArrayForPriceCompare($level);
3846
                if (!empty(array_diff_assoc($newPriceData, $lastPriceData)) || !getDolGlobalString('PRODUIT_MULTIPRICES')) {
3847
                    $this->_log_price($user, $level); // Save price for level into table product_price
3848
                }
3849
3850
                $this->level = $level; // Store level of price edited for trigger
3851
3852
                // Call trigger
3853
                if (!$notrigger) {
3854
                    $result = $this->call_trigger('PRODUCT_PRICE_MODIFY', $user);
3855
                    if ($result < 0) {
3856
                        $this->db->rollback();
3857
                        return -1;
3858
                    }
3859
                }
3860
                // End call triggers
3861
3862
                $this->db->commit();
3863
            } else {
3864
                $this->db->rollback();
3865
                $this->error = $this->db->lasterror();
3866
                return -1;
3867
            }
3868
        }
3869
3870
        return 1;
3871
    }
3872
3873
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3874
3875
    /**
3876
     *  used to check if price have really change to avoid log pollution
3877
     *
3878
     * @param int $level price level to change
3879
     * @return array
3880
     */
3881
    private function getArrayForPriceCompare($level = 0)
3882
    {
3883
        $testExit = array('multiprices', 'multiprices_ttc', 'multiprices_base_type', 'multiprices_min', 'multiprices_min_ttc', 'multiprices_tva_tx', 'multiprices_recuperableonly');
3884
3885
        foreach ($testExit as $field) {
3886
            if (!isset($this->$field)) {
3887
                return array();
3888
            }
3889
            $tmparray = $this->$field;
3890
            if (!isset($tmparray[$level])) {
3891
                return array();
3892
            }
3893
        }
3894
3895
        $lastPrice = array(
3896
            'level' => $level ? $level : 1,
3897
            'multiprices' => (float)$this->multiprices[$level],
3898
            'multiprices_ttc' => (float)$this->multiprices_ttc[$level],
3899
            'multiprices_base_type' => $this->multiprices_base_type[$level],
3900
            'multiprices_min' => (float)$this->multiprices_min[$level],
3901
            'multiprices_min_ttc' => (float)$this->multiprices_min_ttc[$level],
3902
            'multiprices_tva_tx' => (float)$this->multiprices_tva_tx[$level],
3903
            'multiprices_recuperableonly' => (float)$this->multiprices_recuperableonly[$level],
3904
        );
3905
3906
        return $lastPrice;
3907
    }
3908
3909
3910
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3911
3912
    /**
3913
     * Generates prices for a product based on product multiprice generation rules
3914
     *
3915
     * @param User $user User that updates the prices
3916
     * @param float $baseprice Base price
3917
     * @param string $price_type Base price type
3918
     * @param float $price_vat VAT % tax
3919
     * @param int $npr NPR
3920
     * @param string $psq ¿?
3921
     * @return int -1 KO, 1 OK
3922
     */
3923
    public function generateMultiprices(User $user, $baseprice, $price_type, $price_vat, $npr, $psq)
3924
    {
3925
        global $conf;
3926
3927
        $sql = "SELECT rowid, level, fk_level, var_percent, var_min_percent FROM " . $this->db->prefix() . "product_pricerules";
3928
        $query = $this->db->query($sql);
3929
3930
        $rules = array();
3931
3932
        while ($result = $this->db->fetch_object($query)) {
3933
            $rules[$result->level] = $result;
3934
        }
3935
3936
        //Because prices can be based on other level's prices, we temporarily store them
3937
        $prices = array(
3938
            1 => $baseprice
3939
        );
3940
3941
        $nbofproducts = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
3942
        for ($i = 1; $i <= $nbofproducts; $i++) {
3943
            $price = $baseprice;
3944
            $price_min = $baseprice;
3945
3946
            //We have to make sure it does exist and it is > 0
3947
            //First price level only allows changing min_price
3948
            if ($i > 1 && isset($rules[$i]->var_percent) && $rules[$i]->var_percent) {
3949
                $price = $prices[$rules[$i]->fk_level] * (1 + ($rules[$i]->var_percent / 100));
3950
            }
3951
3952
            $prices[$i] = $price;
3953
3954
            //We have to make sure it does exist and it is > 0
3955
            if (isset($rules[$i]->var_min_percent) && $rules[$i]->var_min_percent) {
3956
                $price_min = $price * (1 - ($rules[$i]->var_min_percent / 100));
3957
            }
3958
3959
            //Little check to make sure the price is modified before triggering generation
3960
            $check_amount = (($price == $this->multiprices[$i]) && ($price_min == $this->multiprices_min[$i]));
3961
            $check_type = ($baseprice == $this->multiprices_base_type[$i]);
3962
3963
            if ($check_amount && $check_type) {
3964
                continue;
3965
            }
3966
3967
            if ($this->updatePrice($price, $price_type, $user, $price_vat, $price_min, $i, $npr, $psq, true) < 0) {
3968
                return -1;
3969
            }
3970
        }
3971
3972
        return 1;
3973
    }
3974
3975
3976
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3977
3978
    /**
3979
     *  Sets the supplier price expression
3980
     *
3981
     * @param int $expression_id Expression
3982
     * @return     int                     Return integer <0 if KO, >0 if OK
3983
     * @deprecated Use Product::update instead
3984
     */
3985
    public function setPriceExpression($expression_id)
3986
    {
3987
        global $user;
3988
3989
        $this->fk_price_expression = $expression_id;
3990
3991
        return $this->update($this->id, $user);
3992
    }
3993
3994
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3995
3996
    /**
3997
     *  Charge tableau des stats OF pour le produit/service
3998
     *
3999
     * @param int $socid Id societe
4000
     * @return int                     Array of stats in $this->stats_mo, <0 if ko or >0 if ok
4001
     */
4002
    public function load_stats_mo($socid = 0)
4003
    {
4004
        // phpcs:enable
4005
        global $user, $hookmanager, $action;
4006
4007
        $error = 0;
4008
4009
        foreach (array('toconsume', 'consumed', 'toproduce', 'produced') as $role) {
4010
            $this->stats_mo['customers_' . $role] = 0;
4011
            $this->stats_mo['nb_' . $role] = 0;
4012
            $this->stats_mo['qty_' . $role] = 0;
4013
4014
            $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
4015
            $sql .= " SUM(mp.qty) as qty";
4016
            $sql .= " FROM " . $this->db->prefix() . "mrp_mo as c";
4017
            $sql .= " INNER JOIN " . $this->db->prefix() . "mrp_production as mp ON mp.fk_mo=c.rowid";
4018
            if (!$user->hasRight('societe', 'client', 'voir')) {
4019
                $sql .= " INNER JOIN " . $this->db->prefix() . "societe_commerciaux as sc ON sc.fk_soc=c.fk_soc AND sc.fk_user = " . ((int)$user->id);
4020
            }
4021
            $sql .= " WHERE ";
4022
            $sql .= " c.entity IN (" . getEntity('mo') . ")";
4023
4024
            $sql .= " AND mp.fk_product = " . ((int)$this->id);
4025
            $sql .= " AND mp.role ='" . $this->db->escape($role) . "'";
4026
            if ($socid > 0) {
4027
                $sql .= " AND c.fk_soc = " . ((int)$socid);
4028
            }
4029
4030
            $result = $this->db->query($sql);
4031
            if ($result) {
4032
                $obj = $this->db->fetch_object($result);
4033
                $this->stats_mo['customers_' . $role] = $obj->nb_customers ? $obj->nb_customers : 0;
4034
                $this->stats_mo['nb_' . $role] = $obj->nb ? $obj->nb : 0;
4035
                $this->stats_mo['qty_' . $role] = $obj->qty ? price2num($obj->qty, 'MS') : 0;     // qty may be a float due to the SUM()
4036
            } else {
4037
                $this->error = $this->db->error();
4038
                $error++;
4039
            }
4040
        }
4041
4042
        if (!empty($error)) {
4043
            return -1;
4044
        }
4045
4046
        $parameters = array('socid' => $socid);
4047
        $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
4048
        if ($reshook > 0) {
4049
            $this->stats_mo = $hookmanager->resArray['stats_mo'];
4050
        }
4051
4052
        return 1;
4053
    }
4054
4055
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4056
4057
    /**
4058
     *  Charge tableau des stats OF pour le produit/service
4059
     *
4060
     * @param int $socid Id societe
4061
     * @return int        Array of stats in $this->stats_bom, <0 if ko or >0 if ok
4062
     */
4063
    public function load_stats_bom($socid = 0)
4064
    {
4065
        // phpcs:enable
4066
        global $user, $hookmanager, $action;
4067
4068
        $error = 0;
4069
4070
        $this->stats_bom['nb_toproduce'] = 0;
4071
        $this->stats_bom['nb_toconsume'] = 0;
4072
        $this->stats_bom['qty_toproduce'] = 0;
4073
        $this->stats_bom['qty_toconsume'] = 0;
4074
4075
        $sql = "SELECT COUNT(DISTINCT b.rowid) as nb_toproduce,";
4076
        $sql .= " SUM(b.qty) as qty_toproduce";
4077
        $sql .= " FROM " . $this->db->prefix() . "bom_bom as b";
4078
        $sql .= " INNER JOIN " . $this->db->prefix() . "bom_bomline as bl ON bl.fk_bom=b.rowid";
4079
        $sql .= " WHERE ";
4080
        $sql .= " b.entity IN (" . getEntity('bom') . ")";
4081
        $sql .= " AND b.fk_product =" . ((int)$this->id);
4082
        $sql .= " GROUP BY b.rowid";
4083
4084
        $result = $this->db->query($sql);
4085
        if ($result) {
4086
            $obj = $this->db->fetch_object($result);
4087
            $this->stats_bom['nb_toproduce'] = !empty($obj->nb_toproduce) ? $obj->nb_toproduce : 0;
4088
            $this->stats_bom['qty_toproduce'] = !empty($obj->qty_toproduce) ? price2num($obj->qty_toproduce) : 0;
4089
        } else {
4090
            $this->error = $this->db->error();
4091
            $error++;
4092
        }
4093
4094
        $sql = "SELECT COUNT(DISTINCT bl.rowid) as nb_toconsume,";
4095
        $sql .= " SUM(bl.qty) as qty_toconsume";
4096
        $sql .= " FROM " . $this->db->prefix() . "bom_bom as b";
4097
        $sql .= " INNER JOIN " . $this->db->prefix() . "bom_bomline as bl ON bl.fk_bom=b.rowid";
4098
        $sql .= " WHERE ";
4099
        $sql .= " b.entity IN (" . getEntity('bom') . ")";
4100
        $sql .= " AND bl.fk_product =" . ((int)$this->id);
4101
4102
        $result = $this->db->query($sql);
4103
        if ($result) {
4104
            $obj = $this->db->fetch_object($result);
4105
            $this->stats_bom['nb_toconsume'] = !empty($obj->nb_toconsume) ? $obj->nb_toconsume : 0;
4106
            $this->stats_bom['qty_toconsume'] = !empty($obj->qty_toconsume) ? price2num($obj->qty_toconsume) : 0;
4107
        } else {
4108
            $this->error = $this->db->error();
4109
            $error++;
4110
        }
4111
4112
        if (!empty($error)) {
4113
            return -1;
4114
        }
4115
4116
        $parameters = array('socid' => $socid);
4117
        $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
4118
        if ($reshook > 0) {
4119
            $this->stats_bom = $hookmanager->resArray['stats_bom'];
4120
        }
4121
4122
        return 1;
4123
    }
4124
4125
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4126
4127
    /**
4128
     *  Charge tableau des stats propale pour le produit/service
4129
     *
4130
     * @param int $socid Id societe
4131
     * @return int        Array of stats in $this->stats_propale, <0 if ko or >0 if ok
4132
     */
4133
    public function load_stats_propale($socid = 0)
4134
    {
4135
        // phpcs:enable
4136
        global $conf, $user, $hookmanager, $action;
4137
4138
        $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_customers, COUNT(DISTINCT p.rowid) as nb,";
4139
        $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
4140
        $sql .= " FROM " . $this->db->prefix() . "propaldet as pd";
4141
        $sql .= ", " . $this->db->prefix() . "propal as p";
4142
        $sql .= ", " . $this->db->prefix() . "societe as s";
4143
        if (!$user->hasRight('societe', 'client', 'voir')) {
4144
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
4145
        }
4146
        $sql .= " WHERE p.rowid = pd.fk_propal";
4147
        $sql .= " AND p.fk_soc = s.rowid";
4148
        $sql .= " AND p.entity IN (" . getEntity('propal') . ")";
4149
        $sql .= " AND pd.fk_product = " . ((int)$this->id);
4150
        if (!$user->hasRight('societe', 'client', 'voir')) {
4151
            $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int)$user->id);
4152
        }
4153
        //$sql.= " AND pr.fk_statut != 0";
4154
        if ($socid > 0) {
4155
            $sql .= " AND p.fk_soc = " . ((int)$socid);
4156
        }
4157
4158
        $result = $this->db->query($sql);
4159
        if ($result) {
4160
            $obj = $this->db->fetch_object($result);
4161
            $this->stats_propale['customers'] = $obj->nb_customers;
4162
            $this->stats_propale['nb'] = $obj->nb;
4163
            $this->stats_propale['rows'] = $obj->nb_rows;
4164
            $this->stats_propale['qty'] = $obj->qty ? $obj->qty : 0;
4165
4166
            // if it's a virtual product, maybe it is in proposal by extension
4167
            if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4168
                $TFather = $this->getFather();
4169
                if (is_array($TFather) && !empty($TFather)) {
4170
                    foreach ($TFather as &$fatherData) {
4171
                        $pFather = new Product($this->db);
4172
                        $pFather->id = $fatherData['id'];
4173
                        $qtyCoef = $fatherData['qty'];
4174
4175
                        if ($fatherData['incdec']) {
4176
                            $pFather->load_stats_propale($socid);
4177
4178
                            $this->stats_propale['customers'] += $pFather->stats_propale['customers'];
4179
                            $this->stats_propale['nb'] += $pFather->stats_propale['nb'];
4180
                            $this->stats_propale['rows'] += $pFather->stats_propale['rows'];
4181
                            $this->stats_propale['qty'] += $pFather->stats_propale['qty'] * $qtyCoef;
4182
                        }
4183
                    }
4184
                }
4185
            }
4186
4187
            $parameters = array('socid' => $socid);
4188
            $reshook = $hookmanager->executeHooks('loadStatsCustomerProposal', $parameters, $this, $action);
4189
            if ($reshook > 0) {
4190
                $this->stats_propale = $hookmanager->resArray['stats_propale'];
4191
            }
4192
4193
            return 1;
4194
        } else {
4195
            $this->error = $this->db->error();
4196
            return -1;
4197
        }
4198
    }
4199
4200
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4201
4202
    /**
4203
     *  Charge tableau des stats propale pour le produit/service
4204
     *
4205
     * @param int $socid Id thirdparty
4206
     * @return int        Array of stats in $this->stats_proposal_supplier, <0 if ko or >0 if ok
4207
     */
4208
    public function load_stats_proposal_supplier($socid = 0)
4209
    {
4210
        // phpcs:enable
4211
        global $conf, $user, $hookmanager, $action;
4212
4213
        $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_suppliers, COUNT(DISTINCT p.rowid) as nb,";
4214
        $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
4215
        $sql .= " FROM " . $this->db->prefix() . "supplier_proposaldet as pd";
4216
        $sql .= ", " . $this->db->prefix() . "supplier_proposal as p";
4217
        $sql .= ", " . $this->db->prefix() . "societe as s";
4218
        if (!$user->hasRight('societe', 'client', 'voir')) {
4219
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
4220
        }
4221
        $sql .= " WHERE p.rowid = pd.fk_supplier_proposal";
4222
        $sql .= " AND p.fk_soc = s.rowid";
4223
        $sql .= " AND p.entity IN (" . getEntity('supplier_proposal') . ")";
4224
        $sql .= " AND pd.fk_product = " . ((int)$this->id);
4225
        if (!$user->hasRight('societe', 'client', 'voir')) {
4226
            $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int)$user->id);
4227
        }
4228
        //$sql.= " AND pr.fk_statut != 0";
4229
        if ($socid > 0) {
4230
            $sql .= " AND p.fk_soc = " . ((int)$socid);
4231
        }
4232
4233
        $result = $this->db->query($sql);
4234
        if ($result) {
4235
            $obj = $this->db->fetch_object($result);
4236
            $this->stats_proposal_supplier['suppliers'] = $obj->nb_suppliers;
4237
            $this->stats_proposal_supplier['nb'] = $obj->nb;
4238
            $this->stats_proposal_supplier['rows'] = $obj->nb_rows;
4239
            $this->stats_proposal_supplier['qty'] = $obj->qty ? $obj->qty : 0;
4240
4241
            $parameters = array('socid' => $socid);
4242
            $reshook = $hookmanager->executeHooks('loadStatsSupplierProposal', $parameters, $this, $action);
4243
            if ($reshook > 0) {
4244
                $this->stats_proposal_supplier = $hookmanager->resArray['stats_proposal_supplier'];
4245
            }
4246
4247
            return 1;
4248
        } else {
4249
            $this->error = $this->db->error();
4250
            return -1;
4251
        }
4252
    }
4253
4254
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4255
4256
    /**
4257
     *  Charge tableau des stats contrat pour le produit/service
4258
     *
4259
     * @param int $socid Id societe
4260
     * @return int                     Array of stats in $this->stats_contrat, <0 if ko or >0 if ok
4261
     */
4262
    public function load_stats_contrat($socid = 0)
4263
    {
4264
        // phpcs:enable
4265
        global $conf, $user, $hookmanager, $action;
4266
4267
        $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
4268
        $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
4269
        $sql .= " FROM " . $this->db->prefix() . "contratdet as cd";
4270
        $sql .= ", " . $this->db->prefix() . "contrat as c";
4271
        $sql .= ", " . $this->db->prefix() . "societe as s";
4272
        if (!$user->hasRight('societe', 'client', 'voir')) {
4273
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
4274
        }
4275
        $sql .= " WHERE c.rowid = cd.fk_contrat";
4276
        $sql .= " AND c.fk_soc = s.rowid";
4277
        $sql .= " AND c.entity IN (" . getEntity('contract') . ")";
4278
        $sql .= " AND cd.fk_product = " . ((int)$this->id);
4279
        if (!$user->hasRight('societe', 'client', 'voir')) {
4280
            $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int)$user->id);
4281
        }
4282
        //$sql.= " AND c.statut != 0";
4283
        if ($socid > 0) {
4284
            $sql .= " AND c.fk_soc = " . ((int)$socid);
4285
        }
4286
4287
        $result = $this->db->query($sql);
4288
        if ($result) {
4289
            $obj = $this->db->fetch_object($result);
4290
            $this->stats_contrat['customers'] = $obj->nb_customers;
4291
            $this->stats_contrat['nb'] = $obj->nb;
4292
            $this->stats_contrat['rows'] = $obj->nb_rows;
4293
            $this->stats_contrat['qty'] = $obj->qty ? $obj->qty : 0;
4294
4295
            // if it's a virtual product, maybe it is in contract by extension
4296
            if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4297
                $TFather = $this->getFather();
4298
                if (is_array($TFather) && !empty($TFather)) {
4299
                    foreach ($TFather as &$fatherData) {
4300
                        $pFather = new Product($this->db);
4301
                        $pFather->id = $fatherData['id'];
4302
                        $qtyCoef = $fatherData['qty'];
4303
4304
                        if ($fatherData['incdec']) {
4305
                            $pFather->load_stats_contrat($socid);
4306
4307
                            $this->stats_contrat['customers'] += $pFather->stats_contrat['customers'];
4308
                            $this->stats_contrat['nb'] += $pFather->stats_contrat['nb'];
4309
                            $this->stats_contrat['rows'] += $pFather->stats_contrat['rows'];
4310
                            $this->stats_contrat['qty'] += $pFather->stats_contrat['qty'] * $qtyCoef;
4311
                        }
4312
                    }
4313
                }
4314
            }
4315
4316
            $parameters = array('socid' => $socid);
4317
            $reshook = $hookmanager->executeHooks('loadStatsContract', $parameters, $this, $action);
4318
            if ($reshook > 0) {
4319
                $this->stats_contrat = $hookmanager->resArray['stats_contrat'];
4320
            }
4321
4322
            return 1;
4323
        } else {
4324
            $this->error = $this->db->error() . ' sql=' . $sql;
4325
            return -1;
4326
        }
4327
    }
4328
4329
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4330
4331
    /**
4332
     *  Charge tableau des stats facture recurrentes pour le produit/service
4333
     *
4334
     * @param int $socid Id societe
4335
     * @return int                     Array of stats in $this->stats_facture, <0 if ko or >0 if ok
4336
     */
4337
    public function load_stats_facturerec($socid = 0)
4338
    {
4339
        // phpcs:enable
4340
        global $conf, $user, $hookmanager, $action;
4341
4342
        $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
4343
        $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4344
        $sql .= " FROM " . MAIN_DB_PREFIX . "facturedet_rec as fd";
4345
        $sql .= ", " . MAIN_DB_PREFIX . "facture_rec as f";
4346
        $sql .= ", " . MAIN_DB_PREFIX . "societe as s";
4347
        if (!$user->hasRight('societe', 'client', 'voir')) {
4348
            $sql .= ", " . MAIN_DB_PREFIX . "societe_commerciaux as sc";
4349
        }
4350
        $sql .= " WHERE f.rowid = fd.fk_facture";
4351
        $sql .= " AND f.fk_soc = s.rowid";
4352
        $sql .= " AND f.entity IN (" . getEntity('invoice') . ")";
4353
        $sql .= " AND fd.fk_product = " . ((int)$this->id);
4354
        if (!$user->hasRight('societe', 'client', 'voir')) {
4355
            $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int)$user->id);
4356
        }
4357
        //$sql.= " AND f.fk_statut != 0";
4358
        if ($socid > 0) {
4359
            $sql .= " AND f.fk_soc = " . ((int)$socid);
4360
        }
4361
4362
        $result = $this->db->query($sql);
4363
        if ($result) {
4364
            $obj = $this->db->fetch_object($result);
4365
            $this->stats_facturerec['customers'] = $obj->nb_customers;
4366
            $this->stats_facturerec['nb'] = $obj->nb;
4367
            $this->stats_facturerec['rows'] = $obj->nb_rows;
4368
            $this->stats_facturerec['qty'] = $obj->qty ? $obj->qty : 0;
4369
4370
            // if it's a virtual product, maybe it is in invoice by extension
4371
            if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4372
                $TFather = $this->getFather();
4373
                if (is_array($TFather) && !empty($TFather)) {
4374
                    foreach ($TFather as &$fatherData) {
4375
                        $pFather = new Product($this->db);
4376
                        $pFather->id = $fatherData['id'];
4377
                        $qtyCoef = $fatherData['qty'];
4378
4379
                        if ($fatherData['incdec']) {
4380
                            $pFather->load_stats_facture($socid);
4381
4382
                            $this->stats_facturerec['customers'] += $pFather->stats_facturerec['customers'];
4383
                            $this->stats_facturerec['nb'] += $pFather->stats_facturerec['nb'];
4384
                            $this->stats_facturerec['rows'] += $pFather->stats_facturerec['rows'];
4385
                            $this->stats_facturerec['qty'] += $pFather->stats_facturerec['qty'] * $qtyCoef;
4386
                        }
4387
                    }
4388
                }
4389
            }
4390
4391
            $parameters = array('socid' => $socid);
4392
            $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoiceRec', $parameters, $this, $action);
4393
            if ($reshook > 0) {
4394
                $this->stats_facturerec = $hookmanager->resArray['stats_facturerec'];
4395
            }
4396
4397
            return 1;
4398
        } else {
4399
            $this->error = $this->db->error();
4400
            return -1;
4401
        }
4402
    }
4403
4404
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4405
4406
    /**
4407
     *  Charge tableau des stats facture pour le produit/service
4408
     *
4409
     * @param int $socid Id societe
4410
     * @return int                     Array of stats in $this->stats_facture, <0 if ko or >0 if ok
4411
     */
4412
    public function load_stats_facture($socid = 0)
4413
    {
4414
        // phpcs:enable
4415
        global $conf, $user, $hookmanager, $action;
4416
4417
        $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
4418
        $sql .= " COUNT(fd.rowid) as nb_rows, SUM(" . $this->db->ifsql('f.type != 2', 'fd.qty', 'fd.qty * -1') . ") as qty";
4419
        $sql .= " FROM " . $this->db->prefix() . "facturedet as fd";
4420
        $sql .= ", " . $this->db->prefix() . "facture as f";
4421
        $sql .= ", " . $this->db->prefix() . "societe as s";
4422
        if (!$user->hasRight('societe', 'client', 'voir')) {
4423
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
4424
        }
4425
        $sql .= " WHERE f.rowid = fd.fk_facture";
4426
        $sql .= " AND f.fk_soc = s.rowid";
4427
        $sql .= " AND f.entity IN (" . getEntity('invoice') . ")";
4428
        $sql .= " AND fd.fk_product = " . ((int)$this->id);
4429
        if (!$user->hasRight('societe', 'client', 'voir')) {
4430
            $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int)$user->id);
4431
        }
4432
        //$sql.= " AND f.fk_statut != 0";
4433
        if ($socid > 0) {
4434
            $sql .= " AND f.fk_soc = " . ((int)$socid);
4435
        }
4436
4437
        $result = $this->db->query($sql);
4438
        if ($result) {
4439
            $obj = $this->db->fetch_object($result);
4440
            $this->stats_facture['customers'] = $obj->nb_customers;
4441
            $this->stats_facture['nb'] = $obj->nb;
4442
            $this->stats_facture['rows'] = $obj->nb_rows;
4443
            $this->stats_facture['qty'] = $obj->qty ? $obj->qty : 0;
4444
4445
            // if it's a virtual product, maybe it is in invoice by extension
4446
            if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4447
                $TFather = $this->getFather();
4448
                if (is_array($TFather) && !empty($TFather)) {
4449
                    foreach ($TFather as &$fatherData) {
4450
                        $pFather = new Product($this->db);
4451
                        $pFather->id = $fatherData['id'];
4452
                        $qtyCoef = $fatherData['qty'];
4453
4454
                        if ($fatherData['incdec']) {
4455
                            $pFather->load_stats_facture($socid);
4456
4457
                            $this->stats_facture['customers'] += $pFather->stats_facture['customers'];
4458
                            $this->stats_facture['nb'] += $pFather->stats_facture['nb'];
4459
                            $this->stats_facture['rows'] += $pFather->stats_facture['rows'];
4460
                            $this->stats_facture['qty'] += $pFather->stats_facture['qty'] * $qtyCoef;
4461
                        }
4462
                    }
4463
                }
4464
            }
4465
4466
            $parameters = array('socid' => $socid);
4467
            $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoice', $parameters, $this, $action);
4468
            if ($reshook > 0) {
4469
                $this->stats_facture = $hookmanager->resArray['stats_facture'];
4470
            }
4471
4472
            return 1;
4473
        } else {
4474
            $this->error = $this->db->error();
4475
            return -1;
4476
        }
4477
    }
4478
4479
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4480
4481
    /**
4482
     *  Charge tableau des stats facture pour le produit/service
4483
     *
4484
     * @param int $socid Id societe
4485
     * @return int                     Array of stats in $this->stats_facture_fournisseur, <0 if ko or >0 if ok
4486
     */
4487
    public function load_stats_facture_fournisseur($socid = 0)
4488
    {
4489
        // phpcs:enable
4490
        global $conf, $user, $hookmanager, $action;
4491
4492
        $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,";
4493
        $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4494
        $sql .= " FROM " . $this->db->prefix() . "facture_fourn_det as fd";
4495
        $sql .= ", " . $this->db->prefix() . "facture_fourn as f";
4496
        $sql .= ", " . $this->db->prefix() . "societe as s";
4497
        if (!$user->hasRight('societe', 'client', 'voir')) {
4498
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
4499
        }
4500
        $sql .= " WHERE f.rowid = fd.fk_facture_fourn";
4501
        $sql .= " AND f.fk_soc = s.rowid";
4502
        $sql .= " AND f.entity IN (" . getEntity('facture_fourn') . ")";
4503
        $sql .= " AND fd.fk_product = " . ((int)$this->id);
4504
        if (!$user->hasRight('societe', 'client', 'voir')) {
4505
            $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int)$user->id);
4506
        }
4507
        //$sql.= " AND f.fk_statut != 0";
4508
        if ($socid > 0) {
4509
            $sql .= " AND f.fk_soc = " . ((int)$socid);
4510
        }
4511
4512
        $result = $this->db->query($sql);
4513
        if ($result) {
4514
            $obj = $this->db->fetch_object($result);
4515
            $this->stats_facture_fournisseur['suppliers'] = $obj->nb_suppliers;
4516
            $this->stats_facture_fournisseur['nb'] = $obj->nb;
4517
            $this->stats_facture_fournisseur['rows'] = $obj->nb_rows;
4518
            $this->stats_facture_fournisseur['qty'] = $obj->qty ? $obj->qty : 0;
4519
4520
            $parameters = array('socid' => $socid);
4521
            $reshook = $hookmanager->executeHooks('loadStatsSupplierInvoice', $parameters, $this, $action);
4522
            if ($reshook > 0) {
4523
                $this->stats_facture_fournisseur = $hookmanager->resArray['stats_facture_fournisseur'];
4524
            }
4525
4526
            return 1;
4527
        } else {
4528
            $this->error = $this->db->error();
4529
            return -1;
4530
        }
4531
    }
4532
4533
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4534
4535
    /**
4536
     *  Return nb of units or customers invoices in which product is included
4537
     *
4538
     * @param int $socid Limit count on a particular third party id
4539
     * @param string $mode 'byunit'=number of unit, 'bynumber'=nb of entities
4540
     * @param int $filteronproducttype 0=To filter on product only, 1=To filter on services only
4541
     * @param int $year Year (0=last 12 month, -1=all years)
4542
     * @param string $morefilter More sql filters
4543
     * @return array                       Return integer <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
4544
     */
4545
    public function get_nb_vente($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4546
    {
4547
        // phpcs:enable
4548
        global $conf;
4549
        global $user;
4550
4551
        $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4552
        if ($mode == 'bynumber') {
4553
            $sql .= ", count(DISTINCT f.rowid)";
4554
        }
4555
        $sql .= ", sum(d.total_ht) as total_ht";
4556
        $sql .= " FROM " . $this->db->prefix() . "facturedet as d, " . $this->db->prefix() . "facture as f, " . $this->db->prefix() . "societe as s";
4557
        if ($filteronproducttype >= 0) {
4558
            $sql .= ", " . $this->db->prefix() . "product as p";
4559
        }
4560
        if (!$user->hasRight('societe', 'client', 'voir')) {
4561
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
4562
        }
4563
        $sql .= " WHERE f.rowid = d.fk_facture";
4564
        if ($this->id > 0) {
4565
            $sql .= " AND d.fk_product = " . ((int)$this->id);
4566
        } else {
4567
            $sql .= " AND d.fk_product > 0";
4568
        }
4569
        if ($filteronproducttype >= 0) {
4570
            $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = " . ((int)$filteronproducttype);
4571
        }
4572
        $sql .= " AND f.fk_soc = s.rowid";
4573
        $sql .= " AND f.entity IN (" . getEntity('invoice') . ")";
4574
        if (!$user->hasRight('societe', 'client', 'voir')) {
4575
            $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int)$user->id);
4576
        }
4577
        if ($socid > 0) {
4578
            $sql .= " AND f.fk_soc = $socid";
4579
        }
4580
        $sql .= $morefilter;
4581
        $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4582
        $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4583
4584
        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...
4585
    }
4586
4587
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4588
4589
    /**
4590
     *  Return an array formatted for showing graphs
4591
     *
4592
     * @param string $sql Request to execute
4593
     * @param string $mode 'byunit'=number of unit, 'bynumber'=nb of entities
4594
     * @param int $year Year (0=current year, -1=all years)
4595
     * @return array|int            Return integer <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
4596
     */
4597
    private function _get_stats($sql, $mode, $year = 0)
4598
    {
4599
        // phpcs:enable
4600
        $tab = array();
4601
4602
        $resql = $this->db->query($sql);
4603
        if ($resql) {
4604
            $num = $this->db->num_rows($resql);
4605
            $i = 0;
4606
            while ($i < $num) {
4607
                $arr = $this->db->fetch_array($resql);
4608
                if (is_array($arr)) {
4609
                    $keyfortab = (string)$arr[1];
4610
                    if ($year == -1) {
4611
                        $keyfortab = substr($keyfortab, -2);
4612
                    }
4613
4614
                    if ($mode == 'byunit') {
4615
                        $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[0]; // 1st field
4616
                    } elseif ($mode == 'bynumber') {
4617
                        $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
4618
                    } elseif ($mode == 'byamount') {
4619
                        $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
4620
                    } else {
4621
                        // Bad value for $mode
4622
                        return -1;
4623
                    }
4624
                }
4625
                $i++;
4626
            }
4627
        } else {
4628
            $this->error = $this->db->error() . ' sql=' . $sql;
4629
            return -1;
4630
        }
4631
4632
        if (empty($year)) {
4633
            $year = dol_print_date(time(), '%Y');
4634
            $month = dol_print_date(time(), '%m');
4635
        } elseif ($year == -1) {
4636
            $year = '';
4637
            $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
4638
        } else {
4639
            $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
4640
        }
4641
4642
        $result = array();
4643
4644
        for ($j = 0; $j < 12; $j++) {
4645
            // $ids is 'D', 'N', 'O', 'S', ... (First letter of month in user language)
4646
            $idx = ucfirst(dol_trunc(dol_print_date(dol_mktime(12, 0, 0, $month, 1, 1970), "%b"), 1, 'right', 'UTF-8', 1));
4647
4648
            //print $idx.'-'.$year.'-'.$month.'<br>';
4649
            $result[$j] = array($idx, isset($tab[$year . $month]) ? $tab[$year . $month] : 0);
4650
            //            $result[$j] = array($monthnum,isset($tab[$year.$month])?$tab[$year.$month]:0);
4651
4652
            $month = "0" . ($month - 1);
4653
            if (dol_strlen($month) == 3) {
4654
                $month = substr($month, 1);
4655
            }
4656
            if ($month == 0) {
4657
                $month = 12;
4658
                $year = $year - 1;
4659
            }
4660
        }
4661
4662
        return array_reverse($result);
4663
    }
4664
4665
4666
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4667
4668
    /**
4669
     *  Return nb of units or supplier invoices in which product is included
4670
     *
4671
     * @param int $socid Limit count on a particular third party id
4672
     * @param string $mode 'byunit'=number of unit, 'bynumber'=nb of entities
4673
     * @param int $filteronproducttype 0=To filter on product only, 1=To filter on services only
4674
     * @param int $year Year (0=last 12 month, -1=all years)
4675
     * @param string $morefilter More sql filters
4676
     * @return array                       Return integer <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
4677
     */
4678
    public function get_nb_achat($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4679
    {
4680
        // phpcs:enable
4681
        global $conf;
4682
        global $user;
4683
4684
        $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4685
        if ($mode == 'bynumber') {
4686
            $sql .= ", count(DISTINCT f.rowid)";
4687
        }
4688
        $sql .= ", sum(d.total_ht) as total_ht";
4689
        $sql .= " FROM " . $this->db->prefix() . "facture_fourn_det as d, " . $this->db->prefix() . "facture_fourn as f, " . $this->db->prefix() . "societe as s";
4690
        if ($filteronproducttype >= 0) {
4691
            $sql .= ", " . $this->db->prefix() . "product as p";
4692
        }
4693
        if (!$user->hasRight('societe', 'client', 'voir')) {
4694
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
4695
        }
4696
        $sql .= " WHERE f.rowid = d.fk_facture_fourn";
4697
        if ($this->id > 0) {
4698
            $sql .= " AND d.fk_product = " . ((int)$this->id);
4699
        } else {
4700
            $sql .= " AND d.fk_product > 0";
4701
        }
4702
        if ($filteronproducttype >= 0) {
4703
            $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = " . ((int)$filteronproducttype);
4704
        }
4705
        $sql .= " AND f.fk_soc = s.rowid";
4706
        $sql .= " AND f.entity IN (" . getEntity('facture_fourn') . ")";
4707
        if (!$user->hasRight('societe', 'client', 'voir')) {
4708
            $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int)$user->id);
4709
        }
4710
        if ($socid > 0) {
4711
            $sql .= " AND f.fk_soc = $socid";
4712
        }
4713
        $sql .= $morefilter;
4714
        $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4715
        $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4716
4717
        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...
4718
    }
4719
4720
4721
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4722
4723
    /**
4724
     * Return nb of units in proposals in which product is included
4725
     *
4726
     * @param int $socid Limit count on a particular third party id
4727
     * @param string $mode 'byunit'=number of unit, 'bynumber'=nb of entities, 'byamount'=amount
4728
     * @param int $filteronproducttype 0=To filter on product only, 1=To filter on services only
4729
     * @param int $year Year (0=last 12 month, -1=all years)
4730
     * @param string $morefilter More sql filters
4731
     * @return array                       Return integer <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
4732
     */
4733
    public function get_nb_propal($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4734
    {
4735
        // phpcs:enable
4736
        global $conf, $user;
4737
4738
        $sql = "SELECT sum(d.qty) as qty, date_format(p.datep, '%Y%m')";
4739
        if ($mode == 'bynumber') {
4740
            $sql .= ", count(DISTINCT p.rowid)";
4741
        }
4742
        $sql .= ", sum(d.total_ht) as total_ht";
4743
        $sql .= " FROM " . $this->db->prefix() . "propaldet as d, " . $this->db->prefix() . "propal as p, " . $this->db->prefix() . "societe as s";
4744
        if ($filteronproducttype >= 0) {
4745
            $sql .= ", " . $this->db->prefix() . "product as prod";
4746
        }
4747
        if (!$user->hasRight('societe', 'client', 'voir')) {
4748
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
4749
        }
4750
        $sql .= " WHERE p.rowid = d.fk_propal";
4751
        if ($this->id > 0) {
4752
            $sql .= " AND d.fk_product = " . ((int)$this->id);
4753
        } else {
4754
            $sql .= " AND d.fk_product > 0";
4755
        }
4756
        if ($filteronproducttype >= 0) {
4757
            $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = " . ((int)$filteronproducttype);
4758
        }
4759
        $sql .= " AND p.fk_soc = s.rowid";
4760
        $sql .= " AND p.entity IN (" . getEntity('propal') . ")";
4761
        if (!$user->hasRight('societe', 'client', 'voir')) {
4762
            $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int)$user->id);
4763
        }
4764
        if ($socid > 0) {
4765
            $sql .= " AND p.fk_soc = " . ((int)$socid);
4766
        }
4767
        $sql .= $morefilter;
4768
        $sql .= " GROUP BY date_format(p.datep,'%Y%m')";
4769
        $sql .= " ORDER BY date_format(p.datep,'%Y%m') DESC";
4770
4771
        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...
4772
    }
4773
4774
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4775
4776
    /**
4777
     *  Return nb of units in proposals in which product is included
4778
     *
4779
     * @param int $socid Limit count on a particular third party id
4780
     * @param string $mode 'byunit'=number of unit, 'bynumber'=nb of entities
4781
     * @param int $filteronproducttype 0=To filter on product only, 1=To filter on services only
4782
     * @param int $year Year (0=last 12 month, -1=all years)
4783
     * @param string $morefilter More sql filters
4784
     * @return array                       Return integer <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
4785
     */
4786
    public function get_nb_propalsupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4787
    {
4788
        // phpcs:enable
4789
        global $conf;
4790
        global $user;
4791
4792
        $sql = "SELECT sum(d.qty) as qty, date_format(p.date_valid, '%Y%m')";
4793
        if ($mode == 'bynumber') {
4794
            $sql .= ", count(DISTINCT p.rowid)";
4795
        }
4796
        $sql .= ", sum(d.total_ht) as total_ht";
4797
        $sql .= " FROM " . $this->db->prefix() . "supplier_proposaldet as d, " . $this->db->prefix() . "supplier_proposal as p, " . $this->db->prefix() . "societe as s";
4798
        if ($filteronproducttype >= 0) {
4799
            $sql .= ", " . $this->db->prefix() . "product as prod";
4800
        }
4801
        if (!$user->hasRight('societe', 'client', 'voir')) {
4802
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
4803
        }
4804
        $sql .= " WHERE p.rowid = d.fk_supplier_proposal";
4805
        if ($this->id > 0) {
4806
            $sql .= " AND d.fk_product = " . ((int)$this->id);
4807
        } else {
4808
            $sql .= " AND d.fk_product > 0";
4809
        }
4810
        if ($filteronproducttype >= 0) {
4811
            $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = " . ((int)$filteronproducttype);
4812
        }
4813
        $sql .= " AND p.fk_soc = s.rowid";
4814
        $sql .= " AND p.entity IN (" . getEntity('supplier_proposal') . ")";
4815
        if (!$user->hasRight('societe', 'client', 'voir')) {
4816
            $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int)$user->id);
4817
        }
4818
        if ($socid > 0) {
4819
            $sql .= " AND p.fk_soc = " . ((int)$socid);
4820
        }
4821
        $sql .= $morefilter;
4822
        $sql .= " GROUP BY date_format(p.date_valid,'%Y%m')";
4823
        $sql .= " ORDER BY date_format(p.date_valid,'%Y%m') DESC";
4824
4825
        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...
4826
    }
4827
4828
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4829
4830
    /**
4831
     *  Return nb of units in orders in which product is included
4832
     *
4833
     * @param int $socid Limit count on a particular third party id
4834
     * @param string $mode 'byunit'=number of unit, 'bynumber'=nb of entities
4835
     * @param int $filteronproducttype 0=To filter on product only, 1=To filter on services only
4836
     * @param int $year Year (0=last 12 month, -1=all years)
4837
     * @param string $morefilter More sql filters
4838
     * @return array                       Return integer <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
4839
     */
4840
    public function get_nb_order($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4841
    {
4842
        // phpcs:enable
4843
        global $conf, $user;
4844
4845
        $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4846
        if ($mode == 'bynumber') {
4847
            $sql .= ", count(DISTINCT c.rowid)";
4848
        }
4849
        $sql .= ", sum(d.total_ht) as total_ht";
4850
        $sql .= " FROM " . $this->db->prefix() . "commandedet as d, " . $this->db->prefix() . "commande as c, " . $this->db->prefix() . "societe as s";
4851
        if ($filteronproducttype >= 0) {
4852
            $sql .= ", " . $this->db->prefix() . "product as p";
4853
        }
4854
        if (!$user->hasRight('societe', 'client', 'voir')) {
4855
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
4856
        }
4857
        $sql .= " WHERE c.rowid = d.fk_commande";
4858
        if ($this->id > 0) {
4859
            $sql .= " AND d.fk_product = " . ((int)$this->id);
4860
        } else {
4861
            $sql .= " AND d.fk_product > 0";
4862
        }
4863
        if ($filteronproducttype >= 0) {
4864
            $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = " . ((int)$filteronproducttype);
4865
        }
4866
        $sql .= " AND c.fk_soc = s.rowid";
4867
        $sql .= " AND c.entity IN (" . getEntity('commande') . ")";
4868
        if (!$user->hasRight('societe', 'client', 'voir')) {
4869
            $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int)$user->id);
4870
        }
4871
        if ($socid > 0) {
4872
            $sql .= " AND c.fk_soc = " . ((int)$socid);
4873
        }
4874
        $sql .= $morefilter;
4875
        $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4876
        $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4877
4878
        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...
4879
    }
4880
4881
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4882
4883
    /**
4884
     *  Return nb of units in orders in which product is included
4885
     *
4886
     * @param int $socid Limit count on a particular third party id
4887
     * @param string $mode 'byunit'=number of unit, 'bynumber'=nb of entities
4888
     * @param int $filteronproducttype 0=To filter on product only, 1=To filter on services only
4889
     * @param int $year Year (0=last 12 month, -1=all years)
4890
     * @param string $morefilter More sql filters
4891
     * @return array                       Return integer <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
4892
     */
4893
    public function get_nb_ordersupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4894
    {
4895
        // phpcs:enable
4896
        global $conf, $user;
4897
4898
        $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4899
        if ($mode == 'bynumber') {
4900
            $sql .= ", count(DISTINCT c.rowid)";
4901
        }
4902
        $sql .= ", sum(d.total_ht) as total_ht";
4903
        $sql .= " FROM " . $this->db->prefix() . "commande_fournisseurdet as d, " . $this->db->prefix() . "commande_fournisseur as c, " . $this->db->prefix() . "societe as s";
4904
        if ($filteronproducttype >= 0) {
4905
            $sql .= ", " . $this->db->prefix() . "product as p";
4906
        }
4907
        if (!$user->hasRight('societe', 'client', 'voir')) {
4908
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
4909
        }
4910
        $sql .= " WHERE c.rowid = d.fk_commande";
4911
        if ($this->id > 0) {
4912
            $sql .= " AND d.fk_product = " . ((int)$this->id);
4913
        } else {
4914
            $sql .= " AND d.fk_product > 0";
4915
        }
4916
        if ($filteronproducttype >= 0) {
4917
            $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = " . ((int)$filteronproducttype);
4918
        }
4919
        $sql .= " AND c.fk_soc = s.rowid";
4920
        $sql .= " AND c.entity IN (" . getEntity('supplier_order') . ")";
4921
        if (!$user->hasRight('societe', 'client', 'voir')) {
4922
            $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int)$user->id);
4923
        }
4924
        if ($socid > 0) {
4925
            $sql .= " AND c.fk_soc = " . ((int)$socid);
4926
        }
4927
        $sql .= $morefilter;
4928
        $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4929
        $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4930
4931
        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...
4932
    }
4933
4934
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4935
4936
    /**
4937
     *  Return nb of units in orders in which product is included
4938
     *
4939
     * @param int $socid Limit count on a particular third party id
4940
     * @param string $mode 'byunit'=number of unit, 'bynumber'=nb of entities
4941
     * @param int $filteronproducttype 0=To filter on product only, 1=To filter on services only
4942
     * @param int $year Year (0=last 12 month, -1=all years)
4943
     * @param string $morefilter More sql filters
4944
     * @return array                       Return integer <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
4945
     */
4946
    public function get_nb_contract($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4947
    {
4948
        // phpcs:enable
4949
        global $conf, $user;
4950
4951
        $sql = "SELECT sum(d.qty) as qty, date_format(c.date_contrat, '%Y%m')";
4952
        if ($mode == 'bynumber') {
4953
            $sql .= ", count(DISTINCT c.rowid)";
4954
        }
4955
        $sql .= ", sum(d.total_ht) as total_ht";
4956
        $sql .= " FROM " . $this->db->prefix() . "contratdet as d, " . $this->db->prefix() . "contrat as c, " . $this->db->prefix() . "societe as s";
4957
        if ($filteronproducttype >= 0) {
4958
            $sql .= ", " . $this->db->prefix() . "product as p";
4959
        }
4960
        if (!$user->hasRight('societe', 'client', 'voir')) {
4961
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
4962
        }
4963
        $sql .= " WHERE c.entity IN (" . getEntity('contract') . ")";
4964
        $sql .= " AND c.rowid = d.fk_contrat";
4965
4966
        if ($this->id > 0) {
4967
            $sql .= " AND d.fk_product = " . ((int)$this->id);
4968
        } else {
4969
            $sql .= " AND d.fk_product > 0";
4970
        }
4971
        if ($filteronproducttype >= 0) {
4972
            $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = " . ((int)$filteronproducttype);
4973
        }
4974
        $sql .= " AND c.fk_soc = s.rowid";
4975
4976
        if (!$user->hasRight('societe', 'client', 'voir')) {
4977
            $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int)$user->id);
4978
        }
4979
        if ($socid > 0) {
4980
            $sql .= " AND c.fk_soc = " . ((int)$socid);
4981
        }
4982
        $sql .= $morefilter;
4983
        $sql .= " GROUP BY date_format(c.date_contrat,'%Y%m')";
4984
        $sql .= " ORDER BY date_format(c.date_contrat,'%Y%m') DESC";
4985
4986
        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...
4987
    }
4988
4989
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4990
4991
    /**
4992
     *  Return nb of units in orders in which product is included
4993
     *
4994
     * @param int $socid Limit count on a particular third party id
4995
     * @param string $mode 'byunit'=number of unit, 'bynumber'=nb of entities
4996
     * @param int $filteronproducttype 0=To filter on product only, 1=To filter on services only
4997
     * @param int $year Year (0=last 12 month, -1=all years)
4998
     * @param string $morefilter More sql filters
4999
     * @return array                       Return integer <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
5000
     */
5001
    public function get_nb_mos($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
5002
    {
5003
        // phpcs:enable
5004
        global $conf, $user;
5005
5006
        $sql = "SELECT sum(d.qty), date_format(d.date_valid, '%Y%m')";
5007
        if ($mode == 'bynumber') {
5008
            $sql .= ", count(DISTINCT d.rowid)";
5009
        }
5010
        $sql .= " FROM " . $this->db->prefix() . "mrp_mo as d LEFT JOIN  " . $this->db->prefix() . "societe as s ON d.fk_soc = s.rowid";
5011
        if ($filteronproducttype >= 0) {
5012
            $sql .= ", " . $this->db->prefix() . "product as p";
5013
        }
5014
        if (!$user->hasRight('societe', 'client', 'voir')) {
5015
            $sql .= ", " . $this->db->prefix() . "societe_commerciaux as sc";
5016
        }
5017
5018
        $sql .= " WHERE d.entity IN (" . getEntity('mo') . ")";
5019
        $sql .= " AND d.status > 0";
5020
5021
        if ($this->id > 0) {
5022
            $sql .= " AND d.fk_product = " . ((int)$this->id);
5023
        } else {
5024
            $sql .= " AND d.fk_product > 0";
5025
        }
5026
        if ($filteronproducttype >= 0) {
5027
            $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = " . ((int)$filteronproducttype);
5028
        }
5029
5030
        if (!$user->hasRight('societe', 'client', 'voir')) {
5031
            $sql .= " AND d.fk_soc = sc.fk_soc AND sc.fk_user = " . ((int)$user->id);
5032
        }
5033
        if ($socid > 0) {
5034
            $sql .= " AND d.fk_soc = " . ((int)$socid);
5035
        }
5036
        $sql .= $morefilter;
5037
        $sql .= " GROUP BY date_format(d.date_valid,'%Y%m')";
5038
        $sql .= " ORDER BY date_format(d.date_valid,'%Y%m') DESC";
5039
5040
        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...
5041
    }
5042
5043
    /**
5044
     *  Link a product/service to a parent product/service
5045
     *
5046
     * @param int $id_pere Id of parent product/service
5047
     * @param int $id_fils Id of child product/service
5048
     * @param float $qty Quantity
5049
     * @param int $incdec 1=Increase/decrease stock of child when parent stock increase/decrease
5050
     * @param int $notrigger Disable triggers
5051
     * @return int                Return integer < 0 if KO, > 0 if OK
5052
     */
5053
    public function add_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
5054
    {
5055
        global $user;
5056
5057
        // phpcs:enable
5058
        // Clean parameters
5059
        if (!is_numeric($id_pere)) {
5060
            $id_pere = 0;
5061
        }
5062
        if (!is_numeric($id_fils)) {
5063
            $id_fils = 0;
5064
        }
5065
        if (!is_numeric($incdec)) {
5066
            $incdec = 0;
5067
        }
5068
5069
        $result = $this->del_sousproduit($id_pere, $id_fils);
5070
        if ($result < 0) {
5071
            return $result;
5072
        }
5073
5074
        // Check not already father of id_pere (to avoid father -> child -> father links)
5075
        $sql = "SELECT fk_product_pere from " . $this->db->prefix() . "product_association";
5076
        $sql .= " WHERE fk_product_pere = " . ((int)$id_fils) . " AND fk_product_fils = " . ((int)$id_pere);
5077
        if (!$this->db->query($sql)) {
5078
            dol_print_error($this->db);
5079
            return -1;
5080
        } else {
5081
            //Selection of the highest row
5082
            $sql = "SELECT MAX(rang) as max_rank FROM " . $this->db->prefix() . "product_association";
5083
            $sql .= " WHERE fk_product_pere  = " . ((int)$id_pere);
5084
            $resql = $this->db->query($sql);
5085
            if ($resql) {
5086
                $obj = $this->db->fetch_object($resql);
5087
                $rank = $obj->max_rank + 1;
5088
                //Addition of a product with the highest rank +1
5089
                $sql = "INSERT INTO " . $this->db->prefix() . "product_association(fk_product_pere,fk_product_fils,qty,incdec,rang)";
5090
                $sql .= " VALUES (" . ((int)$id_pere) . ", " . ((int)$id_fils) . ", " . price2num($qty, 'MS') . ", " . ((int)$incdec) . ", " . ((int)$rank) . ")";
5091
                if (!$this->db->query($sql)) {
5092
                    dol_print_error($this->db);
5093
                    return -1;
5094
                } else {
5095
                    if (!$notrigger) {
5096
                        // Call trigger
5097
                        $result = $this->call_trigger('PRODUCT_SUBPRODUCT_ADD', $user);
5098
                        if ($result < 0) {
5099
                            $this->error = $this->db->lasterror();
5100
                            dol_syslog(get_only_class($this) . '::addSubproduct error=' . $this->error, LOG_ERR);
5101
                            return -1;
5102
                        }
5103
                    }
5104
                    // End call triggers
5105
5106
                    return 1;
5107
                }
5108
            } else {
5109
                dol_print_error($this->db);
5110
                return -1;
5111
            }
5112
        }
5113
    }
5114
5115
    /**
5116
     *  Remove a link between a subproduct and a parent product/service
5117
     *
5118
     * @param int $fk_parent Id of parent product (child will no more be linked to it)
5119
     * @param int $fk_child Id of child product
5120
     * @param int $notrigger Disable triggers
5121
     * @return int            Return integer < 0 if KO, > 0 if OK
5122
     */
5123
    public function del_sousproduit($fk_parent, $fk_child, $notrigger = 0)
5124
    {
5125
        global $user;
5126
5127
        // phpcs:enable
5128
        if (!is_numeric($fk_parent)) {
5129
            $fk_parent = 0;
5130
        }
5131
        if (!is_numeric($fk_child)) {
5132
            $fk_child = 0;
5133
        }
5134
5135
        $sql = "DELETE FROM " . $this->db->prefix() . "product_association";
5136
        $sql .= " WHERE fk_product_pere  = " . ((int)$fk_parent);
5137
        $sql .= " AND fk_product_fils = " . ((int)$fk_child);
5138
5139
        dol_syslog(get_only_class($this) . '::del_sousproduit', LOG_DEBUG);
5140
        if (!$this->db->query($sql)) {
5141
            dol_print_error($this->db);
5142
            return -1;
5143
        }
5144
5145
        // Updated ranks so that none are missing
5146
        $sqlrank = "SELECT rowid, rang FROM " . $this->db->prefix() . "product_association";
5147
        $sqlrank .= " WHERE fk_product_pere = " . ((int)$fk_parent);
5148
        $sqlrank .= " ORDER BY rang";
5149
        $resqlrank = $this->db->query($sqlrank);
5150
        if ($resqlrank) {
5151
            $cpt = 0;
5152
            while ($objrank = $this->db->fetch_object($resqlrank)) {
5153
                $cpt++;
5154
                $sql = "UPDATE " . $this->db->prefix() . "product_association";
5155
                $sql .= " SET rang = " . ((int)$cpt);
5156
                $sql .= " WHERE rowid = " . ((int)$objrank->rowid);
5157
                if (!$this->db->query($sql)) {
5158
                    dol_print_error($this->db);
5159
                    return -1;
5160
                }
5161
            }
5162
        }
5163
5164
        if (!$notrigger) {
5165
            // Call trigger
5166
            $result = $this->call_trigger('PRODUCT_SUBPRODUCT_DELETE', $user);
5167
            if ($result < 0) {
5168
                $this->error = $this->db->lasterror();
5169
                dol_syslog(get_only_class($this) . '::delSubproduct error=' . $this->error, LOG_ERR);
5170
                return -1;
5171
            }
5172
            // End call triggers
5173
        }
5174
5175
        return 1;
5176
    }
5177
5178
    /**
5179
     *  Modify composed product
5180
     *
5181
     * @param int $id_pere Id of parent product/service
5182
     * @param int $id_fils Id of child product/service
5183
     * @param float $qty Quantity
5184
     * @param int $incdec 1=Increase/decrease stock of child when parent stock increase/decrease
5185
     * @param int $notrigger Disable triggers
5186
     * @return int                Return integer < 0 if KO, > 0 if OK
5187
     */
5188
    public function update_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
5189
    {
5190
        global $user;
5191
5192
        // phpcs:enable
5193
        // Clean parameters
5194
        if (!is_numeric($id_pere)) {
5195
            $id_pere = 0;
5196
        }
5197
        if (!is_numeric($id_fils)) {
5198
            $id_fils = 0;
5199
        }
5200
        if (!is_numeric($incdec)) {
5201
            $incdec = 1;
5202
        }
5203
        if (!is_numeric($qty)) {
5204
            $qty = 1;
5205
        }
5206
5207
        $sql = 'UPDATE ' . $this->db->prefix() . 'product_association SET ';
5208
        $sql .= 'qty = ' . price2num($qty, 'MS');
5209
        $sql .= ',incdec = ' . ((int)$incdec);
5210
        $sql .= ' WHERE fk_product_pere = ' . ((int)$id_pere) . ' AND fk_product_fils = ' . ((int)$id_fils);
5211
5212
        if (!$this->db->query($sql)) {
5213
            dol_print_error($this->db);
5214
            return -1;
5215
        } else {
5216
            if (!$notrigger) {
5217
                // Call trigger
5218
                $result = $this->call_trigger('PRODUCT_SUBPRODUCT_UPDATE', $user);
5219
                if ($result < 0) {
5220
                    $this->error = $this->db->lasterror();
5221
                    dol_syslog(get_only_class($this) . '::updateSubproduct error=' . $this->error, LOG_ERR);
5222
                    return -1;
5223
                }
5224
                // End call triggers
5225
            }
5226
5227
            return 1;
5228
        }
5229
    }
5230
5231
    /**
5232
     *  Check if it is a sub-product into a kit
5233
     *
5234
     * @param int $fk_parent Id of parent kit product
5235
     * @param int $fk_child Id of child product
5236
     * @return int                  Return 1 or 0; -1 if error
5237
     */
5238
    public function is_sousproduit($fk_parent, $fk_child)
5239
    {
5240
        // phpcs:enable
5241
        $sql = "SELECT fk_product_pere, qty, incdec";
5242
        $sql .= " FROM " . $this->db->prefix() . "product_association";
5243
        $sql .= " WHERE fk_product_pere  = " . ((int)$fk_parent);
5244
        $sql .= " AND fk_product_fils = " . ((int)$fk_child);
5245
5246
        $result = $this->db->query($sql);
5247
        if ($result) {
5248
            $num = $this->db->num_rows($result);
5249
5250
            if ($num > 0) {
5251
                $obj = $this->db->fetch_object($result);
5252
5253
                $this->is_sousproduit_qty = $obj->qty;
5254
                $this->is_sousproduit_incdec = $obj->incdec;
5255
5256
                return 1;
5257
            } else {
5258
                return 0;
5259
            }
5260
        } else {
5261
            dol_print_error($this->db);
5262
            return -1;
5263
        }
5264
    }
5265
5266
    /**
5267
     *  Add a supplier price for the product.
5268
     *  Note: Duplicate ref is accepted for different quantity only, or for different companies.
5269
     *
5270
     * @param User $user User that make link
5271
     * @param int $id_fourn Supplier id
5272
     * @param string $ref_fourn Supplier ref
5273
     * @param float $quantity Quantity minimum for price
5274
     * @return int               Return integer < 0 if KO, 0 if link already exists for this product, > 0 if OK
5275
     */
5276
    public function add_fournisseur($user, $id_fourn, $ref_fourn, $quantity)
5277
    {
5278
        // phpcs:enable
5279
        global $conf;
5280
5281
        $now = dol_now();
5282
5283
        dol_syslog(get_only_class($this) . "::add_fournisseur id_fourn = " . $id_fourn . " ref_fourn=" . $ref_fourn . " quantity=" . $quantity, LOG_DEBUG);
5284
5285
        // Clean parameters
5286
        $quantity = price2num($quantity, 'MS');
5287
5288
        if ($ref_fourn) {
5289
            // Check if ref is not already used
5290
            $sql = "SELECT rowid, fk_product";
5291
            $sql .= " FROM " . $this->db->prefix() . "product_fournisseur_price";
5292
            $sql .= " WHERE fk_soc = " . ((int)$id_fourn);
5293
            $sql .= " AND ref_fourn = '" . $this->db->escape($ref_fourn) . "'";
5294
            $sql .= " AND fk_product <> " . ((int)$this->id);
5295
            $sql .= " AND entity IN (" . getEntity('productsupplierprice') . ")";
5296
5297
            $resql = $this->db->query($sql);
5298
            if ($resql) {
5299
                $obj = $this->db->fetch_object($resql);
5300
                if ($obj) {
5301
                    // If the supplier ref already exists but for another product (duplicate ref is accepted for different quantity only or different companies)
5302
                    $this->product_id_already_linked = $obj->fk_product;
5303
                    return -3;
5304
                }
5305
                $this->db->free($resql);
5306
            }
5307
        }
5308
5309
        $sql = "SELECT rowid";
5310
        $sql .= " FROM " . $this->db->prefix() . "product_fournisseur_price";
5311
        $sql .= " WHERE fk_soc = " . ((int)$id_fourn);
5312
        if ($ref_fourn) {
5313
            $sql .= " AND ref_fourn = '" . $this->db->escape($ref_fourn) . "'";
5314
        } else {
5315
            $sql .= " AND (ref_fourn = '' OR ref_fourn IS NULL)";
5316
        }
5317
        $sql .= " AND quantity = " . ((float)$quantity);
5318
        $sql .= " AND fk_product = " . ((int)$this->id);
5319
        $sql .= " AND entity IN (" . getEntity('productsupplierprice') . ")";
5320
5321
        $resql = $this->db->query($sql);
5322
        if ($resql) {
5323
            $obj = $this->db->fetch_object($resql);
5324
5325
            // The reference supplier does not exist, we create it for this product.
5326
            if (empty($obj)) {
5327
                $sql = "INSERT INTO " . $this->db->prefix() . "product_fournisseur_price(";
5328
                $sql .= "datec";
5329
                $sql .= ", entity";
5330
                $sql .= ", fk_product";
5331
                $sql .= ", fk_soc";
5332
                $sql .= ", ref_fourn";
5333
                $sql .= ", quantity";
5334
                $sql .= ", fk_user";
5335
                $sql .= ", tva_tx";
5336
                $sql .= ") VALUES (";
5337
                $sql .= "'" . $this->db->idate($now) . "'";
5338
                $sql .= ", " . ((int)$conf->entity);
5339
                $sql .= ", " . ((int)$this->id);
5340
                $sql .= ", " . ((int)$id_fourn);
5341
                $sql .= ", '" . $this->db->escape($ref_fourn) . "'";
5342
                $sql .= ", " . ((float)$quantity);
5343
                $sql .= ", " . ((int)$user->id);
5344
                $sql .= ", 0";
5345
                $sql .= ")";
5346
5347
                if ($this->db->query($sql)) {
5348
                    $this->product_fourn_price_id = $this->db->last_insert_id($this->db->prefix() . "product_fournisseur_price");
5349
                    return 1;
5350
                } else {
5351
                    $this->error = $this->db->lasterror();
5352
                    return -1;
5353
                }
5354
            } else {
5355
                // If the supplier price already exists for this product and quantity
5356
                $this->product_fourn_price_id = $obj->rowid;
5357
                return 0;
5358
            }
5359
        } else {
5360
            $this->error = $this->db->lasterror();
5361
            return -2;
5362
        }
5363
    }
5364
5365
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5366
5367
    /**
5368
     * Return list of suppliers providing the product or service
5369
     *
5370
     * @return array        Array of vendor ids
5371
     */
5372
    public function list_suppliers()
5373
    {
5374
        // phpcs:enable
5375
        global $conf;
5376
5377
        $list = array();
5378
5379
        $sql = "SELECT DISTINCT p.fk_soc";
5380
        $sql .= " FROM " . $this->db->prefix() . "product_fournisseur_price as p";
5381
        $sql .= " WHERE p.fk_product = " . ((int)$this->id);
5382
        $sql .= " AND p.entity = " . ((int)$conf->entity);
5383
5384
        $result = $this->db->query($sql);
5385
        if ($result) {
5386
            $num = $this->db->num_rows($result);
5387
            $i = 0;
5388
            while ($i < $num) {
5389
                $obj = $this->db->fetch_object($result);
5390
                $list[$i] = $obj->fk_soc;
5391
                $i++;
5392
            }
5393
        }
5394
5395
        return $list;
5396
    }
5397
5398
    /**
5399
     *  Recopie les prix d'un produit/service sur un autre
5400
     *
5401
     * @param int $fromId Id product source
5402
     * @param int $toId Id product target
5403
     * @return int                     Return integer < 0 if KO, > 0 if OK
5404
     */
5405
    public function clone_price($fromId, $toId)
5406
    {
5407
        global $conf, $user;
5408
5409
        $now = dol_now();
5410
5411
        $this->db->begin();
5412
5413
        // prices
5414
        $sql = "INSERT INTO " . $this->db->prefix() . "product_price (";
5415
        $sql .= " entity";
5416
        $sql .= ", fk_product";
5417
        $sql .= ", date_price";
5418
        $sql .= ", price_level";
5419
        $sql .= ", price";
5420
        $sql .= ", price_ttc";
5421
        $sql .= ", price_min";
5422
        $sql .= ", price_min_ttc";
5423
        $sql .= ", price_base_type";
5424
        $sql .= ", price_label";
5425
        $sql .= ", default_vat_code";
5426
        $sql .= ", tva_tx";
5427
        $sql .= ", recuperableonly";
5428
        $sql .= ", localtax1_tx";
5429
        $sql .= ", localtax1_type";
5430
        $sql .= ", localtax2_tx";
5431
        $sql .= ", localtax2_type";
5432
        $sql .= ", fk_user_author";
5433
        $sql .= ", tosell";
5434
        $sql .= ", price_by_qty";
5435
        $sql .= ", fk_price_expression";
5436
        $sql .= ", fk_multicurrency";
5437
        $sql .= ", multicurrency_code";
5438
        $sql .= ", multicurrency_tx";
5439
        $sql .= ", multicurrency_price";
5440
        $sql .= ", multicurrency_price_ttc";
5441
        $sql .= ")";
5442
        $sql .= " SELECT";
5443
        $sql .= " entity";
5444
        $sql .= ", " . $toId;
5445
        $sql .= ", '" . $this->db->idate($now) . "'";
5446
        $sql .= ", price_level";
5447
        $sql .= ", price";
5448
        $sql .= ", price_ttc";
5449
        $sql .= ", price_min";
5450
        $sql .= ", price_min_ttc";
5451
        $sql .= ", price_base_type";
5452
        $sql .= ", price_label";
5453
        $sql .= ", default_vat_code";
5454
        $sql .= ", tva_tx";
5455
        $sql .= ", recuperableonly";
5456
        $sql .= ", localtax1_tx";
5457
        $sql .= ", localtax1_type";
5458
        $sql .= ", localtax2_tx";
5459
        $sql .= ", localtax2_type";
5460
        $sql .= ", " . $user->id;
5461
        $sql .= ", tosell";
5462
        $sql .= ", price_by_qty";
5463
        $sql .= ", fk_price_expression";
5464
        $sql .= ", fk_multicurrency";
5465
        $sql .= ", multicurrency_code";
5466
        $sql .= ", multicurrency_tx";
5467
        $sql .= ", multicurrency_price";
5468
        $sql .= ", multicurrency_price_ttc";
5469
        $sql .= " FROM " . $this->db->prefix() . "product_price ps";
5470
        $sql .= " WHERE fk_product = " . ((int)$fromId);
5471
        $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)";
5472
        $sql .= " ORDER BY date_price DESC";
5473
5474
        dol_syslog(__METHOD__, LOG_DEBUG);
5475
        $resql = $this->db->query($sql);
5476
        if (!$resql) {
5477
            $this->db->rollback();
5478
            return -1;
5479
        }
5480
5481
        $this->db->commit();
5482
        return 1;
5483
    }
5484
5485
    /**
5486
     * Clone links between products
5487
     *
5488
     * @param int $fromId Product id
5489
     * @param int $toId Product id
5490
     * @return int                  Return integer <0 if KO, >0 if OK
5491
     */
5492
    public function clone_associations($fromId, $toId)
5493
    {
5494
        // phpcs:enable
5495
        $this->db->begin();
5496
5497
        $sql = 'INSERT INTO ' . $this->db->prefix() . 'product_association (fk_product_pere, fk_product_fils, qty, incdec)';
5498
        $sql .= " SELECT " . $toId . ", fk_product_fils, qty, incdec FROM " . $this->db->prefix() . "product_association";
5499
        $sql .= " WHERE fk_product_pere = " . ((int)$fromId);
5500
5501
        dol_syslog(get_only_class($this) . '::clone_association', LOG_DEBUG);
5502
        if (!$this->db->query($sql)) {
5503
            $this->db->rollback();
5504
            return -1;
5505
        }
5506
5507
        $this->db->commit();
5508
        return 1;
5509
    }
5510
5511
    /**
5512
     *  Recopie les fournisseurs et prix fournisseurs d'un produit/service sur un autre
5513
     *
5514
     * @param int $fromId Id produit source
5515
     * @param int $toId Id produit cible
5516
     * @return int                 Return integer < 0 si erreur, > 0 si ok
5517
     */
5518
    public function clone_fournisseurs($fromId, $toId)
5519
    {
5520
        // phpcs:enable
5521
        $this->db->begin();
5522
5523
        $now = dol_now();
5524
5525
        // les fournisseurs
5526
        /*$sql = "INSERT ".$this->db->prefix()."product_fournisseur ("
5527
         . " datec, fk_product, fk_soc, ref_fourn, fk_user_author )"
5528
         . " SELECT '".$this->db->idate($now)."', ".$toId.", fk_soc, ref_fourn, fk_user_author"
5529
         . " FROM ".$this->db->prefix()."product_fournisseur"
5530
         . " WHERE fk_product = ".((int) $fromId);
5531
5532
         if ( ! $this->db->query($sql ) )
5533
         {
5534
         $this->db->rollback();
5535
         return -1;
5536
         }*/
5537
5538
        // les prix de fournisseurs.
5539
        $sql = "INSERT " . $this->db->prefix() . "product_fournisseur_price (";
5540
        $sql .= " datec, fk_product, fk_soc, price, quantity, fk_user, tva_tx)";
5541
        $sql .= " SELECT '" . $this->db->idate($now) . "', " . ((int)$toId) . ", fk_soc, price, quantity, fk_user, tva_tx";
5542
        $sql .= " FROM " . $this->db->prefix() . "product_fournisseur_price";
5543
        $sql .= " WHERE fk_product = " . ((int)$fromId);
5544
5545
        dol_syslog(get_only_class($this) . '::clone_fournisseurs', LOG_DEBUG);
5546
        $resql = $this->db->query($sql);
5547
        if (!$resql) {
5548
            $this->db->rollback();
5549
            return -1;
5550
        } else {
5551
            $this->db->commit();
5552
            return 1;
5553
        }
5554
    }
5555
5556
    /**
5557
     *  Build the tree of subproducts and return it.
5558
     *  this->sousprods must have been loaded by this->get_sousproduits_arbo()
5559
     *
5560
     * @param int $multiply Because each sublevel must be multiplicated by parent nb
5561
     * @param int $ignore_stock_load Ignore stock load
5562
     * @return array                        Array with tree
5563
     */
5564
    public function get_arbo_each_prod($multiply = 1, $ignore_stock_load = 0)
5565
    {
5566
        // phpcs:enable
5567
        $this->res = array();
5568
        if (isset($this->sousprods) && is_array($this->sousprods)) {
5569
            foreach ($this->sousprods as $prod_name => $desc_product) {
5570
                if (is_array($desc_product)) {
5571
                    $this->fetch_prod_arbo($desc_product, "", $multiply, 1, $this->id, $ignore_stock_load); // This set $this->res
5572
                }
5573
            }
5574
        }
5575
        //var_dump($res);
5576
        return $this->res;
5577
    }
5578
5579
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5580
5581
    /**
5582
     *  Function recursive, used only by get_arbo_each_prod(), to build tree of subproducts into ->res
5583
     *  Define value of this->res
5584
     *
5585
     * @param array $prod Products array
5586
     * @param string $compl_path Directory path of parents to add before
5587
     * @param int $multiply Because each sublevel must be multiplicated by parent nb
5588
     * @param int $level Init level
5589
     * @param int $id_parent Id parent
5590
     * @param int $ignore_stock_load Ignore stock load
5591
     * @return void
5592
     */
5593
    public function fetch_prod_arbo($prod, $compl_path = '', $multiply = 1, $level = 1, $id_parent = 0, $ignore_stock_load = 0)
5594
    {
5595
        // phpcs:enable
5596
        global $conf, $langs;
5597
5598
        $tmpproduct = null;
5599
        //var_dump($prod);
5600
        foreach ($prod as $id_product => $desc_pere) {    // $id_product is 0 (first call starting with root top) or an id of a sub_product
5601
            if (is_array($desc_pere)) {    // If desc_pere is an array, this means it's a child
5602
                $id = (!empty($desc_pere[0]) ? $desc_pere[0] : '');
5603
                $nb = (!empty($desc_pere[1]) ? $desc_pere[1] : '');
5604
                $type = (!empty($desc_pere[2]) ? $desc_pere[2] : '');
5605
                $label = (!empty($desc_pere[3]) ? $desc_pere[3] : '');
5606
                $incdec = (!empty($desc_pere[4]) ? $desc_pere[4] : 0);
5607
5608
                if ($multiply < 1) {
5609
                    $multiply = 1;
5610
                }
5611
5612
                //print "XXX We add id=".$id." - label=".$label." - nb=".$nb." - multiply=".$multiply." fullpath=".$compl_path.$label."\n";
5613
                if (is_null($tmpproduct)) {
5614
                    $tmpproduct = new Product($this->db); // So we initialize tmpproduct only once for all loop.
5615
                }
5616
                $tmpproduct->fetch($id); // Load product to get ->ref
5617
5618
                if (empty($ignore_stock_load) && ($tmpproduct->isProduct() || getDolGlobalString('STOCK_SUPPORTS_SERVICES'))) {
5619
                    $tmpproduct->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
5620
                }
5621
5622
                $this->res[] = array(
5623
                    'id' => $id, // Id product
5624
                    'id_parent' => $id_parent,
5625
                    'ref' => $tmpproduct->ref, // Ref product
5626
                    'nb' => $nb, // Nb of units that compose parent product
5627
                    'nb_total' => $nb * $multiply, // Nb of units for all nb of product
5628
                    'stock' => $tmpproduct->stock_reel, // Stock
5629
                    'stock_alert' => $tmpproduct->seuil_stock_alerte, // Stock alert
5630
                    'label' => $label,
5631
                    'fullpath' => $compl_path . $label, // Label
5632
                    'type' => $type, // Nb of units that compose parent product
5633
                    'desiredstock' => $tmpproduct->desiredstock,
5634
                    'level' => $level,
5635
                    'incdec' => $incdec,
5636
                    'entity' => $tmpproduct->entity
5637
                );
5638
5639
                // Recursive call if there child has children of its own
5640
                if (isset($desc_pere['childs']) && is_array($desc_pere['childs'])) {
5641
                    //print 'YYY We go down for '.$desc_pere[3]." -> \n";
5642
                    $this->fetch_prod_arbo($desc_pere['childs'], $compl_path . $desc_pere[3] . " -> ", $desc_pere[1] * $multiply, $level + 1, $id, $ignore_stock_load);
5643
                }
5644
            }
5645
        }
5646
    }
5647
5648
    /**
5649
     * Count all parent and children products for current product (first level only)
5650
     *
5651
     * @param int $mode 0=Both parent and child, -1=Parents only, 1=Children only
5652
     * @return  int             Nb of father + child
5653
     * @see getFather(), get_sousproduits_arbo()
5654
     */
5655
    public function hasFatherOrChild($mode = 0)
5656
    {
5657
        $nb = 0;
5658
5659
        $sql = "SELECT COUNT(pa.rowid) as nb";
5660
        $sql .= " FROM " . $this->db->prefix() . "product_association as pa";
5661
        if ($mode == 0) {
5662
            $sql .= " WHERE pa.fk_product_fils = " . ((int)$this->id) . " OR pa.fk_product_pere = " . ((int)$this->id);
5663
        } elseif ($mode == -1) {
5664
            $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)
5665
        } elseif ($mode == 1) {
5666
            $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)
5667
        }
5668
5669
        $resql = $this->db->query($sql);
5670
        if ($resql) {
5671
            $obj = $this->db->fetch_object($resql);
5672
            if ($obj) {
5673
                $nb = $obj->nb;
5674
            }
5675
        } else {
5676
            return -1;
5677
        }
5678
5679
        return $nb;
5680
    }
5681
5682
5683
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5684
5685
    /**
5686
     * Return if a product has variants or not
5687
     *
5688
     * @return int        Number of variants
5689
     */
5690
    public function hasVariants()
5691
    {
5692
        $nb = 0;
5693
        $sql = "SELECT count(rowid) as nb FROM " . $this->db->prefix() . "product_attribute_combination WHERE fk_product_parent = " . ((int)$this->id);
5694
        $sql .= " AND entity IN (" . getEntity('product') . ")";
5695
5696
        $resql = $this->db->query($sql);
5697
        if ($resql) {
5698
            $obj = $this->db->fetch_object($resql);
5699
            if ($obj) {
5700
                $nb = $obj->nb;
5701
            }
5702
        }
5703
5704
        return $nb;
5705
    }
5706
5707
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5708
5709
    /**
5710
     * Return if loaded product is a variant
5711
     *
5712
     * @return bool|int     Return true if the product is a variant, false if not, -1 if error
5713
     */
5714
    public function isVariant()
5715
    {
5716
        global $conf;
5717
        if (isModEnabled('variants')) {
5718
            $sql = "SELECT rowid FROM " . $this->db->prefix() . "product_attribute_combination WHERE fk_product_child = " . ((int)$this->id) . " AND entity IN (" . getEntity('product') . ")";
5719
5720
            $query = $this->db->query($sql);
5721
5722
            if ($query) {
5723
                if (!$this->db->num_rows($query)) {
5724
                    return false;
5725
                }
5726
                return true;
5727
            } else {
5728
                dol_print_error($this->db);
5729
                return -1;
5730
            }
5731
        } else {
5732
            return false;
5733
        }
5734
    }
5735
5736
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5737
5738
    /**
5739
     *     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)
5740
     *     Set this->sousprods
5741
     *
5742
     * @return void
5743
     */
5744
    public function get_sousproduits_arbo()
5745
    {
5746
        // phpcs:enable
5747
        $parent = array();
5748
5749
        foreach ($this->getChildsArbo($this->id) as $keyChild => $valueChild) {    // Warning. getChildsArbo can call getChildsArbo recursively. Starting point is $value[0]=id of product
5750
            $parent[$this->label][$keyChild] = $valueChild;
5751
        }
5752
        foreach ($parent as $key => $value) {        // key=label, value is array of children
5753
            $this->sousprods[$key] = $value;
5754
        }
5755
    }
5756
5757
5758
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5759
5760
    /**
5761
     *  Return children of product $id
5762
     *
5763
     * @param int $id Id of product to search children of
5764
     * @param int $firstlevelonly Return only direct child
5765
     * @param int $level Level of recursing call (start to 1)
5766
     * @param array $parents Array of all parents of $id
5767
     * @return array|int                    Return array(prodid=>array(0=prodid, 1=>qty, 2=>product type, 3=>label, 4=>incdec, 5=>product ref)
5768
     */
5769
    public function getChildsArbo($id, $firstlevelonly = 0, $level = 1, $parents = array())
5770
    {
5771
        global $alreadyfound;
5772
5773
        if (empty($id)) {
5774
            return array();
5775
        }
5776
5777
        $sql = "SELECT p.rowid, p.ref, p.label as label, p.fk_product_type,";
5778
        $sql .= " pa.qty as qty, pa.fk_product_fils as id, pa.incdec,";
5779
        $sql .= " pa.rowid as fk_association, pa.rang";
5780
        $sql .= " FROM " . $this->db->prefix() . "product as p,";
5781
        $sql .= " " . $this->db->prefix() . "product_association as pa";
5782
        $sql .= " WHERE p.rowid = pa.fk_product_fils";
5783
        $sql .= " AND pa.fk_product_pere = " . ((int)$id);
5784
        $sql .= " AND pa.fk_product_fils <> " . ((int)$id); // This should not happens, it is to avoid infinite loop if it happens
5785
        $sql .= " ORDER BY pa.rang";
5786
5787
        dol_syslog(get_only_class($this) . '::getChildsArbo id=' . $id . ' level=' . $level . ' parents=' . (is_array($parents) ? implode(',', $parents) : $parents), LOG_DEBUG);
5788
5789
        if ($level == 1) {
5790
            $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
5791
        }
5792
        // Protection against infinite loop
5793
        if ($level > 30) {
5794
            return array();
5795
        }
5796
5797
        $res = $this->db->query($sql);
5798
        if ($res) {
5799
            $prods = array();
5800
            while ($rec = $this->db->fetch_array($res)) {
5801
                if (!empty($alreadyfound[$rec['rowid']])) {
5802
                    dol_syslog(get_only_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);
5803
                    if (in_array($rec['id'], $parents)) {
5804
                        continue; // We discard this child if it is already found at a higher level in tree in the same branch.
5805
                    }
5806
                }
5807
                $alreadyfound[$rec['rowid']] = 1;
5808
                $prods[$rec['rowid']] = array(
5809
                    0 => $rec['rowid'],
5810
                    1 => $rec['qty'],
5811
                    2 => $rec['fk_product_type'],
5812
                    3 => $this->db->escape($rec['label']),
5813
                    4 => $rec['incdec'],
5814
                    5 => $rec['ref'],
5815
                    6 => $rec['fk_association'],
5816
                    7 => $rec['rang']
5817
                );
5818
                //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty'],2=>$rec['fk_product_type']);
5819
                //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty']);
5820
                if (empty($firstlevelonly)) {
5821
                    $parents[] = $rec['rowid'];
5822
                    $listofchilds = $this->getChildsArbo($rec['rowid'], 0, $level + 1, $parents);
5823
                    foreach ($listofchilds as $keyChild => $valueChild) {
5824
                        $prods[$rec['rowid']]['childs'][$keyChild] = $valueChild;
5825
                    }
5826
                }
5827
            }
5828
5829
            return $prods;
5830
        } else {
5831
            dol_print_error($this->db);
5832
            return -1;
5833
        }
5834
    }
5835
5836
    /**
5837
     *  Create a document onto disk according to template module.
5838
     *
5839
     * @param string $modele Force model to use ('' to not force)
5840
     * @param Translate $outputlangs Object langs to use for output
5841
     * @param int $hidedetails Hide details of lines
5842
     * @param int $hidedesc Hide description
5843
     * @param int $hideref Hide ref
5844
     * @return int                         0 if KO, 1 if OK
5845
     */
5846
    public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0)
5847
    {
5848
        global $conf, $user, $langs;
5849
5850
        $langs->load("products");
5851
        $outputlangs->load("products");
5852
5853
        // Positionne le modele sur le nom du modele a utiliser
5854
        if (!dol_strlen($modele)) {
5855
            $modele = getDolGlobalString('PRODUCT_ADDON_PDF', 'strato');
5856
        }
5857
5858
        $modelpath = "core/modules/product/doc/";
5859
5860
        return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref);
5861
    }
5862
5863
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5864
5865
    /**
5866
     *  Retour label of nature of product
5867
     *
5868
     * @return string|int        Return label or ''. -1 if error
5869
     */
5870
    public function getLibFinished()
5871
    {
5872
        global $langs;
5873
        $langs->load('products');
5874
        $label = '';
5875
5876
        if (isset($this->finished) && $this->finished >= 0) {
5877
            $sql = "SELECT label, code FROM " . $this->db->prefix() . "c_product_nature where code = " . ((int)$this->finished) . " AND active=1";
5878
            $resql = $this->db->query($sql);
5879
            if (!$resql) {
5880
                $this->error = $this->db->error() . ' sql=' . $sql;
5881
                dol_syslog(__METHOD__ . ' Error ' . $this->error, LOG_ERR);
5882
                return -1;
5883
            } elseif ($this->db->num_rows($resql) > 0 && $res = $this->db->fetch_array($resql)) {
5884
                $label = $langs->trans($res['label']);
5885
            }
5886
            $this->db->free($resql);
5887
        }
5888
5889
        return $label;
5890
    }
5891
5892
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5893
5894
    /**
5895
     *  Load existing information about a serial
5896
     *
5897
     * @param string $batch Lot/serial number
5898
     * @return array                    Array with record into product_batch
5899
     * @see    load_stock(), load_virtual_stock()
5900
     */
5901
    public function loadBatchInfo($batch)
5902
    {
5903
        $result = array();
5904
5905
        $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";
5906
        $sql .= " WHERE pb.fk_product_stock = ps.rowid AND ps.fk_product = " . ((int)$this->id) . " AND pb.batch = '" . $this->db->escape($batch) . "'";
5907
        $sql .= " GROUP BY pb.batch, pb.eatby, pb.sellby";
5908
        dol_syslog(get_only_class($this) . "::loadBatchInfo load first entry found for lot/serial = " . $batch, LOG_DEBUG);
5909
        $resql = $this->db->query($sql);
5910
        if ($resql) {
5911
            $num = $this->db->num_rows($resql);
5912
            $i = 0;
5913
            while ($i < $num) {
5914
                $obj = $this->db->fetch_object($resql);
5915
                $result[] = array('batch' => $batch, 'eatby' => $this->db->jdate($obj->eatby), 'sellby' => $this->db->jdate($obj->sellby), 'qty' => $obj->qty);
5916
                $i++;
5917
            }
5918
            return $result;
5919
        } else {
5920
            dol_print_error($this->db);
5921
            $this->db->rollback();
5922
            return array();
5923
        }
5924
    }
5925
5926
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5927
5928
    /**
5929
     *  Move an uploaded file described into $file array into target directory $sdir.
5930
     *
5931
     * @param string $sdir Target directory
5932
     * @param string $file Array of file info of file to upload: array('name'=>..., 'tmp_name'=>...)
5933
     * @return int                    Return integer <0 if KO, >0 if OK
5934
     */
5935
    public function add_photo($sdir, $file)
5936
    {
5937
        // phpcs:enable
5938
        global $conf;
5939
5940
        include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
5941
5942
        $result = 0;
5943
5944
        $dir = $sdir;
5945
        if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
5946
            $dir .= '/' . get_exdir($this->id, 2, 0, 0, $this, 'product') . $this->id . "/photos";
5947
        } else {
5948
            $dir .= '/' . get_exdir(0, 0, 0, 0, $this, 'product') . dol_sanitizeFileName($this->ref);
5949
        }
5950
5951
        dol_mkdir($dir);
5952
5953
        $dir_osencoded = $dir;
5954
5955
        if (is_dir($dir_osencoded)) {
5956
            $originImage = $dir . '/' . $file['name'];
5957
5958
            // Cree fichier en taille origine
5959
            $result = dol_move_uploaded_file($file['tmp_name'], $originImage, 1);
5960
5961
            if (file_exists(dol_osencode($originImage))) {
5962
                // Create thumbs
5963
                $this->addThumbs($originImage);
5964
            }
5965
        }
5966
5967
        if (is_numeric($result) && $result > 0) {
5968
            return 1;
5969
        } else {
5970
            return -1;
5971
        }
5972
    }
5973
5974
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5975
5976
    /**
5977
     * Return an array with all photos of product found on disk. There is no sorting criteria.
5978
     *
5979
     * @param string $dir Directory to scan
5980
     * @param int $nbmax Number maximum of photos (0=no maximum)
5981
     * @return array            Array of photos
5982
     */
5983
    public function liste_photos($dir, $nbmax = 0)
5984
    {
5985
        // phpcs:enable
5986
        include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
5987
        include_once DOL_DOCUMENT_ROOT . '/core/lib/images.lib.php';
5988
5989
        $nbphoto = 0;
5990
        $tabobj = array();
5991
5992
        $dir_osencoded = dol_osencode($dir);
5993
        $handle = @opendir($dir_osencoded);
5994
        if (is_resource($handle)) {
5995
            while (($file = readdir($handle)) !== false) {
5996
                if (!utf8_check($file)) {
5997
                    $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // readdir returns ISO
5998
                }
5999
                if (dol_is_file($dir . $file) && image_format_supported($file) >= 0) {
6000
                    $nbphoto++;
6001
6002
                    // We forge name of thumb.
6003
                    $photo = $file;
6004
                    $photo_vignette = '';
6005
                    $regs = array();
6006
                    if (preg_match('/(' . $this->regeximgext . ')$/i', $photo, $regs)) {
6007
                        $photo_vignette = preg_replace('/' . $regs[0] . '/i', '', $photo) . '_small' . $regs[0];
6008
                    }
6009
6010
                    $dirthumb = $dir . 'thumbs/';
6011
6012
                    // Object
6013
                    $obj = array();
6014
                    $obj['photo'] = $photo;
6015
                    if ($photo_vignette && dol_is_file($dirthumb . $photo_vignette)) {
6016
                        $obj['photo_vignette'] = 'thumbs/' . $photo_vignette;
6017
                    } else {
6018
                        $obj['photo_vignette'] = "";
6019
                    }
6020
6021
                    $tabobj[$nbphoto - 1] = $obj;
6022
6023
                    // Do we have to continue with next photo ?
6024
                    if ($nbmax && $nbphoto >= $nbmax) {
6025
                        break;
6026
                    }
6027
                }
6028
            }
6029
6030
            closedir($handle);
6031
        }
6032
6033
        return $tabobj;
6034
    }
6035
6036
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6037
6038
    /**
6039
     *  Delete a photo and its thumbs
6040
     *
6041
     * @param string $file Path to image file
6042
     * @return void
6043
     */
6044
    public function delete_photo($file)
6045
    {
6046
        // phpcs:enable
6047
        include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
6048
        include_once DOL_DOCUMENT_ROOT . '/core/lib/images.lib.php';
6049
6050
        $dir = dirname($file) . '/'; // Chemin du dossier contenant l'image d'origine
6051
        $dirthumb = $dir . '/thumbs/'; // Chemin du dossier contenant la vignette
6052
        $filename = preg_replace('/' . preg_quote($dir, '/') . '/i', '', $file); // Nom du fichier
6053
6054
        // On efface l'image d'origine
6055
        dol_delete_file($file, 0, 0, 0, $this); // For triggers
6056
6057
        // Si elle existe, on efface la vignette
6058
        if (preg_match('/(' . $this->regeximgext . ')$/i', $filename, $regs)) {
6059
            $photo_vignette = preg_replace('/' . $regs[0] . '/i', '', $filename) . '_small' . $regs[0];
6060
            if (file_exists(dol_osencode($dirthumb . $photo_vignette))) {
6061
                dol_delete_file($dirthumb . $photo_vignette);
6062
            }
6063
6064
            $photo_vignette = preg_replace('/' . $regs[0] . '/i', '', $filename) . '_mini' . $regs[0];
6065
            if (file_exists(dol_osencode($dirthumb . $photo_vignette))) {
6066
                dol_delete_file($dirthumb . $photo_vignette);
6067
            }
6068
        }
6069
    }
6070
6071
    /**
6072
     *  Load size of image file
6073
     *
6074
     * @param string $file Path to file
6075
     * @return void
6076
     */
6077
    public function get_image_size($file)
6078
    {
6079
        // phpcs:enable
6080
        $file_osencoded = dol_osencode($file);
6081
        $infoImg = getimagesize($file_osencoded); // Get information on image
6082
        $this->imgWidth = $infoImg[0]; // Largeur de l'image
6083
        $this->imgHeight = $infoImg[1]; // Hauteur de l'image
6084
    }
6085
6086
    /**
6087
     *  Load indicators this->nb for the dashboard
6088
     *
6089
     * @return int                 Return integer <0 if KO, >0 if OK
6090
     */
6091
    public function loadStateBoard()
6092
    {
6093
        global $hookmanager;
6094
6095
        $this->nb = array();
6096
6097
        $sql = "SELECT count(p.rowid) as nb, fk_product_type";
6098
        $sql .= " FROM " . $this->db->prefix() . "product as p";
6099
        $sql .= ' WHERE p.entity IN (' . getEntity($this->element, 1) . ')';
6100
        // Add where from hooks
6101
        if (is_object($hookmanager)) {
6102
            $parameters = array();
6103
            $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
6104
            $sql .= $hookmanager->resPrint;
6105
        }
6106
        $sql .= ' GROUP BY fk_product_type';
6107
6108
        $resql = $this->db->query($sql);
6109
        if ($resql) {
6110
            while ($obj = $this->db->fetch_object($resql)) {
6111
                if ($obj->fk_product_type == 1) {
6112
                    $this->nb["services"] = $obj->nb;
6113
                } else {
6114
                    $this->nb["products"] = $obj->nb;
6115
                }
6116
            }
6117
            $this->db->free($resql);
6118
            return 1;
6119
        } else {
6120
            dol_print_error($this->db);
6121
            $this->error = $this->db->error();
6122
            return -1;
6123
        }
6124
    }
6125
6126
    /**
6127
     * Return if object need to have its stock managed
6128
     *
6129
     * @return boolean     True if it's a service
6130
     */
6131
    public function isStockManaged()
6132
    {
6133
        return ($this->isProduct() || getDolGlobalString('STOCK_SUPPORTS_SERVICES'));
6134
    }
6135
6136
    /**
6137
     * Return if  object have a constraint on mandatory_period
6138
     *
6139
     * @return boolean     True if mandatory_period set to 1
6140
     */
6141
    public function isMandatoryPeriod()
6142
    {
6143
        return ($this->mandatory_period == 1 ? true : false);
6144
    }
6145
6146
    /**
6147
     *  Initialise an instance with random values.
6148
     *  Used to build previews or test instances.
6149
     *    id must be 0 if object instance is a specimen.
6150
     *
6151
     * @return int
6152
     */
6153
    public function initAsSpecimen()
6154
    {
6155
        $now = dol_now();
6156
6157
        // Initialize parameters
6158
        $this->specimen = 1;
6159
        $this->id = 0;
6160
        $this->ref = 'PRODUCT_SPEC';
6161
        $this->label = 'PRODUCT SPECIMEN';
6162
        $this->description = 'This is description of this product specimen that was created the ' . dol_print_date($now, 'dayhourlog') . '.';
6163
        $this->specimen = 1;
6164
        $this->country_id = 1;
6165
        $this->status = 1;
6166
        $this->status_buy = 1;
6167
        $this->tobatch = 0;
6168
        $this->sell_or_eat_by_mandatory = 0;
6169
        $this->note_private = 'This is a comment (private)';
6170
        $this->note_public = 'This is a comment (public)';
6171
        $this->date_creation = $now;
6172
        $this->date_modification = $now;
6173
6174
        $this->weight = 4;
6175
        $this->weight_units = 3;
6176
6177
        $this->length = 5;
6178
        $this->length_units = 1;
6179
        $this->width = 6;
6180
        $this->width_units = 0;
6181
        $this->height = null;
6182
        $this->height_units = null;
6183
6184
        $this->surface = 30;
6185
        $this->surface_units = 0;
6186
        $this->volume = 300;
6187
        $this->volume_units = 0;
6188
6189
        $this->barcode = -1; // Create barcode automatically
6190
6191
        return 1;
6192
    }
6193
6194
    /**
6195
     *    Returns the text label from units dictionary
6196
     *
6197
     * @param string $type Label type (long or short)
6198
     * @return string|int Return integer <0 if ko, label if ok
6199
     */
6200
    public function getLabelOfUnit($type = 'long')
6201
    {
6202
        global $langs;
6203
6204
        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...
6205
            return '';
6206
        }
6207
6208
        $langs->load('products');
6209
        $label = '';
6210
        $label_type = 'label';
6211
        if ($type == 'short') {
6212
            $label_type = 'short_label';
6213
        }
6214
6215
        $sql = "SELECT " . $label_type . ", code from " . $this->db->prefix() . "c_units where rowid = " . ((int)$this->fk_unit);
6216
6217
        $resql = $this->db->query($sql);
6218
        if (!$resql) {
6219
            $this->error = $this->db->error();
6220
            dol_syslog(get_only_class($this) . "::getLabelOfUnit Error " . $this->error, LOG_ERR);
6221
            return -1;
6222
        } elseif ($this->db->num_rows($resql) > 0 && $res = $this->db->fetch_array($resql)) {
6223
            $label = ($label_type == 'short_label' ? $res[$label_type] : 'unit' . $res['code']);
6224
        }
6225
        $this->db->free($resql);
6226
6227
        return $label;
6228
    }
6229
6230
6231
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6232
6233
    /**
6234
     * Return minimum product recommended price
6235
     *
6236
     * @return int            Minimum recommended price that is higher price among all suppliers * PRODUCT_MINIMUM_RECOMMENDED_PRICE
6237
     */
6238
    public function min_recommended_price()
6239
    {
6240
        // phpcs:enable
6241
        global $conf;
6242
6243
        $maxpricesupplier = 0;
6244
6245
        if (getDolGlobalString('PRODUCT_MINIMUM_RECOMMENDED_PRICE')) {
6246
            include_once DOL_DOCUMENT_ROOT . '/fourn/class/fournisseur.product.class.php';
6247
            $product_fourn = new ProductFournisseur($this->db);
6248
            $product_fourn_list = $product_fourn->list_product_fournisseur_price($this->id, '', '');
6249
6250
            if (is_array($product_fourn_list) && count($product_fourn_list) > 0) {
6251
                foreach ($product_fourn_list as $productfourn) {
6252
                    if ($productfourn->fourn_unitprice > $maxpricesupplier) {
6253
                        $maxpricesupplier = $productfourn->fourn_unitprice;
6254
                    }
6255
                }
6256
6257
                $maxpricesupplier *= getDolGlobalString('PRODUCT_MINIMUM_RECOMMENDED_PRICE');
6258
            }
6259
        }
6260
6261
        return $maxpricesupplier;
6262
    }
6263
6264
    /**
6265
     * Sets object to supplied categories.
6266
     *
6267
     * Deletes object from existing categories not supplied.
6268
     * Adds it to non existing supplied categories.
6269
     * Existing categories are left untouch.
6270
     *
6271
     * @param int[]|int $categories Category or categories IDs
6272
     * @return int                          Return integer <0 if KO, >0 if OK
6273
     */
6274
    public function setCategories($categories)
6275
    {
6276
        return parent::setCategoriesCommon($categories, Categorie::TYPE_PRODUCT);
6277
    }
6278
6279
    /**
6280
     *  Load information for tab info
6281
     *
6282
     * @param int $id Id of thirdparty to load
6283
     * @return void
6284
     */
6285
    public function info($id)
6286
    {
6287
        $sql = "SELECT p.rowid, p.ref, p.datec as date_creation, p.tms as date_modification,";
6288
        $sql .= " p.fk_user_author, p.fk_user_modif";
6289
        $sql .= " FROM " . $this->db->prefix() . $this->table_element . " as p";
6290
        $sql .= " WHERE p.rowid = " . ((int)$id);
6291
6292
        $result = $this->db->query($sql);
6293
        if ($result) {
6294
            if ($this->db->num_rows($result)) {
6295
                $obj = $this->db->fetch_object($result);
6296
6297
                $this->id = $obj->rowid;
6298
                $this->ref = $obj->ref;
6299
6300
                $this->user_creation_id = $obj->fk_user_author;
6301
                $this->user_modification_id = $obj->fk_user_modif;
6302
6303
                $this->date_creation = $this->db->jdate($obj->date_creation);
6304
                $this->date_modification = $this->db->jdate($obj->date_modification);
6305
            }
6306
6307
            $this->db->free($result);
6308
        } else {
6309
            dol_print_error($this->db);
6310
        }
6311
    }
6312
6313
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6314
6315
    /**
6316
     * Return the duration of a service in hours (for a service based on duration fields)
6317
     *
6318
     * @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...
6319
     */
6320
    public function getProductDurationHours()
6321
    {
6322
        global $langs;
6323
6324
        if (empty($this->duration_value)) {
6325
            $this->errors[] = 'ErrorDurationForServiceNotDefinedCantCalculateHourlyPrice';
6326
            return -1;
6327
        }
6328
6329
        if ($this->duration_unit == 'i') {
6330
            $prodDurationHours = 1. / 60;
6331
        }
6332
        if ($this->duration_unit == 'h') {
6333
            $prodDurationHours = 1.;
6334
        }
6335
        if ($this->duration_unit == 'd') {
6336
            $prodDurationHours = 24.;
6337
        }
6338
        if ($this->duration_unit == 'w') {
6339
            $prodDurationHours = 24. * 7;
6340
        }
6341
        if ($this->duration_unit == 'm') {
6342
            $prodDurationHours = 24. * 30;
6343
        }
6344
        if ($this->duration_unit == 'y') {
6345
            $prodDurationHours = 24. * 365;
6346
        }
6347
        $prodDurationHours *= $this->duration_value;
6348
6349
        return $prodDurationHours;
6350
    }
6351
6352
    /**
6353
     *  Return clicable link of object (with eventually picto)
6354
     *
6355
     * @param string $option Where point the link (0=> main card, 1,2 => shipment, 'nolink'=>No link)
6356
     * @param array $arraydata Array of data
6357
     * @return     string                              HTML Code for Kanban thumb.
6358
     */
6359
    public function getKanbanView($option = '', $arraydata = null)
6360
    {
6361
        global $langs, $conf;
6362
6363
        $selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);
6364
6365
        $return = '<div class="box-flex-item box-flex-grow-zero">';
6366
        $return .= '<div class="info-box info-box-sm">';
6367
        $return .= '<div class="info-box-img">';
6368
        $label = '';
6369
        if ($this->is_photo_available($conf->product->multidir_output[$this->entity])) {
6370
            $label .= $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 120, 160, 0, 0, 0, '', 'photoref photokanban');
6371
            $return .= $label;
6372
        } else {
6373
            if ($this->type == Product::TYPE_PRODUCT) {
6374
                $label .= img_picto('', 'product');
6375
            } elseif ($this->type == Product::TYPE_SERVICE) {
6376
                $label .= img_picto('', 'service');
6377
            }
6378
            $return .= $label;
6379
        }
6380
        $return .= '</div>';
6381
        $return .= '<div class="info-box-content">';
6382
        $return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">' . (method_exists($this, 'getNomUrl') ? $this->getNomUrl() : $this->ref) . '</span>';
6383
        if ($selected >= 0) {
6384
            $return .= '<input id="cb' . $this->id . '" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="' . $this->id . '"' . ($selected ? ' checked="checked"' : '') . '>';
6385
        }
6386
        if (property_exists($this, 'label')) {
6387
            $return .= '<br><span class="info-box-label opacitymedium inline-block tdoverflowmax150 valignmiddle" title="' . dol_escape_htmltag($this->label) . '">' . dol_escape_htmltag($this->label) . '</span>';
6388
        }
6389
        if (property_exists($this, 'price') && property_exists($this, 'price_ttc')) {
6390
            if ($this->price_base_type == 'TTC') {
6391
                $return .= '<br><span class="info-box-status amount">' . price($this->price_ttc) . ' ' . $langs->trans("TTC") . '</span>';
6392
            } else {
6393
                if ($this->status) {
6394
                    $return .= '<br><span class="info-box-status amount">' . price($this->price) . ' ' . $langs->trans("HT") . '</span>';
6395
                }
6396
            }
6397
        }
6398
        $br = 1;
6399
        if (property_exists($this, 'stock_reel') && $this->isProduct()) {
6400
            $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>';
6401
            $br = 0;
6402
        }
6403
        if (method_exists($this, 'getLibStatut')) {
6404
            if ($br) {
6405
                $return .= '<br><div class="info-box-status inline-block valignmiddle">' . $this->getLibStatut(3, 1) . ' ' . $this->getLibStatut(3, 0) . '</div>';
6406
            } else {
6407
                $return .= '<div class="info-box-status inline-block valignmiddle marginleftonly paddingleft">' . $this->getLibStatut(3, 1) . ' ' . $this->getLibStatut(3, 0) . '</div>';
6408
            }
6409
        }
6410
        $return .= '</div>';
6411
        $return .= '</div>';
6412
        $return .= '</div>';
6413
        return $return;
6414
    }
6415
6416
    /**
6417
     *  Return if at least one photo is available
6418
     *
6419
     * @param string $sdir Directory to scan
6420
     * @return boolean                 True if at least one photo is available, False if not
6421
     */
6422
    public function is_photo_available($sdir)
6423
    {
6424
        // phpcs:enable
6425
        include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
6426
        include_once DOL_DOCUMENT_ROOT . '/core/lib/images.lib.php';
6427
6428
        global $conf;
6429
6430
        $dir = $sdir;
6431
        if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
6432
            $dir .= '/' . get_exdir($this->id, 2, 0, 0, $this, 'product') . $this->id . "/photos/";
6433
        } else {
6434
            $dir .= '/' . get_exdir(0, 0, 0, 0, $this, 'product');
6435
        }
6436
6437
        $nbphoto = 0;
6438
6439
        $dir_osencoded = dol_osencode($dir);
6440
        if (file_exists($dir_osencoded)) {
6441
            $handle = opendir($dir_osencoded);
6442
            if (is_resource($handle)) {
6443
                while (($file = readdir($handle)) !== false) {
6444
                    if (!utf8_check($file)) {
6445
                        $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // To be sure data is stored in UTF8 in memory
6446
                    }
6447
                    if (dol_is_file($dir . $file) && image_format_supported($file) >= 0) {
6448
                        return true;
6449
                    }
6450
                }
6451
            }
6452
        }
6453
        return false;
6454
    }
6455
6456
    /**
6457
     *    Return clickable link of object (with eventually picto)
6458
     *
6459
     * @param int $withpicto Add picto into link
6460
     * @param string $option Where point the link ('stock', 'composition', 'category', 'supplier', '')
6461
     * @param int $maxlength Maxlength of ref
6462
     * @param int $save_lastsearch_value -1=Auto, 0=No save of lastsearch_values when clicking, 1=Save lastsearch_values when clicking
6463
     * @param int $notooltip No tooltip
6464
     * @param string $morecss ''=Add more css on link
6465
     * @param int $add_label 0=Default, 1=Add label into string, >1=Add first chars into string
6466
     * @param string $sep ' - '=Separator between ref and label if option 'add_label' is set
6467
     * @return  string                          String with URL
6468
     */
6469
    public function getNomUrl($withpicto = 0, $option = '', $maxlength = 0, $save_lastsearch_value = -1, $notooltip = 0, $morecss = '', $add_label = 0, $sep = ' - ')
6470
    {
6471
        global $conf, $langs, $hookmanager, $user;
6472
        include_once DOL_DOCUMENT_ROOT . '/core/lib/product.lib.php';
6473
6474
        $result = '';
6475
6476
        $newref = $this->ref;
6477
        if ($maxlength) {
6478
            $newref = dol_trunc($newref, $maxlength, 'middle');
6479
        }
6480
        $params = [
6481
            'id' => $this->id,
6482
            'objecttype' => (isset($this->type) ? ($this->type == 1 ? 'service' : 'product') : $this->element),
6483
            'option' => $option,
6484
            'nofetch' => 1,
6485
        ];
6486
        $classfortooltip = 'classfortooltip';
6487
        $dataparams = '';
6488
        if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
6489
            $classfortooltip = 'classforajaxtooltip';
6490
            $dataparams = ' data-params="' . dol_escape_htmltag(json_encode($params)) . '"';
6491
            $label = '';
6492
        } else {
6493
            $label = implode($this->getTooltipContentArray($params));
6494
        }
6495
6496
        $linkclose = '';
6497
        if (empty($notooltip)) {
6498
            if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
6499
                $label = $langs->trans("ShowProduct");
6500
                $linkclose .= ' alt="' . dol_escape_htmltag($label, 1, 1) . '"';
6501
            }
6502
            $linkclose .= ($label ? ' title="' . dol_escape_htmltag($label, 1, 1) . '"' : ' title="tocomplete"');
6503
            $linkclose .= $dataparams . ' class="nowraponall ' . $classfortooltip . ($morecss ? ' ' . $morecss : '') . '"';
6504
        } else {
6505
            $linkclose = ' class="nowraponall' . ($morecss ? ' ' . $morecss : '') . '"';
6506
        }
6507
6508
        if ($option == 'supplier' || $option == 'category') {
6509
            $url = constant('BASE_URL') . '/product/price_suppliers.php?id=' . $this->id;
6510
        } elseif ($option == 'stock') {
6511
            $url = constant('BASE_URL') . '/product/stock/product.php?id=' . $this->id;
6512
        } elseif ($option == 'composition') {
6513
            $url = constant('BASE_URL') . '/product/composition/card.php?id=' . $this->id;
6514
        } else {
6515
            $url = constant('BASE_URL') . '/product/card.php?id=' . $this->id;
6516
        }
6517
6518
        if ($option !== 'nolink') {
6519
            // Add param to save lastsearch_values or not
6520
            $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
6521
            if ($save_lastsearch_value == -1 && isset($_SERVER["PHP_SELF"]) && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
6522
                $add_save_lastsearch_values = 1;
6523
            }
6524
            if ($add_save_lastsearch_values) {
6525
                $url .= '&save_lastsearch_values=1';
6526
            }
6527
        }
6528
6529
        $linkstart = '<a href="' . $url . '"';
6530
        $linkstart .= $linkclose . '>';
6531
        $linkend = '</a>';
6532
6533
        $result .= $linkstart;
6534
        if ($withpicto) {
6535
            if ($this->type == Product::TYPE_PRODUCT) {
6536
                $result .= (img_object(($notooltip ? '' : $label), 'product', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
6537
            }
6538
            if ($this->type == Product::TYPE_SERVICE) {
6539
                $result .= (img_object(($notooltip ? '' : $label), 'service', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
6540
            }
6541
        }
6542
        $result .= '<span class="aaa">' . dol_escape_htmltag($newref) . '</span>';
6543
        $result .= $linkend;
6544
        if ($withpicto != 2) {
6545
            $result .= (($add_label && $this->label) ? $sep . dol_trunc($this->label, ($add_label > 1 ? $add_label : 0)) : '');
6546
        }
6547
6548
        global $action;
6549
        $hookmanager->initHooks(array('productdao'));
6550
        $parameters = array('id' => $this->id, 'getnomurl' => &$result, 'label' => &$label);
6551
        $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
6552
        if ($reshook > 0) {
6553
            $result = $hookmanager->resPrint;
6554
        } else {
6555
            $result .= $hookmanager->resPrint;
6556
        }
6557
6558
        return $result;
6559
    }
6560
6561
    /**
6562
     * getTooltipContentArray
6563
     * @param array $params params to construct tooltip data
6564
     * @return array
6565
     * @since v18
6566
     */
6567
    public function getTooltipContentArray($params)
6568
    {
6569
        global $conf, $langs, $user;
6570
6571
        $langs->loadLangs(array('products', 'other'));
6572
6573
        $datas = array();
6574
        $nofetch = !empty($params['nofetch']);
6575
6576
        if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
6577
            return ['optimize' => $langs->trans("ShowProduct")];
6578
        }
6579
6580
        if (!empty($this->entity)) {
6581
            $tmpphoto = $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 80, 0, 0, 0, 0, 1);
6582
            if ($this->nbphoto > 0) {
6583
                $datas['photo'] = '<div class="photointooltip floatright">' . "\n" . $tmpphoto . '</div>';
6584
            }
6585
        }
6586
6587
        if ($this->type == Product::TYPE_PRODUCT) {
6588
            $datas['picto'] = img_picto('', 'product') . ' <u class="paddingrightonly">' . $langs->trans("Product") . '</u>';
6589
        } elseif ($this->type == Product::TYPE_SERVICE) {
6590
            $datas['picto'] = img_picto('', 'service') . ' <u class="paddingrightonly">' . $langs->trans("Service") . '</u>';
6591
        }
6592
        if (isset($this->status) && isset($this->status_buy)) {
6593
            $datas['status'] = ' ' . $this->getLibStatut(5, 0) . ' ' . $this->getLibStatut(5, 1);
6594
        }
6595
6596
        if (!empty($this->ref)) {
6597
            $datas['ref'] = '<br><b>' . $langs->trans('ProductRef') . ':</b> ' . $this->ref;
6598
        }
6599
        if (!empty($this->label)) {
6600
            $datas['label'] = '<br><b>' . $langs->trans('ProductLabel') . ':</b> ' . $this->label;
6601
        }
6602
        if (!empty($this->description)) {
6603
            $datas['description'] = '<br><b>' . $langs->trans('ProductDescription') . ':</b> ' . dolGetFirstLineOfText($this->description, 5);
6604
        }
6605
        if ($this->type == Product::TYPE_PRODUCT || getDolGlobalString('STOCK_SUPPORTS_SERVICES')) {
6606
            if (isModEnabled('productbatch')) {
6607
                $langs->load("productbatch");
6608
                $datas['batchstatus'] = "<br><b>" . $langs->trans("ManageLotSerial") . '</b>: ' . $this->getLibStatut(0, 2);
6609
            }
6610
        }
6611
        if (isModEnabled('barcode')) {
6612
            $datas['barcode'] = '<br><b>' . $langs->trans('BarCode') . ':</b> ' . $this->barcode;
6613
        }
6614
6615
        if ($this->type == Product::TYPE_PRODUCT) {
6616
            if ($this->weight) {
6617
                $datas['weight'] = "<br><b>" . $langs->trans("Weight") . '</b>: ' . $this->weight . ' ' . measuringUnitString(0, "weight", $this->weight_units);
6618
            }
6619
            $labelsize = "";
6620
            if ($this->length) {
6621
                $labelsize .= ($labelsize ? " - " : "") . "<b>" . $langs->trans("Length") . '</b>: ' . $this->length . ' ' . measuringUnitString(0, 'size', $this->length_units);
6622
            }
6623
            if ($this->width) {
6624
                $labelsize .= ($labelsize ? " - " : "") . "<b>" . $langs->trans("Width") . '</b>: ' . $this->width . ' ' . measuringUnitString(0, 'size', $this->width_units);
6625
            }
6626
            if ($this->height) {
6627
                $labelsize .= ($labelsize ? " - " : "") . "<b>" . $langs->trans("Height") . '</b>: ' . $this->height . ' ' . measuringUnitString(0, 'size', $this->height_units);
6628
            }
6629
            if ($labelsize) {
6630
                $datas['size'] = "<br>" . $labelsize;
6631
            }
6632
6633
            $labelsurfacevolume = "";
6634
            if ($this->surface) {
6635
                $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "") . "<b>" . $langs->trans("Surface") . '</b>: ' . $this->surface . ' ' . measuringUnitString(0, 'surface', $this->surface_units);
6636
            }
6637
            if ($this->volume) {
6638
                $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "") . "<b>" . $langs->trans("Volume") . '</b>: ' . $this->volume . ' ' . measuringUnitString(0, 'volume', $this->volume_units);
6639
            }
6640
            if ($labelsurfacevolume) {
6641
                $datas['surface'] = "<br>" . $labelsurfacevolume;
6642
            }
6643
        }
6644
        if ($this->type == Product::TYPE_SERVICE && !empty($this->duration_value)) {
6645
            // Duration
6646
            $datas['duration'] = '<br><b>' . $langs->trans("Duration") . ':</b> ' . $this->duration_value;
6647
            if ($this->duration_value > 1) {
6648
                $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"));
6649
            } elseif ($this->duration_value > 0) {
6650
                $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"));
6651
            }
6652
            $datas['duration'] .= (!empty($this->duration_unit) && isset($dur[$this->duration_unit]) ? "&nbsp;" . $langs->trans($dur[$this->duration_unit]) : '');
6653
        }
6654
        if (empty($user->socid)) {
6655
            if (!empty($this->pmp) && $this->pmp) {
6656
                $datas['pmp'] = "<br><b>" . $langs->trans("PMPValue") . '</b>: ' . price($this->pmp, 0, '', 1, -1, -1, $conf->currency);
6657
            }
6658
6659
            if (isModEnabled('accounting')) {
6660
                if ($this->status && isset($this->accountancy_code_sell)) {
6661
                    include_once DOL_DOCUMENT_ROOT . '/core/lib/accounting.lib.php';
6662
                    $selllabel = '<br>';
6663
                    $selllabel .= '<br><b>' . $langs->trans('ProductAccountancySellCode') . ':</b> ' . length_accountg($this->accountancy_code_sell);
6664
                    $selllabel .= '<br><b>' . $langs->trans('ProductAccountancySellIntraCode') . ':</b> ' . length_accountg($this->accountancy_code_sell_intra);
6665
                    $selllabel .= '<br><b>' . $langs->trans('ProductAccountancySellExportCode') . ':</b> ' . length_accountg($this->accountancy_code_sell_export);
6666
                    $datas['accountancysell'] = $selllabel;
6667
                }
6668
                if ($this->status_buy && isset($this->accountancy_code_buy)) {
6669
                    include_once DOL_DOCUMENT_ROOT . '/core/lib/accounting.lib.php';
6670
                    $buylabel = '';
6671
                    if (empty($this->status)) {
6672
                        $buylabel .= '<br>';
6673
                    }
6674
                    $buylabel .= '<br><b>' . $langs->trans('ProductAccountancyBuyCode') . ':</b> ' . length_accountg($this->accountancy_code_buy);
6675
                    $buylabel .= '<br><b>' . $langs->trans('ProductAccountancyBuyIntraCode') . ':</b> ' . length_accountg($this->accountancy_code_buy_intra);
6676
                    $buylabel .= '<br><b>' . $langs->trans('ProductAccountancyBuyExportCode') . ':</b> ' . length_accountg($this->accountancy_code_buy_export);
6677
                    $datas['accountancybuy'] = $buylabel;
6678
                }
6679
            }
6680
        }
6681
        // show categories for this record only in ajax to not overload lists
6682
        if (isModEnabled('category') && !$nofetch) {
6683
            $form = new Form($this->db);
6684
            $datas['categories'] = '<br>' . $form->showCategories($this->id, Categorie::TYPE_PRODUCT, 1);
6685
        }
6686
6687
        return $datas;
6688
    }
6689
6690
    /**
6691
     *    Return label of status of object
6692
     *
6693
     * @param int $mode 0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=Short label + Picto
6694
     * @param int $type 0=Sell, 1=Buy, 2=Batch Number management
6695
     * @return string          Label of status
6696
     */
6697
    public function getLibStatut($mode = 0, $type = 0)
6698
    {
6699
        switch ($type) {
6700
            case 0:
6701
                return $this->LibStatut($this->status, $mode, $type);
6702
            case 1:
6703
                return $this->LibStatut($this->status_buy, $mode, $type);
6704
            case 2:
6705
                return $this->LibStatut($this->status_batch, $mode, $type);
6706
            default:
6707
                //Simulate previous behavior but should return an error string
6708
                return $this->LibStatut($this->status_buy, $mode, $type);
6709
        }
6710
    }
6711
6712
    /**
6713
     *    Return label of a given status
6714
     *
6715
     * @param int $status Statut
6716
     * @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
6717
     * @param int $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
6718
     * @return string               Label of status
6719
     */
6720
    public function LibStatut($status, $mode = 0, $type = 0)
6721
    {
6722
        // phpcs:enable
6723
        global $conf, $langs;
6724
6725
        $labelStatus = $labelStatusShort = '';
6726
6727
        $langs->load('products');
6728
        if (isModEnabled('productbatch')) {
6729
            $langs->load("productbatch");
6730
        }
6731
6732
        if ($type == 2) {
6733
            switch ($mode) {
6734
                case 0:
6735
                    $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatch') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial')));
6736
                    return dolGetStatus($label);
6737
                case 1:
6738
                    $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort')));
6739
                    return dolGetStatus($label);
6740
                case 2:
6741
                    return $this->LibStatut($status, 3, 2) . ' ' . $this->LibStatut($status, 1, 2);
6742
                case 3:
6743
                    return dolGetStatus($langs->transnoentitiesnoconv('ProductStatusNotOnBatch'), '', '', empty($status) ? 'status5' : 'status4', 3, 'dot');
6744
                case 4:
6745
                    return $this->LibStatut($status, 3, 2) . ' ' . $this->LibStatut($status, 0, 2);
6746
                case 5:
6747
                    return $this->LibStatut($status, 1, 2) . ' ' . $this->LibStatut($status, 3, 2);
6748
                default:
6749
                    return dolGetStatus($langs->transnoentitiesnoconv('Unknown'));
6750
            }
6751
        }
6752
6753
        $statuttrans = empty($status) ? 'status5' : 'status4';
6754
6755
        if ($status == 0) {
6756
            // $type   0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
6757
            if ($type == 0) {
6758
                $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnSellShort');
6759
                $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnSell');
6760
            } elseif ($type == 1) {
6761
                $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBuyShort');
6762
                $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBuy');
6763
            } elseif ($type == 2) {
6764
                $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBatch');
6765
                $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort');
6766
            }
6767
        } elseif ($status == 1) {
6768
            // $type   0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
6769
            if ($type == 0) {
6770
                $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSellShort');
6771
                $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSell');
6772
            } elseif ($type == 1) {
6773
                $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnBuyShort');
6774
                $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnBuy');
6775
            } elseif ($type == 2) {
6776
                $labelStatus = ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial'));
6777
                $labelStatusShort = ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort'));
6778
            }
6779
        } elseif ($type == 2 && $status == 2) {
6780
            $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSerial');
6781
            $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSerialShort');
6782
        }
6783
6784
        if ($mode > 6) {
6785
            return dolGetStatus($langs->transnoentitiesnoconv('Unknown'), '', '', 'status0', 0);
6786
        } else {
6787
            return dolGetStatus($labelStatus, $labelStatusShort, '', $statuttrans, $mode);
6788
        }
6789
    }
6790
6791
    /**
6792
     * Returns the rights used for this class
6793
     *
6794
     * @return Object
6795
     */
6796
    public function getRights()
6797
    {
6798
        global $user;
6799
6800
        if ($this->isProduct()) {
6801
            return $user->rights->produit;
6802
        } else {
6803
            return $user->rights->service;
6804
        }
6805
    }
6806
}
6807