Passed
Pull Request — dev (#6)
by Rafael
79:24 queued 24:08
created

Product   F

Complexity

Total Complexity 1118

Size/Duplication

Total Lines 6723
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3626
dl 0
loc 6723
rs 0.8
c 1
b 0
f 0
wmc 1118

93 Methods

Rating   Name   Duplication   Size   Complexity  
B liste_photos() 0 51 11
A info() 0 25 3
A isProduct() 0 3 2
B getLabelOfUnit() 0 28 7
A isStockManaged() 0 3 2
A delete_photo() 0 23 4
A initAsSpecimen() 0 39 1
A isMandatoryPeriod() 0 3 2
A get_image_size() 0 7 1
A loadBatchInfo() 0 22 3
A setCategories() 0 3 1
C generateMultiprices() 0 50 12
A hasbatch() 0 3 2
B is_photo_available() 0 32 8
A replaceThirdparty() 0 8 1
A isService() 0 3 2
A getRights() 0 8 2
A get_barcode() 0 23 4
A min_recommended_price() 0 24 6
F getKanbanView() 0 55 17
A loadStateBoard() 0 32 5
A add_photo() 0 36 6
B getProductDurationHours() 0 30 8
B get_nb_propal() 0 39 8
A getSellOrEatByMandatoryList() 0 11 1
D getSellPrice() 0 107 26
B get_nb_order() 0 39 8
F fetch() 0 412 72
C load_stats_facturerec() 0 64 12
B getChildsArbo() 0 64 11
F load_stats_sending() 0 73 18
B load_stats_mo() 0 51 10
A check_barcode() 0 23 5
A getMultiLangs() 0 27 4
A hasFatherOrChild() 0 25 6
C load_stock() 0 65 13
A list_suppliers() 0 24 3
B correct_stock_batch() 0 40 6
F fetch_prod_arbo() 0 50 15
F _get_stats() 0 66 17
B clone_price() 0 78 2
F get_buyprice() 0 152 22
D setMultiLangs() 0 100 18
A delMultiLangs() 0 21 3
C load_stats_propale() 0 64 12
F load_stats_inproduction() 0 113 35
A setPriceExpression() 0 7 1
A check() 0 21 5
A getLibStatut() 0 12 4
F delete() 0 151 36
A generateDocument() 0 15 2
A getLibFinished() 0 20 6
A log_price_delete() 0 15 2
B add_sousproduit() 0 58 10
F create() 0 308 61
B del_sousproduit() 0 53 9
C load_stats_reception() 0 48 13
F updatePrice() 0 187 38
B get_nb_vente() 0 40 8
A clone_fournisseurs() 0 35 2
C load_stats_facture() 0 64 12
F update() 0 408 134
B get_nb_propalsupplier() 0 40 8
C load_stats_contrat() 0 64 12
A hasVariants() 0 15 3
B get_nb_contract() 0 41 8
A getSellOrEatByMandatoryLabel() 0 10 2
F LibStatut() 0 68 28
F getTooltipContentArray() 0 121 46
B load_stats_proposal_supplier() 0 43 7
B setAccountancyCode() 0 52 10
B load_stats_facture_fournisseur() 0 43 7
B correct_stock() 0 40 6
A get_arbo_each_prod() 0 13 5
A is_sousproduit() 0 25 3
A get_sousproduits_arbo() 0 10 3
B update_sousproduit() 0 40 8
A __construct() 0 8 1
B _log_price() 0 27 7
B add_fournisseur() 0 86 8
F getNomUrl() 0 90 32
B verify() 0 39 10
A isVariant() 0 19 4
A getArrayForPriceCompare() 0 26 5
B get_nb_achat() 0 40 8
F load_virtual_stock() 0 125 40
C load_stats_commande_fournisseur() 0 48 13
B get_nb_ordersupplier() 0 39 8
A getFather() 0 28 3
A clone_associations() 0 17 2
F load_stats_commande() 0 114 24
B load_stats_bom() 0 60 9
B get_nb_mos() 0 40 8

How to fix   Complexity   

Complex Class

Complex classes like Product often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Product, and based on these observations, apply Extract Interface, too.

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\User\Classes\User;
40
use Dolibarr\Core\Base\CommonObject;
41
use DoliDB;
42
43
/**
44
 *    \file       htdocs/product/class/product.class.php
45
 *    \ingroup    produit
46
 *    \brief      File of class to manage the predefined products or services
47
 */
48
49
require_once constant('DOL_DOCUMENT_ROOT') . '/core/lib/product.lib.php';
50
51
/**
52
 * Class to manage products or services
53
 */
54
class Product extends CommonObject
55
{
56
    /**
57
     * Const sell or eat by mandatory id
58
     */
59
    const SELL_OR_EAT_BY_MANDATORY_ID_NONE = 0;
60
    const SELL_OR_EAT_BY_MANDATORY_ID_SELL_BY = 1;
61
    const SELL_OR_EAT_BY_MANDATORY_ID_EAT_BY = 2;
62
    const SELL_OR_EAT_BY_MANDATORY_ID_SELL_AND_EAT = 3;
63
64
    /**
65
     * @var string ID to identify managed object
66
     */
67
    public $element = 'product';
68
69
    /**
70
     * @var string Name of table without prefix where object is stored
71
     */
72
    public $table_element = 'product';
73
74
    /**
75
     * @var string Field with ID of parent key if this field has a parent
76
     */
77
    public $fk_element = 'fk_product';
78
79
    /**
80
     * @var static
81
     */
82
    public $oldcopy;
83
84
    /**
85
     * @var array<string, array<string>>    List of child tables. To test if we can delete object.
86
     */
87
    protected $childtables = array(
88
        'supplier_proposaldet' => array('name' => 'SupplierProposal', 'parent' => 'supplier_proposal', 'parentkey' => 'fk_supplier_proposal'),
89
        'propaldet' => array('name' => 'Proposal', 'parent' => 'propal', 'parentkey' => 'fk_propal'),
90
        'commandedet' => array('name' => 'Order', 'parent' => 'commande', 'parentkey' => 'fk_commande'),
91
        'facturedet' => array('name' => 'Invoice', 'parent' => 'facture', 'parentkey' => 'fk_facture'),
92
        'contratdet' => array('name' => 'Contract', 'parent' => 'contrat', 'parentkey' => 'fk_contrat'),
93
        'facture_fourn_det' => array('name' => 'SupplierInvoice', 'parent' => 'facture_fourn', 'parentkey' => 'fk_facture_fourn'),
94
        'commande_fournisseurdet' => array('name' => 'SupplierOrder', 'parent' => 'commande_fournisseur', 'parentkey' => 'fk_commande'),
95
        'mrp_production' => array('name' => 'Mo', 'parent' => 'mrp_mo', 'parentkey' => 'fk_mo')
96
    );
97
98
    /**
99
     * @var string picto
100
     */
101
    public $picto = 'product';
102
103
    /**
104
     * {@inheritdoc}
105
     */
106
    protected $table_ref_field = 'ref';
107
108
    public $regeximgext = '\.gif|\.jpg|\.jpeg|\.png|\.bmp|\.webp|\.xpm|\.xbm'; // See also into images.lib.php
109
110
    /**
111
     * @deprecated  Use $label instead
112
     * @see $label
113
     */
114
    public $libelle;
115
116
    /**
117
     * Product label
118
     *
119
     * @var string
120
     */
121
    public $label;
122
123
    /**
124
     * Product description
125
     *
126
     * @var string
127
     */
128
    public $description;
129
130
    /**
131
     * Product other fields PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION
132
     *
133
     * @var string
134
     */
135
    public $other;
136
137
    /**
138
     * Check TYPE constants
139
     *
140
     * @var int
141
     */
142
    public $type = self::TYPE_PRODUCT;
143
144
    /**
145
     * Selling price without tax
146
     *
147
     * @var float
148
     */
149
    public $price;
150
151
    public $price_formated;         // used by takepos/ajax/ajax.php
152
153
    /**
154
     * Selling price with tax
155
     *
156
     * @var float
157
     */
158
    public $price_ttc;
159
160
    public $price_ttc_formated;     // used by takepos/ajax/ajax.php
161
162
    /**
163
     * Minimum price net
164
     *
165
     * @var float
166
     */
167
    public $price_min;
168
169
    /**
170
     * Minimum price with tax
171
     *
172
     * @var float
173
     */
174
    public $price_min_ttc;
175
176
    /**
177
     * Base price ('TTC' for price including tax or 'HT' for net price)
178
     * @var string
179
     */
180
    public $price_base_type;
181
    public $price_label;
182
183
    //! Arrays for multiprices
184
    public $multiprices = array();
185
    public $multiprices_ttc = array();
186
    public $multiprices_base_type = array();
187
    public $multiprices_default_vat_code = array();
188
    public $multiprices_min = array();
189
    public $multiprices_min_ttc = array();
190
    public $multiprices_tva_tx = array();
191
    public $multiprices_recuperableonly = array();
192
193
    //! Price by quantity arrays
194
    public $price_by_qty;
195
    public $prices_by_qty = array();
196
    public $prices_by_qty_id = array();
197
    public $prices_by_qty_list = array();
198
199
    /**
200
     * @var int price level set after updateprice for trigger
201
     */
202
    public $level;
203
204
    //! Array for multilangs
205
    public $multilangs = array();
206
207
    //! Default VAT code for product (link to code into llx_c_tva but without foreign keys)
208
    public $default_vat_code;
209
210
    //! Default VAT rate of product
211
    public $tva_tx;
212
213
    /**
214
     * int  French VAT NPR is used (0 or 1)
215
     */
216
    public $tva_npr = 0;
217
218
    //! Default discount percent
219
    public $remise_percent;
220
221
    //! Other local taxes
222
    public $localtax1_tx;
223
    public $localtax2_tx;
224
    public $localtax1_type;
225
    public $localtax2_type;
226
227
    // Properties set by get_buyprice() for return
228
229
    public $desc_supplier;
230
    public $vatrate_supplier;
231
    public $default_vat_code_supplier;
232
    public $fourn_multicurrency_price;
233
    public $fourn_multicurrency_unitprice;
234
    public $fourn_multicurrency_tx;
235
    public $fourn_multicurrency_id;
236
    public $fourn_multicurrency_code;
237
    public $packaging;
238
239
240
    /**
241
     * Lifetime (in seconds)
242
     *
243
     * @var int|null
244
     * @see ProductLot
245
     */
246
    public $lifetime;
247
248
    /**
249
     * Quality control frequency (in days ?)
250
     *
251
     * @var int|null
252
     * @see ProductLot
253
     */
254
    public $qc_frequency;
255
256
    /**
257
     * Stock real (denormalized data)
258
     *
259
     * @var int
260
     */
261
    public $stock_reel = 0;
262
263
    /**
264
     * Stock virtual
265
     *
266
     * @var int
267
     */
268
    public $stock_theorique;
269
270
    /**
271
     * Cost price
272
     *
273
     * @var float
274
     */
275
    public $cost_price;
276
277
    //! Average price value for product entry into stock (PMP)
278
    public $pmp;
279
280
    /**
281
     * Stock alert
282
     *
283
     * @var float
284
     */
285
    public $seuil_stock_alerte = 0;
286
287
    /**
288
     * Ask for replenishment when $desiredstock < $stock_reel
289
     */
290
    public $desiredstock = 0;
291
292
    /**
293
     * Service expiration
294
     */
295
    public $duration_value;
296
    /**
297
     * Service expiration unit
298
     */
299
    public $duration_unit;
300
    /**
301
     * Service expiration label (value + unit)
302
     */
303
    public $duration;
304
305
    /**
306
     * @var int Service Workstation
307
     */
308
    public $fk_default_workstation;
309
310
    /**
311
     * Status indicates whether the product is on sale '1' or not '0'
312
     *
313
     * @var int
314
     */
315
    public $status = 0;
316
317
    /**
318
     * Status indicates whether the product is on sale '1' or not '0'
319
     * @var int
320
     * @deprecated  Use $status instead
321
     * @see $status
322
     */
323
    public $tosell;
324
325
    /**
326
     * Status indicate whether the product is available for purchase '1' or not '0'
327
     *
328
     * @var int
329
     */
330
    public $status_buy = 0;
331
332
    /**
333
     * Status indicate whether the product is available for purchase '1' or not '0'
334
     * @var int
335
     * @deprecated Use $status_buy instead
336
     * @see $status_buy
337
     */
338
    public $tobuy;
339
340
    /**
341
     * Status indicates whether the product is a finished product '1' or a raw material '0'
342
     *
343
     * @var ?int
344
     */
345
    public $finished;
346
347
    /**
348
     * fk_default_bom indicates the default bom
349
     *
350
     * @var int
351
     */
352
    public $fk_default_bom;
353
354
    /**
355
     * product_fourn_price_id indicates the fourn price id
356
     *
357
     * @var int
358
     */
359
    public $product_fourn_price_id;
360
361
    /**
362
     * buyprice indicates buy price off the product
363
     *
364
     * @var float
365
     */
366
    public $buyprice;
367
368
    /**
369
     * for backward compatibility
370
     *
371
     * @var int
372
     */
373
    public $tobatch;
374
375
376
    /**
377
     * We must manage lot/batch number, sell-by date and so on : '0':no, '1':yes, '2": yes with unique serial number
378
     *
379
     * @var int
380
     */
381
    public $status_batch = 0;
382
383
    /**
384
     * Make sell-by or eat-by date mandatory
385
     *
386
     * @var int
387
     */
388
    public $sell_or_eat_by_mandatory = 0;
389
390
    /**
391
     * If allowed, we can edit batch or serial number mask for each product
392
     *
393
     * @var string
394
     */
395
    public $batch_mask = '';
396
397
    /**
398
     * Customs code
399
     *
400
     * @var string
401
     */
402
    public $customcode;
403
404
    /**
405
     * Product URL
406
     *
407
     * @var string
408
     */
409
    public $url;
410
411
    //! Metric of products
412
    public $weight;
413
    public $weight_units;   // scale -3, 0, 3, 6
414
    public $length;
415
    public $length_units;   // scale -3, 0, 3, 6
416
    public $width;
417
    public $width_units;    // scale -3, 0, 3, 6
418
    public $height;
419
    public $height_units;   // scale -3, 0, 3, 6
420
    public $surface;
421
    public $surface_units;  // scale -3, 0, 3, 6
422
    public $volume;
423
    public $volume_units;   // scale -3, 0, 3, 6
424
425
    public $net_measure;
426
    public $net_measure_units;  // scale -3, 0, 3, 6
427
428
    public $accountancy_code_sell;
429
    public $accountancy_code_sell_intra;
430
    public $accountancy_code_sell_export;
431
    public $accountancy_code_buy;
432
    public $accountancy_code_buy_intra;
433
    public $accountancy_code_buy_export;
434
435
    /**
436
     * @var string|int  Main Barcode value, -1 or 'auto' for auto code
437
     */
438
    public $barcode;
439
440
    /**
441
     * @var int     Main Barcode type ID
442
     */
443
    public $barcode_type;
444
445
    /**
446
     * @var string  Main Barcode type code
447
     */
448
    public $barcode_type_code;
449
450
    public $stats_propale = array();
451
    public $stats_commande = array();
452
    public $stats_contrat = array();
453
    public $stats_facture = array();
454
    public $stats_proposal_supplier = array();
455
    public $stats_commande_fournisseur = array();
456
    public $stats_expedition = array();
457
    public $stats_reception = array();
458
    public $stats_mo = array();
459
    public $stats_bom = array();
460
    public $stats_mrptoconsume = array();
461
    public $stats_mrptoproduce = array();
462
    public $stats_facturerec = array();
463
    public $stats_facture_fournisseur = array();
464
465
    //! Size of image
466
    public $imgWidth;
467
    public $imgHeight;
468
469
    /**
470
     * @var integer|string date_creation
471
     */
472
    public $date_creation;
473
474
    /**
475
     * @var integer|string date_modification
476
     */
477
    public $date_modification;
478
479
    //! Id du fournisseur
480
    public $product_fourn_id;
481
482
    //! Product ID already linked to a reference supplier
483
    public $product_id_already_linked;
484
485
    public $nbphoto = 0;
486
487
    //! Contains detail of stock of product into each warehouse
488
    public $stock_warehouse = array();
489
490
    /**
491
     * @var int Default warehouse Id
492
     */
493
    public $fk_default_warehouse;
494
    /**
495
     * @var int ID
496
     */
497
    public $fk_price_expression;
498
499
    /* To store supplier price found */
500
    public $fourn_qty;
501
    public $fourn_pu;
502
    public $fourn_price_base_type;
503
504
    /**
505
     * @var int ID
506
     */
507
    public $fourn_socid;
508
509
    /**
510
     * @deprecated
511
     * @see        $ref_supplier
512
     */
513
    public $ref_fourn;
514
515
    /**
516
     * @var string ref supplier
517
     */
518
    public $ref_supplier;
519
520
    /**
521
     * @var int|null                ID of the unit of measurement (rowid in llx_c_units table)
522
     * @see measuringUnitString()
523
     * @see getLabelOfUnit()
524
     */
525
    public $fk_unit;
526
527
    /**
528
     * Price is generated using multiprice rules
529
     *
530
     * @var int
531
     */
532
    public $price_autogen = 0;
533
534
    /**
535
     * Array with list of supplier prices of product
536
     *
537
     * @var array
538
     */
539
    public $supplierprices;
540
541
    /**
542
     * Array with list of sub-products for Kits
543
     *
544
     * @var array
545
     */
546
    public $sousprods;
547
548
    /**
549
     * @var array Path of subproducts. Build from ->sousprods with get_arbo_each_prod()
550
     */
551
    public $res;
552
553
554
    /**
555
     * Property set to save result of isObjectUsed(). Used for example by Product API.
556
     *
557
     * @var boolean
558
     */
559
    public $is_object_used;
560
561
    /**
562
     * If this Product is within a kit:
563
     * Quantity of this Product within this kit
564
     *
565
     * @var float
566
     * @see Product::is_sousproduit()       To set this property
567
     * @see Product::add_sousproduit()
568
     * @see Product::update_sousproduit()
569
     */
570
    public $is_sousproduit_qty;
571
572
    /**
573
     * If this Product is within a kit:
574
     * 1 = modify the stock of this child Product upon modification of the stock of its parent Product
575
     * ("incdec" stands for increase/decrease)
576
     *
577
     * @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...
578
     * @see Product::is_sousproduit()       To set this property
579
     * @see Product::add_sousproduit()
580
     * @see Product::update_sousproduit()
581
     */
582
    public $is_sousproduit_incdec;
583
584
    public $mandatory_period;
585
586
587
    /**
588
     *  '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')
589
     *         Note: Filter can be a string like "(t.ref:like:'SO-%') or (t.date_creation:<:'20160101') or (t.nature:is:NULL)"
590
     *  'label' the translation key.
591
     *  'enabled' is a condition when the field must be managed (Example: 1 or 'getDolGlobalString("MY_SETUP_PARAM")'
592
     *  'position' is the sort order of field.
593
     *  'notnull' is set to 1 if not null in database. Set to -1 if we must set data to null if empty ('' or 0).
594
     *  '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)
595
     *  'noteditable' says if field is not editable (1 or 0)
596
     *  '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.
597
     *  'index' if we want an index in database.
598
     *  'foreignkey'=>'tablename.field' if the field is a foreign key (it is recommended to name the field fk_...).
599
     *  'searchall' is 1 if we want to search in this field when making a search from the quick search button.
600
     *  '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).
601
     *  'css' is the CSS style to use on field. For example: 'maxwidth200'
602
     *  'help' is a string visible as a tooltip on field
603
     *  'showoncombobox' if value of the field must be visible into the label of the combobox that list record
604
     *  '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.
605
     *  'arrayofkeyval' to set list of value if type is a list of predefined values. For example: array("0"=>"Draft","1"=>"Active","-1"=>"Cancel")
606
     *  'autofocusoncreate' to have field having the focus on a create form. Only 1 field should have this property set to 1.
607
     *  'comment' is not used. You can store here any text of your choice. It is not used by application.
608
     *
609
     *  Note: To have value dynamic, you can set value to 0 in definition and edit the value on the fly into the constructor.
610
     */
611
612
    /**
613
     * @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...
614
     */
615
    public $fields = array(
616
        'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'index' => 1, 'position' => 1, 'comment' => 'Id'),
617
        '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'),
618
        'entity'        => array('type' => 'integer', 'label' => 'Entity', 'enabled' => 1, 'visible' => 0, 'default' => '1', 'notnull' => 1, 'index' => 1, 'position' => 5),
619
        'label'         => array('type' => 'varchar(255)', 'label' => 'Label', 'enabled' => 1, 'visible' => 1, 'notnull' => 1, 'showoncombobox' => 2, 'position' => 15, 'csslist' => 'tdoverflowmax250'),
620
        'barcode'       => array('type' => 'varchar(255)', 'label' => 'Barcode', 'enabled' => 'isModEnabled("barcode")', 'position' => 20, 'visible' => -1, 'showoncombobox' => 3, 'cssview' => 'tdwordbreak', 'csslist' => 'tdoverflowmax125'),
621
        'fk_barcode_type' => array('type' => 'integer', 'label' => 'BarcodeType', 'enabled' => 1, 'position' => 21, 'notnull' => 0, 'visible' => -1,),
622
        'note_public'   => array('type' => 'html', 'label' => 'NotePublic', 'enabled' => 1, 'visible' => 0, 'position' => 61),
623
        'note'          => array('type' => 'html', 'label' => 'NotePrivate', 'enabled' => 1, 'visible' => 0, 'position' => 62),
624
        'datec'         => array('type' => 'datetime', 'label' => 'DateCreation', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 500),
625
        'tms'           => array('type' => 'timestamp', 'label' => 'DateModification', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 501),
626
        //'date_valid'    =>array('type'=>'datetime',     'label'=>'DateCreation',     'enabled'=>1, 'visible'=>-2, 'position'=>502),
627
        'fk_user_author' => array('type' => 'integer', 'label' => 'UserAuthor', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 510, 'foreignkey' => 'llx_user.rowid'),
628
        'fk_user_modif' => array('type' => 'integer', 'label' => 'UserModif', 'enabled' => 1, 'visible' => -2, 'notnull' => -1, 'position' => 511),
629
        //'fk_user_valid' =>array('type'=>'integer',      'label'=>'UserValidation',        'enabled'=>1, 'visible'=>-1, 'position'=>512),
630
        'localtax1_tx' => array('type' => 'double(6,3)', 'label' => 'Localtax1tx', 'enabled' => 1, 'position' => 150, 'notnull' => 0, 'visible' => -1,),
631
        'localtax1_type' => array('type' => 'varchar(10)', 'label' => 'Localtax1type', 'enabled' => 1, 'position' => 155, 'notnull' => 1, 'visible' => -1,),
632
        'localtax2_tx' => array('type' => 'double(6,3)', 'label' => 'Localtax2tx', 'enabled' => 1, 'position' => 160, 'notnull' => 0, 'visible' => -1,),
633
        'localtax2_type' => array('type' => 'varchar(10)', 'label' => 'Localtax2type', 'enabled' => 1, 'position' => 165, 'notnull' => 1, 'visible' => -1,),
634
        'last_main_doc' => array('type' => 'varchar(255)', 'label' => 'LastMainDoc', 'enabled' => 1, 'visible' => -1, 'position' => 170),
635
        'import_key'    => array('type' => 'varchar(14)', 'label' => 'ImportId', 'enabled' => 1, 'visible' => -2, 'notnull' => -1, 'index' => 0, 'position' => 1000),
636
        //'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')),
637
        //'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')),
638
        'mandatory_period' => array('type' => 'integer', 'label' => 'mandatoryperiod', 'enabled' => 1, 'visible' => 1,  'notnull' => 1, 'default' => '0', 'index' => 1,  'position' => 1000),
639
    );
640
641
    /**
642
     * Regular product
643
     */
644
    const TYPE_PRODUCT = 0;
645
    /**
646
     * Service
647
     */
648
    const TYPE_SERVICE = 1;
649
650
    /**
651
     *  Constructor
652
     *
653
     * @param DoliDB $db Database handler
654
     */
655
    public function __construct($db)
656
    {
657
        $this->db = $db;
658
659
        $this->ismultientitymanaged = 1;
660
        $this->isextrafieldmanaged = 1;
661
662
        $this->canvas = '';
663
    }
664
665
    /**
666
     *    Check that ref and label are ok
667
     *
668
     * @return int         >1 if OK, <=0 if KO
669
     */
670
    public function check()
671
    {
672
        if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
673
            $this->ref = trim($this->ref);
674
        } else {
675
            $this->ref = dol_sanitizeFileName(stripslashes($this->ref));
676
        }
677
678
        $err = 0;
679
        if (dol_strlen(trim($this->ref)) == 0) {
680
            $err++;
681
        }
682
683
        if (dol_strlen(trim($this->label)) == 0) {
684
            $err++;
685
        }
686
687
        if ($err > 0) {
688
            return 0;
689
        } else {
690
            return 1;
691
        }
692
    }
693
694
    /**
695
     * Insert product into database
696
     *
697
     * @param  User     $user           User making insert
698
     * @param  int      $notrigger      Disable triggers
699
     * @return int                      Id of product/service if OK, < 0 if KO
700
     */
701
    public function create($user, $notrigger = 0)
702
    {
703
        global $conf, $langs;
704
705
        $error = 0;
706
707
        // Clean parameters
708
        if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
709
            $this->ref = trim($this->ref);
710
        } else {
711
            $this->ref = dol_sanitizeFileName(dol_string_nospecial(trim($this->ref)));
712
        }
713
        $this->label = trim($this->label);
714
        $this->price_ttc = (float) price2num($this->price_ttc);
715
        $this->price = (float) price2num($this->price);
716
        $this->price_min_ttc = (float) price2num($this->price_min_ttc);
717
        $this->price_min = (float) price2num($this->price_min);
718
        $this->price_label = trim($this->price_label);
719
        if (empty($this->tva_tx)) {
720
            $this->tva_tx = 0;
721
        }
722
        if (empty($this->tva_npr)) {
723
            $this->tva_npr = 0;
724
        }
725
        //Local taxes
726
        if (empty($this->localtax1_tx)) {
727
            $this->localtax1_tx = 0;
728
        }
729
        if (empty($this->localtax2_tx)) {
730
            $this->localtax2_tx = 0;
731
        }
732
        if (empty($this->localtax1_type)) {
733
            $this->localtax1_type = '0';
734
        }
735
        if (empty($this->localtax2_type)) {
736
            $this->localtax2_type = '0';
737
        }
738
        if (empty($this->price)) {
739
            $this->price = 0;
740
        }
741
        if (empty($this->price_min)) {
742
            $this->price_min = 0;
743
        }
744
        // Price by quantity
745
        if (empty($this->price_by_qty)) {
746
            $this->price_by_qty = 0;
747
        }
748
749
        if (empty($this->status)) {
750
            $this->status = 0;
751
        }
752
        if (empty($this->status_buy)) {
753
            $this->status_buy = 0;
754
        }
755
756
        $price_ht = 0;
757
        $price_ttc = 0;
758
        $price_min_ht = 0;
759
        $price_min_ttc = 0;
760
761
        //
762
        if ($this->price_base_type == 'TTC' && $this->price_ttc > 0) {
763
            $price_ttc = price2num($this->price_ttc, 'MU');
764
            $price_ht = price2num($this->price_ttc / (1 + ($this->tva_tx / 100)), 'MU');
765
        }
766
767
        //
768
        if ($this->price_base_type != 'TTC' && $this->price > 0) {
769
            $price_ht = price2num($this->price, 'MU');
770
            $price_ttc = price2num($this->price * (1 + ($this->tva_tx / 100)), 'MU');
771
        }
772
773
        //
774
        if (($this->price_min_ttc > 0) && ($this->price_base_type == 'TTC')) {
775
            $price_min_ttc = price2num($this->price_min_ttc, 'MU');
776
            $price_min_ht = price2num($this->price_min_ttc / (1 + ($this->tva_tx / 100)), 'MU');
777
        }
778
779
        //
780
        if (($this->price_min > 0) && ($this->price_base_type != 'TTC')) {
781
            $price_min_ht = price2num($this->price_min, 'MU');
782
            $price_min_ttc = price2num($this->price_min * (1 + ($this->tva_tx / 100)), 'MU');
783
        }
784
785
        $this->accountancy_code_buy = trim($this->accountancy_code_buy);
786
        $this->accountancy_code_buy_intra = trim($this->accountancy_code_buy_intra);
787
        $this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export);
788
        $this->accountancy_code_sell = trim($this->accountancy_code_sell);
789
        $this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra);
790
        $this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export);
791
792
        // Barcode value
793
        $this->barcode = trim($this->barcode);
794
        $this->mandatory_period = empty($this->mandatory_period) ? 0 : $this->mandatory_period;
795
        // Check parameters
796
        if (empty($this->label)) {
797
            $this->error = 'ErrorMandatoryParametersNotProvided';
798
            return -1;
799
        }
800
801
        if (empty($this->ref) || $this->ref == 'auto') {
802
            // Load object modCodeProduct
803
            $module = getDolGlobalString('PRODUCT_CODEPRODUCT_ADDON', 'mod_codeproduct_leopard');
804
            if ($module != 'mod_codeproduct_leopard') {    // Do not load module file for leopard
805
                if (substr($module, 0, 16) == 'mod_codeproduct_' && substr($module, -3) == 'php') {
806
                    $module = substr($module, 0, dol_strlen($module) - 4);
807
                }
808
                dol_include_once('/core/modules/product/' . $module . '.php');
809
                $modCodeProduct = new $module();
810
                '@phan-var-force ModeleProductCode $modCodeProduct';
811
                if (!empty($modCodeProduct->code_auto)) {
812
                    $this->ref = $modCodeProduct->getNextValue($this, $this->type);
813
                }
814
                unset($modCodeProduct);
815
            }
816
817
            if (empty($this->ref)) {
818
                $this->error = 'ProductModuleNotSetupForAutoRef';
819
                return -2;
820
            }
821
        }
822
823
        dol_syslog(get_class($this) . "::create ref=" . $this->ref . " price=" . $this->price . " price_ttc=" . $this->price_ttc . " tva_tx=" . $this->tva_tx . " price_base_type=" . $this->price_base_type, LOG_DEBUG);
824
825
        $now = dol_now();
826
827
        if (empty($this->date_creation)) {
828
            $this->date_creation = $now;
829
        }
830
831
        $this->db->begin();
832
833
        // For automatic creation during create action (not used by Dolibarr GUI, can be used by scripts)
834
        if ($this->barcode == '-1' || $this->barcode == 'auto') {
835
            $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
836
        }
837
838
        // Check more parameters
839
        // If error, this->errors[] is filled
840
        $result = $this->verify();
841
842
        if ($result >= 0) {
843
            $sql = "SELECT count(*) as nb";
844
            $sql .= " FROM " . $this->db->prefix() . "product";
845
            $sql .= " WHERE entity IN (" . getEntity('product') . ")";
846
            $sql .= " AND ref = '" . $this->db->escape($this->ref) . "'";
847
848
            $result = $this->db->query($sql);
849
            if ($result) {
850
                $obj = $this->db->fetch_object($result);
851
                if ($obj->nb == 0) {
852
                    // Insert new product, no previous one found
853
                    $sql = "INSERT INTO " . $this->db->prefix() . "product (";
854
                    $sql .= "datec";
855
                    $sql .= ", entity";
856
                    $sql .= ", ref";
857
                    $sql .= ", ref_ext";
858
                    $sql .= ", price_min";
859
                    $sql .= ", price_min_ttc";
860
                    $sql .= ", label";
861
                    $sql .= ", fk_user_author";
862
                    $sql .= ", fk_product_type";
863
                    $sql .= ", price";
864
                    $sql .= ", price_ttc";
865
                    $sql .= ", price_base_type";
866
                    $sql .= ", price_label";
867
                    $sql .= ", tobuy";
868
                    $sql .= ", tosell";
869
                    if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
870
                        $sql .= ", accountancy_code_buy";
871
                        $sql .= ", accountancy_code_buy_intra";
872
                        $sql .= ", accountancy_code_buy_export";
873
                        $sql .= ", accountancy_code_sell";
874
                        $sql .= ", accountancy_code_sell_intra";
875
                        $sql .= ", accountancy_code_sell_export";
876
                    }
877
                    $sql .= ", canvas";
878
                    $sql .= ", finished";
879
                    $sql .= ", tobatch";
880
                    $sql .= ", sell_or_eat_by_mandatory";
881
                    $sql .= ", batch_mask";
882
                    $sql .= ", fk_unit";
883
                    $sql .= ", mandatory_period";
884
                    $sql .= ") VALUES (";
885
                    $sql .= "'" . $this->db->idate($this->date_creation) . "'";
886
                    $sql .= ", " . (!empty($this->entity) ? (int) $this->entity : (int) $conf->entity);
887
                    $sql .= ", '" . $this->db->escape($this->ref) . "'";
888
                    $sql .= ", " . (!empty($this->ref_ext) ? "'" . $this->db->escape($this->ref_ext) . "'" : "null");
889
                    $sql .= ", " . price2num($price_min_ht);
890
                    $sql .= ", " . price2num($price_min_ttc);
891
                    $sql .= ", " . (!empty($this->label) ? "'" . $this->db->escape($this->label) . "'" : "null");
892
                    $sql .= ", " . ((int) $user->id);
893
                    $sql .= ", " . ((int) $this->type);
894
                    $sql .= ", " . price2num($price_ht, 'MT');
895
                    $sql .= ", " . price2num($price_ttc, 'MT');
896
                    $sql .= ", '" . $this->db->escape($this->price_base_type) . "'";
897
                    $sql .= ", " . (!empty($this->price_label) ? "'" . $this->db->escape($this->price_label) . "'" : "null");
898
                    $sql .= ", " . ((int) $this->status);
899
                    $sql .= ", " . ((int) $this->status_buy);
900
                    if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
901
                        $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
902
                        $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
903
                        $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
904
                        $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
905
                        $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
906
                        $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
907
                    }
908
                    $sql .= ", '" . $this->db->escape($this->canvas) . "'";
909
                    $sql .= ", " . ((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? 'NULL' : (int) $this->finished);
910
                    $sql .= ", " . ((empty($this->status_batch) || $this->status_batch < 0) ? '0' : ((int) $this->status_batch));
911
                    $sql .= ", " . ((empty($this->sell_or_eat_by_mandatory) || $this->sell_or_eat_by_mandatory < 0) ? 0 : ((int) $this->sell_or_eat_by_mandatory));
912
                    $sql .= ", '" . $this->db->escape($this->batch_mask) . "'";
913
                    $sql .= ", " . ($this->fk_unit > 0 ? ((int) $this->fk_unit) : 'NULL');
914
                    $sql .= ", '" . $this->db->escape($this->mandatory_period) . "'";
915
                    $sql .= ")";
916
917
                    dol_syslog(get_class($this) . "::Create", LOG_DEBUG);
918
919
                    $result = $this->db->query($sql);
920
                    if ($result) {
921
                        $id = $this->db->last_insert_id($this->db->prefix() . "product");
922
923
                        if ($id > 0) {
924
                            $this->id = $id;
925
                            $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...
926
                            $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...
927
                            $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...
928
                            $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...
929
930
                            $result = $this->_log_price($user);
931
                            if ($result > 0) {
932
                                if ($this->update($id, $user, true, 'add') <= 0) {
933
                                    $error++;
934
                                }
935
                            } else {
936
                                $error++;
937
                                $this->error = $this->db->lasterror();
938
                            }
939
940
                            // update accountancy for this entity
941
                            if (!$error && getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
942
                                $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " . ((int) $this->id) . " AND entity = " . ((int) $conf->entity));
943
944
                                $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
945
                                $sql .= " fk_product";
946
                                $sql .= ", entity";
947
                                $sql .= ", accountancy_code_buy";
948
                                $sql .= ", accountancy_code_buy_intra";
949
                                $sql .= ", accountancy_code_buy_export";
950
                                $sql .= ", accountancy_code_sell";
951
                                $sql .= ", accountancy_code_sell_intra";
952
                                $sql .= ", accountancy_code_sell_export";
953
                                $sql .= ") VALUES (";
954
                                $sql .= $this->id;
955
                                $sql .= ", " . $conf->entity;
956
                                $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
957
                                $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
958
                                $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
959
                                $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
960
                                $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
961
                                $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
962
                                $sql .= ")";
963
                                $result = $this->db->query($sql);
964
                                if (!$result) {
965
                                    $error++;
966
                                    $this->error = 'ErrorFailedToInsertAccountancyForEntity';
967
                                }
968
                            }
969
                        } else {
970
                            $error++;
971
                            $this->error = 'ErrorFailedToGetInsertedId';
972
                        }
973
                    } else {
974
                        $error++;
975
                        $this->error = $this->db->lasterror();
976
                    }
977
                } else {
978
                    // Product already exists with this ref
979
                    $langs->load("products");
980
                    $error++;
981
                    $this->error = "ErrorProductAlreadyExists";
982
                    dol_syslog(get_class($this) . "::Create fails, ref " . $this->ref . " already exists");
983
                }
984
            } else {
985
                $error++;
986
                $this->error = $this->db->lasterror();
987
            }
988
989
            if (!$error && !$notrigger) {
990
                // Call trigger
991
                $result = $this->call_trigger('PRODUCT_CREATE', $user);
992
                if ($result < 0) {
993
                    $error++;
994
                }
995
                // End call triggers
996
            }
997
998
            if (!$error) {
999
                $this->db->commit();
1000
                return $this->id;
1001
            } else {
1002
                $this->db->rollback();
1003
                return -$error;
1004
            }
1005
        } else {
1006
            $this->db->rollback();
1007
            dol_syslog(get_class($this) . "::Create fails verify " . implode(',', $this->errors), LOG_WARNING);
1008
            return -3;
1009
        }
1010
    }
1011
1012
1013
    /**
1014
     *    Check properties of product are ok (like name, barcode, ...).
1015
     *    All properties must be already loaded on object (this->barcode, this->barcode_type_code, ...).
1016
     *
1017
     * @return int        0 if OK, <0 if KO
1018
     */
1019
    public function verify()
1020
    {
1021
        global $langs;
1022
1023
        $this->errors = array();
1024
1025
        $result = 0;
1026
        $this->ref = trim($this->ref);
1027
1028
        if (!$this->ref) {
1029
            $this->errors[] = 'ErrorBadRef';
1030
            $result = -2;
1031
        }
1032
1033
        $arrayofnonnegativevalue = array('weight' => 'Weight', 'width' => 'Width', 'height' => 'Height', 'length' => 'Length', 'surface' => 'Surface', 'volume' => 'Volume');
1034
        foreach ($arrayofnonnegativevalue as $key => $value) {
1035
            if (property_exists($this, $key) && !empty($this->$key) && ($this->$key < 0)) {
1036
                $langs->loadLangs(array("main", "other"));
1037
                $this->error = $langs->trans("FieldCannotBeNegative", $langs->transnoentitiesnoconv($value));
1038
                $this->errors[] = $this->error;
1039
                $result = -4;
1040
            }
1041
        }
1042
1043
        $rescode = $this->check_barcode($this->barcode, $this->barcode_type_code);
1044
        if ($rescode) {
1045
            if ($rescode == -1) {
1046
                $this->errors[] = 'ErrorBadBarCodeSyntax';
1047
            } elseif ($rescode == -2) {
1048
                $this->errors[] = 'ErrorBarCodeRequired';
1049
            } elseif ($rescode == -3) {
1050
                // Note: Common usage is to have barcode unique. For variants, we should have a different barcode.
1051
                $this->errors[] = 'ErrorBarCodeAlreadyUsed';
1052
            }
1053
1054
            $result = -3;
1055
        }
1056
1057
        return $result;
1058
    }
1059
1060
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1061
    /**
1062
     *  Check barcode
1063
     *
1064
     * @param  string $valuetotest Value to test
1065
     * @param  string $typefortest Type of barcode (ISBN, EAN, ...)
1066
     * @return int                        0 if OK
1067
     *                                     -1 ErrorBadBarCodeSyntax
1068
     *                                     -2 ErrorBarCodeRequired
1069
     *                                     -3 ErrorBarCodeAlreadyUsed
1070
     */
1071
    public function check_barcode($valuetotest, $typefortest)
1072
    {
1073
		// phpcs:enable
1074
        global $conf;
1075
        if (isModEnabled('barcode') && getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
1076
            $module = strtolower(getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM'));
1077
1078
            $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
1079
            foreach ($dirsociete as $dirroot) {
1080
                $res = dol_include_once($dirroot . $module . '.php');
1081
                if ($res) {
1082
                    break;
1083
                }
1084
            }
1085
1086
            $mod = new $module();
1087
            '@phan-var-force ModeleNumRefBarCode $mod';
1088
1089
            dol_syslog(get_class($this) . "::check_barcode value=" . $valuetotest . " type=" . $typefortest . " module=" . $module);
1090
            $result = $mod->verif($this->db, $valuetotest, $this, 0, $typefortest);
1091
            return $result;
1092
        } else {
1093
            return 0;
1094
        }
1095
    }
1096
1097
    /**
1098
     *  Update a record into database.
1099
     *  If batch flag is set to on, we create records into llx_product_batch
1100
     *
1101
     * @param  int     $id          Id of product
1102
     * @param  User    $user        Object user making update
1103
     * @param  int     $notrigger   Disable triggers
1104
     * @param  string  $action      Current action for hookmanager ('add' or 'update')
1105
     * @param  boolean $updatetype  Update product type
1106
     * @return int                  1 if OK, -1 if ref already exists, -2 if other error
1107
     */
1108
    public function update($id, $user, $notrigger = 0, $action = 'update', $updatetype = false)
1109
    {
1110
        global $langs, $conf, $hookmanager;
1111
1112
        $error = 0;
1113
1114
        // Check parameters
1115
        if (!$this->label) {
1116
            $this->label = 'MISSING LABEL';
1117
        }
1118
1119
        // Clean parameters
1120
        if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
1121
            $this->ref = trim($this->ref);
1122
        } else {
1123
            $this->ref = dol_string_nospecial(trim($this->ref));
1124
        }
1125
        $this->label = trim($this->label);
1126
        $this->description = trim($this->description);
1127
        $this->note_private = (isset($this->note_private) ? trim($this->note_private) : null);
1128
        $this->note_public = (isset($this->note_public) ? trim($this->note_public) : null);
1129
        $this->net_measure = price2num($this->net_measure);
1130
        $this->net_measure_units = (empty($this->net_measure_units) ? '' : trim($this->net_measure_units));
1131
        $this->weight = price2num($this->weight);
1132
        $this->weight_units = (empty($this->weight_units) ? '' : trim($this->weight_units));
1133
        $this->length = price2num($this->length);
1134
        $this->length_units = (empty($this->length_units) ? '' : trim($this->length_units));
1135
        $this->width = price2num($this->width);
1136
        $this->width_units = (empty($this->width_units) ? '' : trim($this->width_units));
1137
        $this->height = price2num($this->height);
1138
        $this->height_units = (empty($this->height_units) ? '' : trim($this->height_units));
1139
        $this->surface = price2num($this->surface);
1140
        $this->surface_units = (empty($this->surface_units) ? '' : trim($this->surface_units));
1141
        $this->volume = price2num($this->volume);
1142
        $this->volume_units = (empty($this->volume_units) ? '' : trim($this->volume_units));
1143
1144
        // set unit not defined
1145
        if (is_numeric($this->length_units)) {
1146
            $this->width_units = $this->length_units; // Not used yet
1147
        }
1148
        if (is_numeric($this->length_units)) {
1149
            $this->height_units = $this->length_units; // Not used yet
1150
        }
1151
1152
        // Automated compute surface and volume if not filled
1153
        if (empty($this->surface) && !empty($this->length) && !empty($this->width) && $this->length_units == $this->width_units) {
1154
            $this->surface = (float) $this->length * (float) $this->width;
1155
            $this->surface_units = measuring_units_squared($this->length_units);
1156
        }
1157
        if (empty($this->volume) && !empty($this->surface) && !empty($this->height) && $this->length_units == $this->height_units) {
1158
            $this->volume = $this->surface * (float) $this->height;
1159
            $this->volume_units = measuring_units_cubed($this->height_units);
1160
        }
1161
1162
        if (empty($this->tva_tx)) {
1163
            $this->tva_tx = 0;
1164
        }
1165
        if (empty($this->tva_npr)) {
1166
            $this->tva_npr = 0;
1167
        }
1168
        if (empty($this->localtax1_tx)) {
1169
            $this->localtax1_tx = 0;
1170
        }
1171
        if (empty($this->localtax2_tx)) {
1172
            $this->localtax2_tx = 0;
1173
        }
1174
        if (empty($this->localtax1_type)) {
1175
            $this->localtax1_type = '0';
1176
        }
1177
        if (empty($this->localtax2_type)) {
1178
            $this->localtax2_type = '0';
1179
        }
1180
        if (empty($this->status)) {
1181
            $this->status = 0;
1182
        }
1183
        if (empty($this->status_buy)) {
1184
            $this->status_buy = 0;
1185
        }
1186
1187
        if (empty($this->country_id)) {
1188
            $this->country_id = 0;
1189
        }
1190
1191
        if (empty($this->state_id)) {
1192
            $this->state_id = 0;
1193
        }
1194
1195
        // Barcode value
1196
        $this->barcode = (empty($this->barcode) ? '' : trim($this->barcode));
1197
1198
        $this->accountancy_code_buy = trim($this->accountancy_code_buy);
1199
        $this->accountancy_code_buy_intra = (!empty($this->accountancy_code_buy_intra) ? trim($this->accountancy_code_buy_intra) : '');
1200
        $this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export);
1201
        $this->accountancy_code_sell = trim($this->accountancy_code_sell);
1202
        $this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra);
1203
        $this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export);
1204
1205
1206
        $this->db->begin();
1207
1208
        $result = 0;
1209
        // Check name is required and codes are ok or unique. If error, this->errors[] is filled
1210
        if ($action != 'add') {
1211
            $result = $this->verify(); // We don't check when update called during a create because verify was already done
1212
        } else {
1213
            // we can continue
1214
            $result = 0;
1215
        }
1216
1217
        if ($result >= 0) {
1218
            // $this->oldcopy should have been set by the caller of update (here properties were already modified)
1219
            if (is_null($this->oldcopy) || (is_object($this->oldcopy) && $this->oldcopy->isEmpty())) {
1220
                $this->oldcopy = dol_clone($this, 1);
1221
            }
1222
            // Test if batch management is activated on existing product
1223
            // If yes, we create missing entries into product_batch
1224
            if ($this->hasbatch() && !$this->oldcopy->hasbatch()) {
1225
                //$valueforundefinedlot = 'Undefined';  // In previous version, 39 and lower
1226
                $valueforundefinedlot = '000000';
1227
                if (getDolGlobalString('STOCK_DEFAULT_BATCH')) {
1228
                    $valueforundefinedlot = getDolGlobalString('STOCK_DEFAULT_BATCH');
1229
                }
1230
1231
                dol_syslog("Flag batch of product id=" . $this->id . " is set to ON, so we will create missing records into product_batch");
1232
1233
                $this->load_stock();
1234
                foreach ($this->stock_warehouse as $idW => $ObjW) {   // For each warehouse where we have stocks defined for this product (for each lines in product_stock)
1235
                    $qty_batch = 0;
1236
                    foreach ($ObjW->detail_batch as $detail) {    // Each lines of detail in product_batch of the current $ObjW = product_stock
1237
                        if ($detail->batch == $valueforundefinedlot || $detail->batch == 'Undefined') {
1238
                            // We discard this line, we will create it later
1239
                            $sqlclean = "DELETE FROM " . $this->db->prefix() . "product_batch WHERE batch in('Undefined', '" . $this->db->escape($valueforundefinedlot) . "') AND fk_product_stock = " . ((int) $ObjW->id);
1240
                            $result = $this->db->query($sqlclean);
1241
                            if (!$result) {
1242
                                dol_print_error($this->db);
1243
                                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...
1244
                            }
1245
                            continue;
1246
                        }
1247
1248
                        $qty_batch += $detail->qty;
1249
                    }
1250
                    // Quantities in batch details are not same as stock quantity,
1251
                    // so we add a default batch record to complete and get same qty in parent and child table
1252
                    if ($ObjW->real != $qty_batch) {
1253
                        $ObjBatch = new Productbatch($this->db);
1254
                        $ObjBatch->batch = $valueforundefinedlot;
1255
                        $ObjBatch->qty = ($ObjW->real - $qty_batch);
1256
                        $ObjBatch->fk_product_stock = $ObjW->id;
1257
1258
                        if ($ObjBatch->create($user, 1) < 0) {
1259
                            $error++;
1260
                            $this->errors = $ObjBatch->errors;
1261
                        } else {
1262
                            // we also add lot record if not exist
1263
                            $ObjLot = new Productlot($this->db);
1264
                            // @phan-suppress-next-line PhanPluginSuspiciousParamPosition
1265
                            if ($ObjLot->fetch(0, $this->id, $valueforundefinedlot) == 0) {
1266
                                $ObjLot->fk_product = $this->id;
1267
                                $ObjLot->entity = $this->entity;
1268
                                $ObjLot->fk_user_creat = $user->id;
1269
                                $ObjLot->batch = $valueforundefinedlot;
1270
                                if ($ObjLot->create($user, true) < 0) {
1271
                                    $error++;
1272
                                    $this->errors = $ObjLot->errors;
1273
                                }
1274
                            }
1275
                        }
1276
                    }
1277
                }
1278
            }
1279
1280
            // For automatic creation
1281
            if ($this->barcode == -1) {
1282
                $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
1283
            }
1284
1285
            $sql = "UPDATE " . $this->db->prefix() . "product";
1286
            $sql .= " SET label = '" . $this->db->escape($this->label) . "'";
1287
1288
            if ($updatetype && ($this->isProduct() || $this->isService())) {
1289
                $sql .= ", fk_product_type = " . ((int) $this->type);
1290
            }
1291
1292
            $sql .= ", ref = '" . $this->db->escape($this->ref) . "'";
1293
            $sql .= ", ref_ext = " . (!empty($this->ref_ext) ? "'" . $this->db->escape($this->ref_ext) . "'" : "null");
1294
            $sql .= ", default_vat_code = " . ($this->default_vat_code ? "'" . $this->db->escape($this->default_vat_code) . "'" : "null");
1295
            $sql .= ", tva_tx = " . ((float) $this->tva_tx);
1296
            $sql .= ", recuperableonly = " . ((int) $this->tva_npr);
1297
            $sql .= ", localtax1_tx = " . ((float) $this->localtax1_tx);
1298
            $sql .= ", localtax2_tx = " . ((float) $this->localtax2_tx);
1299
            $sql .= ", localtax1_type = " . ($this->localtax1_type != '' ? "'" . $this->db->escape($this->localtax1_type) . "'" : "'0'");
1300
            $sql .= ", localtax2_type = " . ($this->localtax2_type != '' ? "'" . $this->db->escape($this->localtax2_type) . "'" : "'0'");
1301
1302
            $sql .= ", barcode = " . (empty($this->barcode) ? "null" : "'" . $this->db->escape($this->barcode) . "'");
1303
            $sql .= ", fk_barcode_type = " . (empty($this->barcode_type) ? "null" : $this->db->escape($this->barcode_type));
1304
1305
            $sql .= ", tosell = " . (int) $this->status;
1306
            $sql .= ", tobuy = " . (int) $this->status_buy;
1307
            $sql .= ", tobatch = " . ((empty($this->status_batch) || $this->status_batch < 0) ? '0' : (int) $this->status_batch);
1308
            $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);
1309
            $sql .= ", batch_mask = '" . $this->db->escape($this->batch_mask) . "'";
1310
1311
            $sql .= ", finished = " . ((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? "null" : (int) $this->finished);
1312
            $sql .= ", fk_default_bom = " . ((!isset($this->fk_default_bom) || $this->fk_default_bom < 0 || $this->fk_default_bom == '') ? "null" : (int) $this->fk_default_bom);
1313
            $sql .= ", net_measure = " . ($this->net_measure != '' ? "'" . $this->db->escape($this->net_measure) . "'" : 'null');
1314
            $sql .= ", net_measure_units = " . ($this->net_measure_units != '' ? "'" . $this->db->escape($this->net_measure_units) . "'" : 'null');
1315
            $sql .= ", weight = " . ($this->weight != '' ? "'" . $this->db->escape($this->weight) . "'" : 'null');
1316
            $sql .= ", weight_units = " . ($this->weight_units != '' ? "'" . $this->db->escape($this->weight_units) . "'" : 'null');
1317
            $sql .= ", length = " . ($this->length != '' ? "'" . $this->db->escape($this->length) . "'" : 'null');
1318
            $sql .= ", length_units = " . ($this->length_units != '' ? "'" . $this->db->escape($this->length_units) . "'" : 'null');
1319
            $sql .= ", width= " . ($this->width != '' ? "'" . $this->db->escape($this->width) . "'" : 'null');
1320
            $sql .= ", width_units = " . ($this->width_units != '' ? "'" . $this->db->escape($this->width_units) . "'" : 'null');
1321
            $sql .= ", height = " . ($this->height != '' ? "'" . $this->db->escape($this->height) . "'" : 'null');
1322
            $sql .= ", height_units = " . ($this->height_units != '' ? "'" . $this->db->escape($this->height_units) . "'" : 'null');
1323
            $sql .= ", surface = " . ($this->surface != '' ? "'" . $this->db->escape($this->surface) . "'" : 'null');
1324
            $sql .= ", surface_units = " . ($this->surface_units != '' ? "'" . $this->db->escape($this->surface_units) . "'" : 'null');
1325
            $sql .= ", volume = " . ($this->volume != '' ? "'" . $this->db->escape($this->volume) . "'" : 'null');
1326
            $sql .= ", volume_units = " . ($this->volume_units != '' ? "'" . $this->db->escape($this->volume_units) . "'" : 'null');
1327
            $sql .= ", fk_default_warehouse = " . ($this->fk_default_warehouse > 0 ? ((int) $this->fk_default_warehouse) : 'null');
1328
            $sql .= ", fk_default_workstation = " . ($this->fk_default_workstation > 0 ? ((int) $this->fk_default_workstation) : 'null');
1329
            $sql .= ", seuil_stock_alerte = " . ((isset($this->seuil_stock_alerte) && is_numeric($this->seuil_stock_alerte)) ? (float) $this->seuil_stock_alerte : 'null');
1330
            $sql .= ", description = '" . $this->db->escape($this->description) . "'";
1331
            $sql .= ", url = " . ($this->url ? "'" . $this->db->escape($this->url) . "'" : 'null');
1332
            $sql .= ", customcode = '" . $this->db->escape($this->customcode) . "'";
1333
            $sql .= ", fk_country = " . ($this->country_id > 0 ? (int) $this->country_id : 'null');
1334
            $sql .= ", fk_state = " . ($this->state_id > 0 ? (int) $this->state_id : 'null');
1335
            $sql .= ", lifetime = " . ($this->lifetime > 0 ? (int) $this->lifetime : 'null');
1336
            $sql .= ", qc_frequency = " . ($this->qc_frequency > 0 ? (int) $this->qc_frequency : 'null');
1337
            $sql .= ", note = " . (isset($this->note_private) ? "'" . $this->db->escape($this->note_private) . "'" : 'null');
1338
            $sql .= ", note_public = " . (isset($this->note_public) ? "'" . $this->db->escape($this->note_public) . "'" : 'null');
1339
            $sql .= ", duration = '" . $this->db->escape($this->duration_value . $this->duration_unit) . "'";
1340
            if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1341
                $sql .= ", accountancy_code_buy = '" . $this->db->escape($this->accountancy_code_buy) . "'";
1342
                $sql .= ", accountancy_code_buy_intra = '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1343
                $sql .= ", accountancy_code_buy_export = '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1344
                $sql .= ", accountancy_code_sell= '" . $this->db->escape($this->accountancy_code_sell) . "'";
1345
                $sql .= ", accountancy_code_sell_intra= '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1346
                $sql .= ", accountancy_code_sell_export= '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1347
            }
1348
            $sql .= ", desiredstock = " . ((isset($this->desiredstock) && is_numeric($this->desiredstock)) ? (float) $this->desiredstock : "null");
1349
            $sql .= ", cost_price = " . ($this->cost_price != '' ? $this->db->escape($this->cost_price) : 'null');
1350
            $sql .= ", fk_unit= " . (!$this->fk_unit ? 'NULL' : (int) $this->fk_unit);
1351
            $sql .= ", price_autogen = " . (!$this->price_autogen ? 0 : 1);
1352
            $sql .= ", fk_price_expression = " . ($this->fk_price_expression != 0 ? (int) $this->fk_price_expression : 'NULL');
1353
            $sql .= ", fk_user_modif = " . ($user->id > 0 ? $user->id : 'NULL');
1354
            $sql .= ", mandatory_period = " . ($this->mandatory_period);
1355
            // stock field is not here because it is a denormalized value from product_stock.
1356
            $sql .= " WHERE rowid = " . ((int) $id);
1357
1358
            dol_syslog(get_class($this) . "::update", LOG_DEBUG);
1359
1360
            $resql = $this->db->query($sql);
1361
            if ($resql) {
1362
                $this->id = $id;
1363
1364
                // Multilangs
1365
                if (getDolGlobalInt('MAIN_MULTILANGS')) {
1366
                    if ($this->setMultiLangs($user) < 0) {
1367
                        $this->error = $langs->trans("Error") . " : " . $this->db->error() . " - " . $sql;
1368
                        $this->db->rollback();
1369
                        return -2;
1370
                    }
1371
                }
1372
1373
                $action = 'update';
1374
1375
                // update accountancy for this entity
1376
                if (!$error && getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1377
                    $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " . ((int) $this->id) . " AND entity = " . ((int) $conf->entity));
1378
1379
                    $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
1380
                    $sql .= " fk_product";
1381
                    $sql .= ", entity";
1382
                    $sql .= ", accountancy_code_buy";
1383
                    $sql .= ", accountancy_code_buy_intra";
1384
                    $sql .= ", accountancy_code_buy_export";
1385
                    $sql .= ", accountancy_code_sell";
1386
                    $sql .= ", accountancy_code_sell_intra";
1387
                    $sql .= ", accountancy_code_sell_export";
1388
                    $sql .= ") VALUES (";
1389
                    $sql .= $this->id;
1390
                    $sql .= ", " . $conf->entity;
1391
                    $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
1392
                    $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1393
                    $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1394
                    $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
1395
                    $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1396
                    $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1397
                    $sql .= ")";
1398
                    $result = $this->db->query($sql);
1399
                    if (!$result) {
1400
                        $error++;
1401
                        $this->error = 'ErrorFailedToUpdateAccountancyForEntity';
1402
                    }
1403
                }
1404
1405
                if (!$this->hasbatch() && $this->oldcopy->hasbatch()) {
1406
                    // Selection of all product stock movements that contains batchs
1407
                    $sql = 'SELECT pb.qty, ps.fk_entrepot, pb.batch FROM ' . MAIN_DB_PREFIX . 'product_batch as pb';
1408
                    $sql .= ' INNER JOIN ' . MAIN_DB_PREFIX . 'product_stock as ps ON (ps.rowid = pb.fk_product_stock)';
1409
                    $sql .= ' WHERE ps.fk_product = ' . (int) $this->id;
1410
1411
                    $resql = $this->db->query($sql);
1412
                    if ($resql) {
1413
                        $inventorycode = dol_print_date(dol_now(), '%Y%m%d%H%M%S');
1414
1415
                        while ($obj = $this->db->fetch_object($resql)) {
1416
                            $value = $obj->qty;
1417
                            $fk_entrepot = $obj->fk_entrepot;
1418
                            $price = 0;
1419
                            $dlc = '';
1420
                            $dluo = '';
1421
                            $batch = $obj->batch;
1422
1423
                            // To know how to revert stockMouvement (add or remove)
1424
                            $addOremove = $value > 0 ? 1 : 0; // 1 if remove, 0 if add
1425
                            $label = $langs->trans('BatchStockMouvementAddInGlobal');
1426
                            $res = $this->correct_stock_batch($user, $fk_entrepot, abs($value), $addOremove, $label, $price, $dlc, $dluo, $batch, $inventorycode, '', null, 0, null, true);
1427
1428
                            if ($res > 0) {
1429
                                $label = $langs->trans('BatchStockMouvementAddInGlobal');
1430
                                $res = $this->correct_stock($user, $fk_entrepot, abs($value), (int) empty($addOremove), $label, $price, $inventorycode, '', null, 0);
1431
                                if ($res < 0) {
1432
                                    $error++;
1433
                                }
1434
                            } else {
1435
                                $error++;
1436
                            }
1437
                        }
1438
                    }
1439
                }
1440
1441
                // Actions on extra fields
1442
                if (!$error) {
1443
                    $result = $this->insertExtraFields();
1444
                    if ($result < 0) {
1445
                        $error++;
1446
                    }
1447
                }
1448
1449
                if (!$error && !$notrigger) {
1450
                    // Call trigger
1451
                    $result = $this->call_trigger('PRODUCT_MODIFY', $user);
1452
                    if ($result < 0) {
1453
                        $error++;
1454
                    }
1455
                    // End call triggers
1456
                }
1457
1458
                if (!$error && (is_object($this->oldcopy) && $this->oldcopy->ref !== $this->ref)) {
1459
                    // We remove directory
1460
                    if ($conf->product->dir_output) {
1461
                        $olddir = $conf->product->dir_output . "/" . dol_sanitizeFileName($this->oldcopy->ref);
1462
                        $newdir = $conf->product->dir_output . "/" . dol_sanitizeFileName($this->ref);
1463
                        if (file_exists($olddir)) {
1464
                            //include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
1465
                            //$res = dol_move($olddir, $newdir);
1466
                            // do not use dol_move with directory
1467
                            $res = @rename($olddir, $newdir);
1468
                            if (!$res) {
1469
                                $langs->load("errors");
1470
                                $this->error = $langs->trans('ErrorFailToRenameDir', $olddir, $newdir);
1471
                                $error++;
1472
                            }
1473
                        }
1474
                    }
1475
                }
1476
1477
                if (!$error) {
1478
                    if (isModEnabled('variants')) {
1479
                        include_once DOL_DOCUMENT_ROOT . '/variants/class/ProductCombination.class.php';
1480
1481
                        $comb = new ProductCombination($this->db);
1482
1483
                        foreach ($comb->fetchAllByFkProductParent($this->id) as $currcomb) {
1484
                            $currcomb->updateProperties($this, $user);
1485
                        }
1486
                    }
1487
1488
                    $this->db->commit();
1489
                    return 1;
1490
                } else {
1491
                    $this->db->rollback();
1492
                    return -$error;
1493
                }
1494
            } else {
1495
                if ($this->db->errno() == 'DB_ERROR_RECORD_ALREADY_EXISTS') {
1496
                    $langs->load("errors");
1497
                    if (empty($conf->barcode->enabled) || empty($this->barcode)) {
1498
                        $this->error = $langs->trans("Error") . " : " . $langs->trans("ErrorProductAlreadyExists", $this->ref);
1499
                    } else {
1500
                        $this->error = $langs->trans("Error") . " : " . $langs->trans("ErrorProductBarCodeAlreadyExists", $this->barcode);
1501
                    }
1502
                    $this->errors[] = $this->error;
1503
                    $this->db->rollback();
1504
                    return -1;
1505
                } else {
1506
                    $this->error = $langs->trans("Error") . " : " . $this->db->error() . " - " . $sql;
1507
                    $this->errors[] = $this->error;
1508
                    $this->db->rollback();
1509
                    return -2;
1510
                }
1511
            }
1512
        } else {
1513
            $this->db->rollback();
1514
            dol_syslog(get_class($this) . "::Update fails verify " . implode(',', $this->errors), LOG_WARNING);
1515
            return -3;
1516
        }
1517
    }
1518
1519
    /**
1520
     *  Delete a product from database (if not used)
1521
     *
1522
     * @param  User $user      User (object) deleting product
1523
     * @param  int  $notrigger Do not execute trigger
1524
     * @return int                    Return integer < 0 if KO, 0 = Not possible, > 0 if OK
1525
     */
1526
    public function delete(User $user, $notrigger = 0)
1527
    {
1528
        global $conf, $langs;
1529
        include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
1530
1531
        $error = 0;
1532
1533
        // Check parameters
1534
        if (empty($this->id)) {
1535
            $this->error = "Object must be fetched before calling delete";
1536
            return -1;
1537
        }
1538
        if (($this->type == Product::TYPE_PRODUCT && !$user->hasRight('produit', 'supprimer')) || ($this->type == Product::TYPE_SERVICE && !$user->hasRight('service', 'supprimer'))) {
1539
            $this->error = "ErrorForbidden";
1540
            return 0;
1541
        }
1542
1543
        $objectisused = $this->isObjectUsed($this->id);
1544
        if (empty($objectisused)) {
1545
            $this->db->begin();
1546
1547
            if (!$error && empty($notrigger)) {
1548
                // Call trigger
1549
                $result = $this->call_trigger('PRODUCT_DELETE', $user);
1550
                if ($result < 0) {
1551
                    $error++;
1552
                }
1553
                // End call triggers
1554
            }
1555
1556
            // Delete from product_batch on product delete
1557
            if (!$error) {
1558
                $sql = "DELETE FROM " . $this->db->prefix() . 'product_batch';
1559
                $sql .= " WHERE fk_product_stock IN (";
1560
                $sql .= "SELECT rowid FROM " . $this->db->prefix() . 'product_stock';
1561
                $sql .= " WHERE fk_product = " . ((int) $this->id) . ")";
1562
1563
                $result = $this->db->query($sql);
1564
                if (!$result) {
1565
                    $error++;
1566
                    $this->errors[] = $this->db->lasterror();
1567
                }
1568
            }
1569
1570
            // Delete all child tables
1571
            if (!$error) {
1572
                $elements = array('product_fournisseur_price', 'product_price', 'product_lang', 'categorie_product', 'product_stock', 'product_customer_price', 'product_lot'); // product_batch is done before
1573
                foreach ($elements as $table) {
1574
                    if (!$error) {
1575
                        $sql = "DELETE FROM " . $this->db->prefix() . $table;
1576
                        $sql .= " WHERE fk_product = " . (int) $this->id;
1577
1578
                        $result = $this->db->query($sql);
1579
                        if (!$result) {
1580
                            $error++;
1581
                            $this->errors[] = $this->db->lasterror();
1582
                        }
1583
                    }
1584
                }
1585
            }
1586
1587
            if (!$error) {
1588
                include_once DOL_DOCUMENT_ROOT . '/variants/class/ProductCombination.class.php';
1589
                include_once DOL_DOCUMENT_ROOT . '/variants/class/ProductCombination2ValuePair.class.php';
1590
1591
                //If it is a parent product, then we remove the association with child products
1592
                $prodcomb = new ProductCombination($this->db);
1593
1594
                if ($prodcomb->deleteByFkProductParent($user, $this->id) < 0) {
1595
                    $error++;
1596
                    $this->errors[] = 'Error deleting combinations';
1597
                }
1598
1599
                //We also check if it is a child product
1600
                if (!$error && ($prodcomb->fetchByFkProductChild($this->id) > 0) && ($prodcomb->delete($user) < 0)) {
1601
                    $error++;
1602
                    $this->errors[] = 'Error deleting child combination';
1603
                }
1604
            }
1605
1606
            // Delete from product_association
1607
            if (!$error) {
1608
                $sql = "DELETE FROM " . $this->db->prefix() . "product_association";
1609
                $sql .= " WHERE fk_product_pere = " . (int) $this->id . " OR fk_product_fils = " . (int) $this->id;
1610
1611
                $result = $this->db->query($sql);
1612
                if (!$result) {
1613
                    $error++;
1614
                    $this->errors[] = $this->db->lasterror();
1615
                }
1616
            }
1617
1618
            // Remove extrafields
1619
            if (!$error) {
1620
                $result = $this->deleteExtraFields();
1621
                if ($result < 0) {
1622
                    $error++;
1623
                    dol_syslog(get_class($this) . "::delete error -4 " . $this->error, LOG_ERR);
1624
                }
1625
            }
1626
1627
            // Delete product
1628
            if (!$error) {
1629
                $sqlz = "DELETE FROM " . $this->db->prefix() . "product";
1630
                $sqlz .= " WHERE rowid = " . (int) $this->id;
1631
1632
                $resultz = $this->db->query($sqlz);
1633
                if (!$resultz) {
1634
                    $error++;
1635
                    $this->errors[] = $this->db->lasterror();
1636
                }
1637
            }
1638
1639
            // Delete record into ECM index and physically
1640
            if (!$error) {
1641
                $res = $this->deleteEcmFiles(0); // Deleting files physically is done later with the dol_delete_dir_recursive
1642
                $res = $this->deleteEcmFiles(1); // Deleting files physically is done later with the dol_delete_dir_recursive
1643
                if (!$res) {
1644
                    $error++;
1645
                }
1646
            }
1647
1648
            if (!$error) {
1649
                // We remove directory
1650
                $ref = dol_sanitizeFileName($this->ref);
1651
                if ($conf->product->dir_output) {
1652
                    $dir = $conf->product->dir_output . "/" . $ref;
1653
                    if (file_exists($dir)) {
1654
                        $res = @dol_delete_dir_recursive($dir);
1655
                        if (!$res) {
1656
                            $this->errors[] = 'ErrorFailToDeleteDir';
1657
                            $error++;
1658
                        }
1659
                    }
1660
                }
1661
            }
1662
1663
            if (!$error) {
1664
                $this->db->commit();
1665
                return 1;
1666
            } else {
1667
                foreach ($this->errors as $errmsg) {
1668
                    dol_syslog(get_class($this) . "::delete " . $errmsg, LOG_ERR);
1669
                    $this->error .= ($this->error ? ', ' . $errmsg : $errmsg);
1670
                }
1671
                $this->db->rollback();
1672
                return -$error;
1673
            }
1674
        } else {
1675
            $this->error = "ErrorRecordIsUsedCantDelete";
1676
            return 0;
1677
        }
1678
    }
1679
1680
    /**
1681
     * Get sell or eat by mandatory list
1682
     *
1683
     * @return  array   Sell or eat by mandatory list
1684
     */
1685
    public static function getSellOrEatByMandatoryList()
1686
    {
1687
        global $langs;
1688
1689
        $sellByLabel = $langs->trans('SellByDate');
1690
        $eatByLabel = $langs->trans('EatByDate');
1691
        return array(
1692
            self::SELL_OR_EAT_BY_MANDATORY_ID_NONE => $langs->trans('BatchSellOrEatByMandatoryNone'),
1693
            self::SELL_OR_EAT_BY_MANDATORY_ID_SELL_BY => $sellByLabel,
1694
            self::SELL_OR_EAT_BY_MANDATORY_ID_EAT_BY => $eatByLabel,
1695
            self::SELL_OR_EAT_BY_MANDATORY_ID_SELL_AND_EAT => $langs->trans('BatchSellOrEatByMandatoryAll', $sellByLabel, $eatByLabel),
1696
        );
1697
    }
1698
1699
    /**
1700
     * Get sell or eat by mandatory label
1701
     *
1702
     * @return  string  Sell or eat by mandatory label
1703
     */
1704
    public function getSellOrEatByMandatoryLabel()
1705
    {
1706
        $sellOrEatByMandatoryLabel = '';
1707
1708
        $sellOrEatByMandatoryList = self::getSellOrEatByMandatoryList();
1709
        if (isset($sellOrEatByMandatoryList[$this->sell_or_eat_by_mandatory])) {
1710
            $sellOrEatByMandatoryLabel = $sellOrEatByMandatoryList[$this->sell_or_eat_by_mandatory];
1711
        }
1712
1713
        return $sellOrEatByMandatoryLabel;
1714
    }
1715
1716
    /**
1717
     *    Update or add a translation for a product
1718
     *
1719
     * @param  User $user Object user making update
1720
     * @return int        Return integer <0 if KO, >0 if OK
1721
     */
1722
    public function setMultiLangs($user)
1723
    {
1724
        global $conf, $langs;
1725
1726
        $langs_available = $langs->get_available_languages(DOL_DOCUMENT_ROOT, 0, 2);
1727
        $current_lang = $langs->getDefaultLang();
1728
1729
        foreach ($langs_available as $key => $value) {
1730
            if ($key == $current_lang) {
1731
                $sql = "SELECT rowid";
1732
                $sql .= " FROM " . $this->db->prefix() . "product_lang";
1733
                $sql .= " WHERE fk_product = " . ((int) $this->id);
1734
                $sql .= " AND lang = '" . $this->db->escape($key) . "'";
1735
1736
                $result = $this->db->query($sql);
1737
1738
                if ($this->db->num_rows($result)) { // if there is already a description line for this language
1739
                    $sql2 = "UPDATE " . $this->db->prefix() . "product_lang";
1740
                    $sql2 .= " SET ";
1741
                    $sql2 .= " label='" . $this->db->escape($this->label) . "',";
1742
                    $sql2 .= " description='" . $this->db->escape($this->description) . "'";
1743
                    if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1744
                        $sql2 .= ", note='" . $this->db->escape($this->other) . "'";
1745
                    }
1746
                    $sql2 .= " WHERE fk_product = " . ((int) $this->id) . " AND lang = '" . $this->db->escape($key) . "'";
1747
                } else {
1748
                    $sql2 = "INSERT INTO " . $this->db->prefix() . "product_lang (fk_product, lang, label, description";
1749
                    if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1750
                        $sql2 .= ", note";
1751
                    }
1752
                    $sql2 .= ")";
1753
                    $sql2 .= " VALUES(" . ((int) $this->id) . ",'" . $this->db->escape($key) . "','" . $this->db->escape($this->label) . "',";
1754
                    $sql2 .= " '" . $this->db->escape($this->description) . "'";
1755
                    if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1756
                        $sql2 .= ", '" . $this->db->escape($this->other) . "'";
1757
                    }
1758
                    $sql2 .= ")";
1759
                }
1760
                dol_syslog(get_class($this) . '::setMultiLangs key = current_lang = ' . $key);
1761
                if (!$this->db->query($sql2)) {
1762
                    $this->error = $this->db->lasterror();
1763
                    return -1;
1764
                }
1765
            } elseif (isset($this->multilangs[$key])) {
1766
                if (empty($this->multilangs["$key"]["label"])) {
1767
                    $this->errors[] = $key . ' : ' . $langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("Label"));
1768
                    return -1;
1769
                }
1770
1771
                $sql = "SELECT rowid";
1772
                $sql .= " FROM " . $this->db->prefix() . "product_lang";
1773
                $sql .= " WHERE fk_product = " . ((int) $this->id);
1774
                $sql .= " AND lang = '" . $this->db->escape($key) . "'";
1775
1776
                $result = $this->db->query($sql);
1777
1778
                if ($this->db->num_rows($result)) { // if there is already a description line for this language
1779
                    $sql2 = "UPDATE " . $this->db->prefix() . "product_lang";
1780
                    $sql2 .= " SET ";
1781
                    $sql2 .= " label = '" . $this->db->escape($this->multilangs["$key"]["label"]) . "',";
1782
                    $sql2 .= " description = '" . $this->db->escape($this->multilangs["$key"]["description"]) . "'";
1783
                    if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1784
                        $sql2 .= ", note = '" . $this->db->escape($this->multilangs["$key"]["other"]) . "'";
1785
                    }
1786
                    $sql2 .= " WHERE fk_product = " . ((int) $this->id) . " AND lang = '" . $this->db->escape($key) . "'";
1787
                } else {
1788
                    $sql2 = "INSERT INTO " . $this->db->prefix() . "product_lang (fk_product, lang, label, description";
1789
                    if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1790
                        $sql2 .= ", note";
1791
                    }
1792
                    $sql2 .= ")";
1793
                    $sql2 .= " VALUES(" . ((int) $this->id) . ",'" . $this->db->escape($key) . "','" . $this->db->escape($this->multilangs["$key"]["label"]) . "',";
1794
                    $sql2 .= " '" . $this->db->escape($this->multilangs["$key"]["description"]) . "'";
1795
                    if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1796
                        $sql2 .= ", '" . $this->db->escape($this->multilangs["$key"]["other"]) . "'";
1797
                    }
1798
                    $sql2 .= ")";
1799
                }
1800
1801
                // We do not save if main fields are empty
1802
                if ($this->multilangs["$key"]["label"] || $this->multilangs["$key"]["description"]) {
1803
                    if (!$this->db->query($sql2)) {
1804
                        $this->error = $this->db->lasterror();
1805
                        return -1;
1806
                    }
1807
                }
1808
            } else {
1809
                // language is not current language and we didn't provide a multilang description for this language
1810
            }
1811
        }
1812
1813
        // Call trigger
1814
        $result = $this->call_trigger('PRODUCT_SET_MULTILANGS', $user);
1815
        if ($result < 0) {
1816
            $this->error = $this->db->lasterror();
1817
            return -1;
1818
        }
1819
        // End call triggers
1820
1821
        return 1;
1822
    }
1823
1824
    /**
1825
     *    Delete a language for this product
1826
     *
1827
     * @param string $langtodelete Language code to delete
1828
     * @param User   $user         Object user making delete
1829
     *
1830
     * @return int                            Return integer <0 if KO, >0 if OK
1831
     */
1832
    public function delMultiLangs($langtodelete, $user)
1833
    {
1834
        $sql = "DELETE FROM " . $this->db->prefix() . "product_lang";
1835
        $sql .= " WHERE fk_product = " . ((int) $this->id) . " AND lang = '" . $this->db->escape($langtodelete) . "'";
1836
1837
        dol_syslog(get_class($this) . '::delMultiLangs', LOG_DEBUG);
1838
        $result = $this->db->query($sql);
1839
        if ($result) {
1840
            // Call trigger
1841
            $result = $this->call_trigger('PRODUCT_DEL_MULTILANGS', $user);
1842
            if ($result < 0) {
1843
                $this->error = $this->db->lasterror();
1844
                dol_syslog(get_class($this) . '::delMultiLangs error=' . $this->error, LOG_ERR);
1845
                return -1;
1846
            }
1847
            // End call triggers
1848
            return 1;
1849
        } else {
1850
            $this->error = $this->db->lasterror();
1851
            dol_syslog(get_class($this) . '::delMultiLangs error=' . $this->error, LOG_ERR);
1852
            return -1;
1853
        }
1854
    }
1855
1856
    /**
1857
     * Sets an accountancy code for a product.
1858
     * Also calls PRODUCT_MODIFY trigger when modified
1859
     *
1860
     * @param   string $type    It can be 'buy', 'buy_intra', 'buy_export', 'sell', 'sell_intra' or 'sell_export'
1861
     * @param   string $value   Accountancy code
1862
     * @return  int             Return integer <0 KO >0 OK
1863
     */
1864
    public function setAccountancyCode($type, $value)
1865
    {
1866
        global $user, $langs, $conf;
1867
1868
        $error = 0;
1869
1870
        $this->db->begin();
1871
1872
        if ($type == 'buy') {
1873
            $field = 'accountancy_code_buy';
1874
        } elseif ($type == 'buy_intra') {
1875
            $field = 'accountancy_code_buy_intra';
1876
        } elseif ($type == 'buy_export') {
1877
            $field = 'accountancy_code_buy_export';
1878
        } elseif ($type == 'sell') {
1879
            $field = 'accountancy_code_sell';
1880
        } elseif ($type == 'sell_intra') {
1881
            $field = 'accountancy_code_sell_intra';
1882
        } elseif ($type == 'sell_export') {
1883
            $field = 'accountancy_code_sell_export';
1884
        } else {
1885
            return -1;
1886
        }
1887
1888
        $sql = "UPDATE " . $this->db->prefix() . $this->table_element . " SET ";
1889
        $sql .= "$field = '" . $this->db->escape($value) . "'";
1890
        $sql .= " WHERE rowid = " . ((int) $this->id);
1891
1892
        dol_syslog(__METHOD__, LOG_DEBUG);
1893
        $resql = $this->db->query($sql);
1894
1895
        if ($resql) {
1896
            // Call trigger
1897
            $result = $this->call_trigger('PRODUCT_MODIFY', $user);
1898
            if ($result < 0) {
1899
                $error++;
1900
            }
1901
            // End call triggers
1902
1903
            if ($error) {
1904
                $this->db->rollback();
1905
                return -1;
1906
            }
1907
1908
            $this->$field = $value;
1909
1910
            $this->db->commit();
1911
            return 1;
1912
        } else {
1913
            $this->error = $this->db->lasterror();
1914
            $this->db->rollback();
1915
            return -1;
1916
        }
1917
    }
1918
1919
    /**
1920
     *    Load array this->multilangs
1921
     *
1922
     * @return int        Return integer <0 if KO, >0 if OK
1923
     */
1924
    public function getMultiLangs()
1925
    {
1926
        global $langs;
1927
1928
        $current_lang = $langs->getDefaultLang();
1929
1930
        $sql = "SELECT lang, label, description, note as other";
1931
        $sql .= " FROM " . $this->db->prefix() . "product_lang";
1932
        $sql .= " WHERE fk_product = " . ((int) $this->id);
1933
1934
        $result = $this->db->query($sql);
1935
        if ($result) {
1936
            while ($obj = $this->db->fetch_object($result)) {
1937
                //print 'lang='.$obj->lang.' current='.$current_lang.'<br>';
1938
                if ($obj->lang == $current_lang) {  // si on a les traduct. dans la langue courante on les charge en infos principales.
1939
                    $this->label       = $obj->label;
1940
                    $this->description = $obj->description;
1941
                    $this->other       = $obj->other;
1942
                }
1943
                $this->multilangs[(string) $obj->lang]["label"]       = $obj->label;
1944
                $this->multilangs[(string) $obj->lang]["description"] = $obj->description;
1945
                $this->multilangs[(string) $obj->lang]["other"]       = $obj->other;
1946
            }
1947
            return 1;
1948
        } else {
1949
            $this->error = "Error: " . $this->db->lasterror() . " - " . $sql;
1950
            return -1;
1951
        }
1952
    }
1953
1954
    /**
1955
     *  used to check if price have really change to avoid log pollution
1956
     *
1957
     * @param  int  $level price level to change
1958
     * @return array
1959
     */
1960
    private function getArrayForPriceCompare($level = 0)
1961
    {
1962
        $testExit = array('multiprices','multiprices_ttc','multiprices_base_type','multiprices_min','multiprices_min_ttc','multiprices_tva_tx','multiprices_recuperableonly');
1963
1964
        foreach ($testExit as $field) {
1965
            if (!isset($this->$field)) {
1966
                return array();
1967
            }
1968
            $tmparray = $this->$field;
1969
            if (!isset($tmparray[$level])) {
1970
                return array();
1971
            }
1972
        }
1973
1974
        $lastPrice = array(
1975
            'level' => $level ? $level : 1,
1976
            'multiprices' => (float) $this->multiprices[$level],
1977
            'multiprices_ttc' => (float) $this->multiprices_ttc[$level],
1978
            'multiprices_base_type' => $this->multiprices_base_type[$level],
1979
            'multiprices_min' => (float) $this->multiprices_min[$level],
1980
            'multiprices_min_ttc' => (float) $this->multiprices_min_ttc[$level],
1981
            'multiprices_tva_tx' => (float) $this->multiprices_tva_tx[$level],
1982
            'multiprices_recuperableonly' => (float) $this->multiprices_recuperableonly[$level],
1983
        );
1984
1985
        return $lastPrice;
1986
    }
1987
1988
1989
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1990
    /**
1991
     *  Insert a track that we changed a customer price
1992
     *
1993
     * @param  User $user  User making change
1994
     * @param  int  $level price level to change
1995
     * @return int                    Return integer <0 if KO, >0 if OK
1996
     */
1997
    private function _log_price($user, $level = 0)
1998
    {
1999
		// phpcs:enable
2000
        global $conf;
2001
2002
        $now = dol_now();
2003
2004
        // Clean parameters
2005
        if (empty($this->price_by_qty)) {
2006
            $this->price_by_qty = 0;
2007
        }
2008
2009
        // Add new price
2010
        $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,";
2011
        $sql .= " localtax1_tx, localtax2_tx, localtax1_type, localtax2_type, price_min,price_min_ttc,price_by_qty,entity,fk_price_expression) ";
2012
        $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) . ",";
2013
        $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');
2014
        $sql .= ")";
2015
2016
        dol_syslog(get_class($this) . "::_log_price", LOG_DEBUG);
2017
        $resql = $this->db->query($sql);
2018
        if (!$resql) {
2019
            $this->error = $this->db->lasterror();
2020
            dol_print_error($this->db);
2021
            return -1;
2022
        } else {
2023
            return 1;
2024
        }
2025
    }
2026
2027
2028
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2029
    /**
2030
     *  Delete a price line
2031
     *
2032
     * @param  User $user  Object user
2033
     * @param  int  $rowid Line id to delete
2034
     * @return int                Return integer <0 if KO, >0 if OK
2035
     */
2036
    public function log_price_delete($user, $rowid)
2037
    {
2038
		// phpcs:enable
2039
        $sql = "DELETE FROM " . $this->db->prefix() . "product_price_by_qty";
2040
        $sql .= " WHERE fk_product_price = " . ((int) $rowid);
2041
        $resql = $this->db->query($sql);
2042
2043
        $sql = "DELETE FROM " . $this->db->prefix() . "product_price";
2044
        $sql .= " WHERE rowid=" . ((int) $rowid);
2045
        $resql = $this->db->query($sql);
2046
        if ($resql) {
2047
            return 1;
2048
        } else {
2049
            $this->error = $this->db->lasterror();
2050
            return -1;
2051
        }
2052
    }
2053
2054
2055
    /**
2056
     * Return price of sell of a product for a seller/buyer/product.
2057
     *
2058
     * @param   Societe     $thirdparty_seller      Seller
2059
     * @param   Societe     $thirdparty_buyer       Buyer
2060
     * @param   int         $pqp                    Id of product price per quantity if a selection was done of such a price
2061
     * @return  array                               Array of price information array('pu_ht'=> , 'pu_ttc'=> , 'tva_tx'=>'X.Y (code)', ...), 'tva_npr'=>0, ...)
2062
     * @see get_buyprice(), find_min_price_product_fournisseur()
2063
     */
2064
    public function getSellPrice($thirdparty_seller, $thirdparty_buyer, $pqp = 0)
2065
    {
2066
        global $conf, $hookmanager, $action;
2067
2068
        // Call hook if any
2069
        if (is_object($hookmanager)) {
2070
            $parameters = array('thirdparty_seller' => $thirdparty_seller, 'thirdparty_buyer' => $thirdparty_buyer, 'pqp' => $pqp);
2071
            // Note that $action and $object may have been modified by some hooks
2072
            $reshook = $hookmanager->executeHooks('getSellPrice', $parameters, $this, $action);
2073
            if ($reshook > 0) {
2074
                return $hookmanager->resArray;
2075
            }
2076
        }
2077
2078
        // Update if prices fields are defined
2079
        $tva_tx = get_default_tva($thirdparty_seller, $thirdparty_buyer, $this->id);
2080
        $tva_npr = get_default_npr($thirdparty_seller, $thirdparty_buyer, $this->id);
2081
        if (empty($tva_tx)) {
2082
            $tva_npr = 0;
2083
        }
2084
2085
        $pu_ht = $this->price;
2086
        $pu_ttc = $this->price_ttc;
2087
        $price_min = $this->price_min;
2088
        $price_base_type = $this->price_base_type;
2089
2090
        // If price per segment
2091
        if (getDolGlobalString('PRODUIT_MULTIPRICES') && !empty($thirdparty_buyer->price_level)) {
2092
            $pu_ht = $this->multiprices[$thirdparty_buyer->price_level];
2093
            $pu_ttc = $this->multiprices_ttc[$thirdparty_buyer->price_level];
2094
            $price_min = $this->multiprices_min[$thirdparty_buyer->price_level];
2095
            $price_base_type = $this->multiprices_base_type[$thirdparty_buyer->price_level];
2096
            if (getDolGlobalString('PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL')) {  // using this option is a bug. kept for backward compatibility
2097
                if (isset($this->multiprices_tva_tx[$thirdparty_buyer->price_level])) {
2098
                    $tva_tx = $this->multiprices_tva_tx[$thirdparty_buyer->price_level];
2099
                }
2100
                if (isset($this->multiprices_recuperableonly[$thirdparty_buyer->price_level])) {
2101
                    $tva_npr = $this->multiprices_recuperableonly[$thirdparty_buyer->price_level];
2102
                }
2103
                if (empty($tva_tx)) {
2104
                    $tva_npr = 0;
2105
                }
2106
            }
2107
        } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES')) {
2108
            // If price per customer
2109
            require_once constant('DOL_DOCUMENT_ROOT') . '/product/class/productcustomerprice.class.php';
2110
2111
            $prodcustprice = new ProductCustomerPrice($this->db);
2112
2113
            $filter = array('t.fk_product' => $this->id, 't.fk_soc' => $thirdparty_buyer->id);
2114
2115
            $result = $prodcustprice->fetchAll('', '', 0, 0, $filter);
2116
            if ($result) {
2117
                if (count($prodcustprice->lines) > 0) {
2118
                    $pu_ht = price($prodcustprice->lines[0]->price);
2119
                    $price_min = price($prodcustprice->lines[0]->price_min);
2120
                    $pu_ttc = price($prodcustprice->lines[0]->price_ttc);
2121
                    $price_base_type = $prodcustprice->lines[0]->price_base_type;
2122
                    $tva_tx = $prodcustprice->lines[0]->tva_tx;
2123
                    if ($prodcustprice->lines[0]->default_vat_code && !preg_match('/\(.*\)/', $tva_tx)) {
2124
                        $tva_tx .= ' (' . $prodcustprice->lines[0]->default_vat_code . ')';
2125
                    }
2126
                    $tva_npr = $prodcustprice->lines[0]->recuperableonly;
2127
                    if (empty($tva_tx)) {
2128
                        $tva_npr = 0;
2129
                    }
2130
                }
2131
            }
2132
        } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY')) {
2133
            // If price per quantity
2134
            if ($this->prices_by_qty[0]) {
2135
                // yes, this product has some prices per quantity
2136
                // Search price into product_price_by_qty from $this->id
2137
                foreach ($this->prices_by_qty_list[0] as $priceforthequantityarray) {
2138
                    if ($priceforthequantityarray['rowid'] != $pqp) {
2139
                        continue;
2140
                    }
2141
                    // We found the price
2142
                    if ($priceforthequantityarray['price_base_type'] == 'HT') {
2143
                        $pu_ht = $priceforthequantityarray['unitprice'];
2144
                    } else {
2145
                        $pu_ttc = $priceforthequantityarray['unitprice'];
2146
                    }
2147
                    break;
2148
                }
2149
            }
2150
        } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES')) {
2151
            // If price per quantity and customer
2152
            if ($this->prices_by_qty[$thirdparty_buyer->price_level]) {
2153
                // yes, this product has some prices per quantity
2154
                // Search price into product_price_by_qty from $this->id
2155
                foreach ($this->prices_by_qty_list[$thirdparty_buyer->price_level] as $priceforthequantityarray) {
2156
                    if ($priceforthequantityarray['rowid'] != $pqp) {
2157
                        continue;
2158
                    }
2159
                    // We found the price
2160
                    if ($priceforthequantityarray['price_base_type'] == 'HT') {
2161
                        $pu_ht = $priceforthequantityarray['unitprice'];
2162
                    } else {
2163
                        $pu_ttc = $priceforthequantityarray['unitprice'];
2164
                    }
2165
                    break;
2166
                }
2167
            }
2168
        }
2169
2170
        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);
2171
    }
2172
2173
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2174
    /**
2175
     * Read price used by a provider.
2176
     * We enter as input couple prodfournprice/qty or triplet qty/product_id/fourn_ref.
2177
     * This also set some properties on product like ->buyprice, ->fourn_pu, ...
2178
     *
2179
     * @param  int    $prodfournprice Id du tarif = rowid table product_fournisseur_price
2180
     * @param  double $qty            Quantity asked or -1 to get first entry found
2181
     * @param  int    $product_id     Filter on a particular product id
2182
     * @param  string $fourn_ref      Filter on a supplier price ref. 'none' to exclude ref in search.
2183
     * @param  int    $fk_soc         If of supplier
2184
     * @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
2185
     * @see getSellPrice(), find_min_price_product_fournisseur()
2186
     */
2187
    public function get_buyprice($prodfournprice, $qty, $product_id = 0, $fourn_ref = '', $fk_soc = 0)
2188
    {
2189
		// phpcs:enable
2190
        global $action, $hookmanager;
2191
2192
        // Call hook if any
2193
        if (is_object($hookmanager)) {
2194
            $parameters = array(
2195
                'prodfournprice' => $prodfournprice,
2196
                'qty' => $qty,
2197
                'product_id' => $product_id,
2198
                'fourn_ref' => $fourn_ref,
2199
                'fk_soc' => $fk_soc,
2200
            );
2201
            // Note that $action and $object may have been modified by some hooks
2202
            $reshook = $hookmanager->executeHooks('getBuyPrice', $parameters, $this, $action);
2203
            if ($reshook > 0) {
2204
                return $hookmanager->resArray;
2205
            }
2206
        }
2207
2208
        $result = 0;
2209
2210
        // 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)
2211
        $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2212
        $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,";
2213
        $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2214
        $sql .= " pfp.packaging";
2215
        $sql .= " FROM " . $this->db->prefix() . "product_fournisseur_price as pfp";
2216
        $sql .= " WHERE pfp.rowid = " . ((int) $prodfournprice);
2217
        if ($qty > 0) {
2218
            $sql .= " AND pfp.quantity <= " . ((float) $qty);
2219
        }
2220
        $sql .= " ORDER BY pfp.quantity DESC";
2221
2222
        dol_syslog(get_class($this) . "::get_buyprice first search by prodfournprice/qty", LOG_DEBUG);
2223
        $resql = $this->db->query($sql);
2224
        if ($resql) {
2225
            $obj = $this->db->fetch_object($resql);
2226
            if ($obj && $obj->quantity > 0) {        // If we found a supplier prices from the id of supplier price
2227
                if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2228
                    $prod_supplier = new ProductFournisseur($this->db);
2229
                    $prod_supplier->product_fourn_price_id = $obj->rowid;
2230
                    $prod_supplier->id = $obj->fk_product;
2231
                    $prod_supplier->fourn_qty = $obj->quantity;
2232
                    $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2233
                    $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2234
2235
                    include_once DOL_DOCUMENT_ROOT . '/product/dynamic_price/class/price_parser.class.php';
2236
                    $priceparser = new PriceParser($this->db);
2237
                    $price_result = $priceparser->parseProductSupplier($prod_supplier);
2238
                    if ($price_result >= 0) {
2239
                        $obj->price = $price_result;
2240
                    }
2241
                }
2242
                $this->product_fourn_price_id = $obj->rowid;
2243
                $this->buyprice = $obj->price; // deprecated
2244
                $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product of supplier
2245
                $this->fourn_price_base_type = 'HT'; // Price base type
2246
                $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2247
                $this->ref_fourn = $obj->ref_supplier; // deprecated
2248
                $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2249
                $this->desc_supplier = $obj->desc_supplier; // desc supplier
2250
                $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2251
                $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2252
                $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2253
                $this->fourn_multicurrency_price = $obj->multicurrency_price;
2254
                $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2255
                $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2256
                $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2257
                $this->fourn_multicurrency_code = $obj->multicurrency_code;
2258
                if (getDolGlobalString('PRODUCT_USE_SUPPLIER_PACKAGING')) {
2259
                    $this->packaging = $obj->packaging;
2260
                }
2261
                $result = $obj->fk_product;
2262
                return $result;
2263
            } else { // If not found
2264
                // 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.
2265
                $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2266
                $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,";
2267
                $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2268
                $sql .= " pfp.packaging";
2269
                $sql .= " FROM " . $this->db->prefix() . "product_fournisseur_price as pfp";
2270
                $sql .= " WHERE 1 = 1";
2271
                if ($product_id > 0) {
2272
                    $sql .= " AND pfp.fk_product = " . ((int) $product_id);
2273
                }
2274
                if ($fourn_ref != 'none') {
2275
                    $sql .= " AND pfp.ref_fourn = '" . $this->db->escape($fourn_ref) . "'";
2276
                }
2277
                if ($fk_soc > 0) {
2278
                    $sql .= " AND pfp.fk_soc = " . ((int) $fk_soc);
2279
                }
2280
                if ($qty > 0) {
2281
                    $sql .= " AND pfp.quantity <= " . ((float) $qty);
2282
                }
2283
                $sql .= " ORDER BY pfp.quantity DESC";
2284
                $sql .= " LIMIT 1";
2285
2286
                dol_syslog(get_class($this) . "::get_buyprice second search from qty/ref/product_id", LOG_DEBUG);
2287
                $resql = $this->db->query($sql);
2288
                if ($resql) {
2289
                    $obj = $this->db->fetch_object($resql);
2290
                    if ($obj && $obj->quantity > 0) {        // If found
2291
                        if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2292
                            $prod_supplier = new ProductFournisseur($this->db);
2293
                            $prod_supplier->product_fourn_price_id = $obj->rowid;
2294
                            $prod_supplier->id = $obj->fk_product;
2295
                            $prod_supplier->fourn_qty = $obj->quantity;
2296
                            $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2297
                            $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2298
2299
                            include_once DOL_DOCUMENT_ROOT . '/product/dynamic_price/class/price_parser.class.php';
2300
                            $priceparser = new PriceParser($this->db);
2301
                            $price_result = $priceparser->parseProductSupplier($prod_supplier);
2302
                            if ($result >= 0) {
2303
                                $obj->price = $price_result;
2304
                            }
2305
                        }
2306
                        $this->product_fourn_price_id = $obj->rowid;
2307
                        $this->buyprice = $obj->price; // deprecated
2308
                        $this->fourn_qty = $obj->quantity; // min quantity for price for a virtual supplier
2309
                        $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product for a virtual supplier
2310
                        $this->fourn_price_base_type = 'HT'; // Price base type for a virtual supplier
2311
                        $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2312
                        $this->ref_fourn = $obj->ref_supplier; // deprecated
2313
                        $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2314
                        $this->desc_supplier = $obj->desc_supplier; // desc supplier
2315
                        $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2316
                        $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2317
                        $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2318
                        $this->fourn_multicurrency_price = $obj->multicurrency_price;
2319
                        $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2320
                        $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2321
                        $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2322
                        $this->fourn_multicurrency_code = $obj->multicurrency_code;
2323
                        if (getDolGlobalString('PRODUCT_USE_SUPPLIER_PACKAGING')) {
2324
                            $this->packaging = $obj->packaging;
2325
                        }
2326
                        $result = $obj->fk_product;
2327
                        return $result;
2328
                    } else {
2329
                        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é.
2330
                    }
2331
                } else {
2332
                    $this->error = $this->db->lasterror();
2333
                    return -3;
2334
                }
2335
            }
2336
        } else {
2337
            $this->error = $this->db->lasterror();
2338
            return -2;
2339
        }
2340
    }
2341
2342
2343
    /**
2344
     * Modify customer price of a product/Service for a given level
2345
     *
2346
     * @param  double $newprice          New price
2347
     * @param  string $newpricebase      HT or TTC
2348
     * @param  User   $user              Object user that make change
2349
     * @param  ?double $newvat           New VAT Rate (For example 8.5. Should not be a string)
2350
     * @param  double $newminprice       New price min
2351
     * @param  int    $level             0=standard, >0 = level if multilevel prices
2352
     * @param  int    $newnpr            0=Standard vat rate, 1=Special vat rate for French NPR VAT
2353
     * @param  int    $newpbq            1 if it has price by quantity
2354
     * @param  int    $ignore_autogen    Used to avoid infinite loops
2355
     * @param  array  $localtaxes_array  Array with localtaxes info array('0'=>type1,'1'=>rate1,'2'=>type2,'3'=>rate2) (loaded by getLocalTaxesFromRate(vatrate, 0, ...) function).
2356
     * @param  string $newdefaultvatcode Default vat code
2357
     * @param  string $price_label       Price Label
2358
     * @param  int    $notrigger         Disable triggers
2359
     * @return int                            Return integer <0 if KO, >0 if OK
2360
     */
2361
    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)
2362
    {
2363
        global $conf, $langs;
2364
2365
        $lastPriceData = $this->getArrayForPriceCompare($level); // temporary store current price before update
2366
2367
        $id = $this->id;
2368
2369
        dol_syslog(get_class($this) . "::update_price id=" . $id . " newprice=" . $newprice . " newpricebase=" . $newpricebase . " newminprice=" . $newminprice . " level=" . $level . " npr=" . $newnpr . " newdefaultvatcode=" . $newdefaultvatcode);
2370
2371
        // Clean parameters
2372
        if (empty($this->tva_tx)) {
2373
            $this->tva_tx = 0;
2374
        }
2375
        if (empty($newnpr)) {
2376
            $newnpr = 0;
2377
        }
2378
        if (empty($newminprice)) {
2379
            $newminprice = 0;
2380
        }
2381
2382
        // Check parameters
2383
        if ($newvat === null || $newvat == '') {  // Maintain '' for backwards compatibility
2384
            $newvat = $this->tva_tx;
2385
        }
2386
2387
        // If multiprices are enabled, then we check if the current product is subject to price autogeneration
2388
        // Price will be modified ONLY when the first one is the one that is being modified
2389
        if ((getDolGlobalString('PRODUIT_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES')) && !$ignore_autogen && $this->price_autogen && ($level == 1)) {
2390
            return $this->generateMultiprices($user, $newprice, $newpricebase, $newvat, $newnpr, $newpbq);
2391
        }
2392
2393
        if (!empty($newminprice) && ($newminprice > $newprice)) {
2394
            $this->error = 'ErrorPriceCantBeLowerThanMinPrice';
2395
            return -1;
2396
        }
2397
2398
        if ($newprice !== '' || $newprice === 0) {
2399
            if ($newpricebase == 'TTC') {
2400
                $price_ttc = price2num($newprice, 'MU');
2401
                $price = (float) price2num($newprice) / (1 + ((float) $newvat / 100));
2402
                $price = price2num($price, 'MU');
2403
2404
                if ($newminprice != '' || $newminprice == 0) {
2405
                    $price_min_ttc = price2num($newminprice, 'MU');
2406
                    $price_min = (float) price2num($newminprice) / (1 + ($newvat / 100));
2407
                    $price_min = price2num($price_min, 'MU');
2408
                } else {
2409
                    $price_min = 0;
2410
                    $price_min_ttc = 0;
2411
                }
2412
            } else {
2413
                $price = (float) price2num($newprice, 'MU');
2414
                $price_ttc = ($newnpr != 1) ? price2num($newprice) * (1 + ($newvat / 100)) : $price;
2415
                $price_ttc = (float) price2num($price_ttc, 'MU');
2416
2417
                if ($newminprice !== '' || $newminprice === 0) {
2418
                    $price_min = price2num($newminprice, 'MU');
2419
                    $price_min_ttc = (float) price2num($newminprice) * (1 + ($newvat / 100));
2420
                    $price_min_ttc = price2num($price_min_ttc, 'MU');
2421
                    //print 'X'.$newminprice.'-'.$price_min;
2422
                } else {
2423
                    $price_min = 0;
2424
                    $price_min_ttc = 0;
2425
                }
2426
            }
2427
            //print 'x'.$id.'-'.$newprice.'-'.$newpricebase.'-'.$price.'-'.$price_ttc.'-'.$price_min.'-'.$price_min_ttc;
2428
2429
            if (count($localtaxes_array) > 0) {
2430
                $localtaxtype1 = $localtaxes_array['0'];
2431
                $localtax1 = $localtaxes_array['1'];
2432
                $localtaxtype2 = $localtaxes_array['2'];
2433
                $localtax2 = $localtaxes_array['3'];
2434
            } else {
2435
                // if array empty, we try to use the vat code
2436
                if (!empty($newdefaultvatcode)) {
2437
                    global $mysoc;
2438
                    // Get record from code
2439
                    $sql = "SELECT t.rowid, t.code, t.recuperableonly as tva_npr, t.localtax1, t.localtax2, t.localtax1_type, t.localtax2_type";
2440
                    $sql .= " FROM " . MAIN_DB_PREFIX . "c_tva as t, " . MAIN_DB_PREFIX . "c_country as c";
2441
                    $sql .= " WHERE t.fk_pays = c.rowid AND c.code = '" . $this->db->escape($mysoc->country_code) . "'";
2442
                    $sql .= " AND t.taux = " . ((float) $newdefaultvatcode) . " AND t.active = 1";
2443
                    $sql .= " AND t.code = '" . $this->db->escape($newdefaultvatcode) . "'";
2444
                    $resql = $this->db->query($sql);
2445
                    if ($resql) {
2446
                        $obj = $this->db->fetch_object($resql);
2447
                        if ($obj) {
2448
                            $npr = $obj->tva_npr;
2449
                            $localtax1 = $obj->localtax1;
2450
                            $localtax2 = $obj->localtax2;
2451
                            $localtaxtype1 = $obj->localtax1_type;
2452
                            $localtaxtype2 = $obj->localtax2_type;
2453
                        }
2454
                    }
2455
                } else {
2456
                    // old method. deprecated because we can't retrieve type
2457
                    $localtaxtype1 = '0';
2458
                    $localtax1 = get_localtax($newvat, 1);
2459
                    $localtaxtype2 = '0';
2460
                    $localtax2 = get_localtax($newvat, 2);
2461
                }
2462
            }
2463
            if (empty($localtax1)) {
2464
                $localtax1 = 0; // If = '' then = 0
2465
            }
2466
            if (empty($localtax2)) {
2467
                $localtax2 = 0; // If = '' then = 0
2468
            }
2469
2470
            $this->db->begin();
2471
2472
            // Ne pas mettre de quote sur les numeriques decimaux.
2473
            // Ceci provoque des stockages avec arrondis en base au lieu des valeurs exactes.
2474
            $sql = "UPDATE " . $this->db->prefix() . "product SET";
2475
            $sql .= " price_base_type = '" . $this->db->escape($newpricebase) . "',";
2476
            $sql .= " price = " . (float) $price . ",";
2477
            $sql .= " price_ttc = " . (float) $price_ttc . ",";
2478
            $sql .= " price_min = " . (float) $price_min . ",";
2479
            $sql .= " price_min_ttc = " . (float) $price_min_ttc . ",";
2480
            $sql .= " localtax1_tx = " . ($localtax1 >= 0 ? (float) $localtax1 : 'NULL') . ",";
2481
            $sql .= " localtax2_tx = " . ($localtax2 >= 0 ? (float) $localtax2 : 'NULL') . ",";
2482
            $sql .= " localtax1_type = " . ($localtaxtype1 != '' ? "'" . $this->db->escape($localtaxtype1) . "'" : "'0'") . ",";
2483
            $sql .= " localtax2_type = " . ($localtaxtype2 != '' ? "'" . $this->db->escape($localtaxtype2) . "'" : "'0'") . ",";
2484
            $sql .= " default_vat_code = " . ($newdefaultvatcode ? "'" . $this->db->escape($newdefaultvatcode) . "'" : "null") . ",";
2485
            $sql .= " price_label = " . (!empty($price_label) ? "'" . $this->db->escape($price_label) . "'" : "null") . ",";
2486
            $sql .= " tva_tx = " . (float) price2num($newvat) . ",";
2487
            $sql .= " recuperableonly = '" . $this->db->escape($newnpr) . "'";
2488
            $sql .= " WHERE rowid = " . ((int) $id);
2489
2490
            dol_syslog(get_class($this) . "::update_price", LOG_DEBUG);
2491
            $resql = $this->db->query($sql);
2492
            if ($resql) {
2493
                $this->multiprices[$level] = $price;
2494
                $this->multiprices_ttc[$level] = $price_ttc;
2495
                $this->multiprices_min[$level] = $price_min;
2496
                $this->multiprices_min_ttc[$level] = $price_min_ttc;
2497
                $this->multiprices_base_type[$level] = $newpricebase;
2498
                $this->multiprices_default_vat_code[$level] = $newdefaultvatcode;
2499
                $this->multiprices_tva_tx[$level] = $newvat;
2500
                $this->multiprices_recuperableonly[$level] = $newnpr;
2501
2502
                $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...
2503
                $this->price_label = $price_label;
2504
                $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...
2505
                $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...
2506
                $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...
2507
                $this->price_base_type = $newpricebase;
2508
                $this->default_vat_code = $newdefaultvatcode;
2509
                $this->tva_tx = $newvat;
2510
                $this->tva_npr = $newnpr;
2511
2512
                //Local taxes
2513
                $this->localtax1_tx = $localtax1;
2514
                $this->localtax2_tx = $localtax2;
2515
                $this->localtax1_type = $localtaxtype1;
2516
                $this->localtax2_type = $localtaxtype2;
2517
2518
                // Price by quantity
2519
                $this->price_by_qty = $newpbq;
2520
2521
                // check if price have really change before log
2522
                $newPriceData = $this->getArrayForPriceCompare($level);
2523
                if (!empty(array_diff_assoc($newPriceData, $lastPriceData)) || !getDolGlobalString('PRODUIT_MULTIPRICES')) {
2524
                    $this->_log_price($user, $level); // Save price for level into table product_price
2525
                }
2526
2527
                $this->level = $level; // Store level of price edited for trigger
2528
2529
                // Call trigger
2530
                if (!$notrigger) {
2531
                    $result = $this->call_trigger('PRODUCT_PRICE_MODIFY', $user);
2532
                    if ($result < 0) {
2533
                        $this->db->rollback();
2534
                        return -1;
2535
                    }
2536
                }
2537
                // End call triggers
2538
2539
                $this->db->commit();
2540
            } else {
2541
                $this->db->rollback();
2542
                $this->error = $this->db->lasterror();
2543
                return -1;
2544
            }
2545
        }
2546
2547
        return 1;
2548
    }
2549
2550
    /**
2551
     *  Sets the supplier price expression
2552
     *
2553
     * @param      int $expression_id Expression
2554
     * @return     int                     Return integer <0 if KO, >0 if OK
2555
     * @deprecated Use Product::update instead
2556
     */
2557
    public function setPriceExpression($expression_id)
2558
    {
2559
        global $user;
2560
2561
        $this->fk_price_expression = $expression_id;
2562
2563
        return $this->update($this->id, $user);
2564
    }
2565
2566
    /**
2567
     *  Load a product in memory from database
2568
     *
2569
     * @param  int    $id                Id of product/service to load
2570
     * @param  string $ref               Ref of product/service to load
2571
     * @param  string $ref_ext           Ref ext of product/service to load
2572
     * @param  string $barcode           Barcode of product/service to load
2573
     * @param  int    $ignore_expression When module dynamicprices is on, ignores the math expression for calculating price and uses the db value instead
2574
     * @param  int    $ignore_price_load Load product without loading $this->multiprices... array (when we are sure we don't need them)
2575
     * @param  int    $ignore_lang_load  Load product without loading $this->multilangs language arrays (when we are sure we don't need them)
2576
     * @return int                       Return integer <0 if KO, 0 if not found, >0 if OK
2577
     */
2578
    public function fetch($id = 0, $ref = '', $ref_ext = '', $barcode = '', $ignore_expression = 0, $ignore_price_load = 0, $ignore_lang_load = 0)
2579
    {
2580
        include_once DOL_DOCUMENT_ROOT . '/core/lib/company.lib.php';
2581
2582
        global $langs, $conf;
2583
2584
        dol_syslog(get_class($this) . "::fetch id=" . $id . " ref=" . $ref . " ref_ext=" . $ref_ext);
2585
2586
        // Check parameters
2587
        if (!$id && !$ref && !$ref_ext && !$barcode) {
2588
            $this->error = 'ErrorWrongParameters';
2589
            dol_syslog(get_class($this) . "::fetch " . $this->error, LOG_ERR);
2590
            return -1;
2591
        }
2592
2593
        $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,";
2594
        $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,";
2595
        $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,";
2596
        $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units, p.last_main_doc,";
2597
        $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,";
2598
        if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
2599
            $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,";
2600
        } else {
2601
            $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,";
2602
        }
2603
2604
        //For MultiCompany
2605
        //PMP per entity & Stocks Sharings stock_reel includes only stocks shared with this entity
2606
        $separatedEntityPMP = false;    // Set to true to get the AWP from table llx_product_perentity instead of field 'pmp' into llx_product.
2607
        $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.
2608
        $visibleWarehousesEntities = $conf->entity;
2609
        if (getDolGlobalString('MULTICOMPANY_PRODUCT_SHARING_ENABLED')) {
2610
            if (getDolGlobalString('MULTICOMPANY_PMP_PER_ENTITY_ENABLED')) {
2611
                $checkPMPPerEntity = $this->db->query("SELECT pmp FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " . ((int) $id) . " AND entity = " . (int) $conf->entity);
2612
                if ($this->db->num_rows($checkPMPPerEntity) > 0) {
2613
                    $separatedEntityPMP = true;
2614
                }
2615
            }
2616
            global $mc;
2617
            $separatedStock = true;
2618
            if (isset($mc->sharings['stock']) && !empty($mc->sharings['stock'])) {
2619
                $visibleWarehousesEntities .= "," . implode(",", $mc->sharings['stock']);
2620
            }
2621
        }
2622
        if ($separatedEntityPMP) {
2623
            $sql .= " ppe.pmp,";
2624
        } else {
2625
            $sql .= " p.pmp,";
2626
        }
2627
        $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,";
2628
        $sql .= " p.fk_price_expression, p.price_autogen, p.model_pdf,";
2629
        $sql .= " p.price_label,";
2630
        if ($separatedStock) {
2631
            $sql .= " SUM(sp.reel) as stock";
2632
        } else {
2633
            $sql .= " p.stock";
2634
        }
2635
        $sql .= " FROM " . $this->db->prefix() . "product as p";
2636
        if (getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED') || $separatedEntityPMP) {
2637
            $sql .= " LEFT JOIN " . $this->db->prefix() . "product_perentity as ppe ON ppe.fk_product = p.rowid AND ppe.entity = " . ((int) $conf->entity);
2638
        }
2639
        if ($separatedStock) {
2640
            $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) . "))";
2641
        }
2642
2643
        if ($id) {
2644
            $sql .= " WHERE p.rowid = " . ((int) $id);
2645
        } else {
2646
            $sql .= " WHERE p.entity IN (" . getEntity($this->element) . ")";
2647
            if ($ref) {
2648
                $sql .= " AND p.ref = '" . $this->db->escape($ref) . "'";
2649
            } elseif ($ref_ext) {
2650
                $sql .= " AND p.ref_ext = '" . $this->db->escape($ref_ext) . "'";
2651
            } elseif ($barcode) {
2652
                $sql .= " AND p.barcode = '" . $this->db->escape($barcode) . "'";
2653
            }
2654
        }
2655
        if ($separatedStock) {
2656
            $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,";
2657
            $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,";
2658
            $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,";
2659
            $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units,";
2660
            $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,";
2661
            if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
2662
                $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,";
2663
            } else {
2664
                $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,";
2665
            }
2666
            if ($separatedEntityPMP) {
2667
                $sql .= " ppe.pmp,";
2668
            } else {
2669
                $sql .= " p.pmp,";
2670
            }
2671
            $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,";
2672
            $sql .= " p.fk_price_expression, p.price_autogen, p.model_pdf";
2673
            $sql .= " ,p.price_label";
2674
            if (!$separatedStock) {
2675
                $sql .= ", p.stock";
2676
            }
2677
        }
2678
2679
        $resql = $this->db->query($sql);
2680
        if ($resql) {
2681
            unset($this->oldcopy);
2682
2683
            if ($this->db->num_rows($resql) > 0) {
2684
                $obj = $this->db->fetch_object($resql);
2685
2686
                $this->id = $obj->rowid;
2687
                $this->ref = $obj->ref;
2688
                $this->ref_ext = $obj->ref_ext;
2689
                $this->label = $obj->label;
2690
                $this->description = $obj->description;
2691
                $this->url = $obj->url;
2692
                $this->note_public = $obj->note_public;
2693
                $this->note_private = $obj->note_private;
2694
                $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

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