Passed
Branch develop (356b3a)
by
unknown
98:06
created

Product::updatePrice()   F

Complexity

Conditions 36
Paths > 20000

Size

Total Lines 185
Code Lines 126

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 36
eloc 126
nc 376416
nop 11
dl 0
loc 185
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
/* Copyright (C) 2001-2007  Rodolphe Quiedeville    <[email protected]>
3
 * Copyright (C) 2004-2014	Laurent Destailleur		<[email protected]>
4
 * Copyright (C) 2005-2015	Regis Houssin			<[email protected]>
5
 * Copyright (C) 2006		Andre Cianfarani		<[email protected]>
6
 * Copyright (C) 2007-2011	Jean Heimburger			<[email protected]>
7
 * Copyright (C) 2010-2018	Juanjo Menent			<[email protected]>
8
 * Copyright (C) 2012       Cedric Salvador         <[email protected]>
9
 * Copyright (C) 2013-2014	Cedric GROSS			<[email protected]>
10
 * Copyright (C) 2013-2016	Marcos García			<[email protected]>
11
 * Copyright (C) 2011-2021	Open-DSI				<[email protected]>
12
 * Copyright (C) 2014		Henry Florian			<[email protected]>
13
 * Copyright (C) 2014-2016	Philippe Grand			<[email protected]>
14
 * Copyright (C) 2014		Ion agorria			    <[email protected]>
15
 * Copyright (C) 2016-2018	Ferran Marcet			<[email protected]>
16
 * Copyright (C) 2017		Gustavo Novaro
17
 * Copyright (C) 2019-2023  Frédéric France         <[email protected]>
18
 *
19
 * This program is free software; you can redistribute it and/or modify
20
 * it under the terms of the GNU General Public License as published by
21
 * the Free Software Foundation; either version 3 of the License, or
22
 * (at your option) any later version.
23
 *
24
 * This program is distributed in the hope that it will be useful,
25
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
26
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
27
 * GNU General Public License for more details.
28
 *
29
 * You should have received a copy of the GNU General Public License
30
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
31
 */
32
33
/**
34
 *    \file       htdocs/product/class/product.class.php
35
 *    \ingroup    produit
36
 *    \brief      File of class to manage predefined products or services
37
 */
38
require_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
39
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
40
require_once DOL_DOCUMENT_ROOT.'/product/class/productbatch.class.php';
41
require_once DOL_DOCUMENT_ROOT.'/product/stock/class/entrepot.class.php';
42
43
/**
44
 * Class to manage products or services
45
 */
46
class Product extends CommonObject
47
{
48
	/**
49
	 * @var string ID to identify managed object
50
	 */
51
	public $element = 'product';
52
53
	/**
54
	 * @var string Name of table without prefix where object is stored
55
	 */
56
	public $table_element = 'product';
57
58
	/**
59
	 * @var string Field with ID of parent key if this field has a parent
60
	 */
61
	public $fk_element = 'fk_product';
62
63
	/**
64
	 * @var array	List of child tables. To test if we can delete object.
65
	 */
66
	protected $childtables = array(
67
		'supplier_proposaldet' => array('name' => 'SupplierProposal', 'parent' => 'supplier_proposal', 'parentkey' => 'fk_supplier_proposal'),
68
		'propaldet' => array('name' => 'Proposal', 'parent' => 'propal', 'parentkey' => 'fk_propal'),
69
		'commandedet' => array('name' => 'Order', 'parent' => 'commande', 'parentkey' => 'fk_commande'),
70
		'facturedet' => array('name' => 'Invoice', 'parent' => 'facture', 'parentkey' => 'fk_facture'),
71
		'contratdet' => array('name' => 'Contract', 'parent' => 'contrat', 'parentkey' => 'fk_contrat'),
72
		'facture_fourn_det' => array('name' => 'SupplierInvoice', 'parent' => 'facture_fourn', 'parentkey' => 'fk_facture_fourn'),
73
		'commande_fournisseurdet' => array('name' => 'SupplierOrder', 'parent' => 'commande_fournisseur', 'parentkey' => 'fk_commande')
74
	);
75
76
	/**
77
	 * 0=No test on entity, 1=Test with field entity, 2=Test with link by societe
78
	 *
79
	 * @var int
80
	 */
81
	public $ismultientitymanaged = 1;
82
83
	/**
84
	 * @var string picto
85
	 */
86
	public $picto = 'product';
87
88
	/**
89
	 * {@inheritdoc}
90
	 */
91
	protected $table_ref_field = 'ref';
92
93
	public $regeximgext = '\.gif|\.jpg|\.jpeg|\.png|\.bmp|\.webp|\.xpm|\.xbm'; // See also into images.lib.php
94
95
	/**
96
	 * @deprecated
97
	 * @see $label
98
	 */
99
	public $libelle;
100
101
	/**
102
	 * Product label
103
	 *
104
	 * @var string
105
	 */
106
	public $label;
107
108
	/**
109
	 * Product description
110
	 *
111
	 * @var string
112
	 */
113
	public $description;
114
115
	/**
116
	 * Product other fields PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION
117
	 *
118
	 * @var string
119
	 */
120
	public $other;
121
122
	/**
123
	 * Check TYPE constants
124
	 *
125
	 * @var int
126
	 */
127
	public $type = self::TYPE_PRODUCT;
128
129
	/**
130
	 * Selling price without tax
131
	 *
132
	 * @var float
133
	 */
134
	public $price;
135
136
	public $price_formated;			// used by takepos/ajax/ajax.php
137
138
	/**
139
	 * Selling price with tax
140
	 *
141
	 * @var float
142
	 */
143
	public $price_ttc;
144
145
	public $price_ttc_formated;		// used by takepos/ajax/ajax.php
146
147
	/**
148
	 * Minimum price net
149
	 *
150
	 * @var float
151
	 */
152
	public $price_min;
153
154
	/**
155
	 * Minimum price with tax
156
	 *
157
	 * @var float
158
	 */
159
	public $price_min_ttc;
160
161
	/**
162
	 * Base price ('TTC' for price including tax or 'HT' for net price)
163
	 * @var string
164
	 */
165
	public $price_base_type;
166
167
	//! Arrays for multiprices
168
	public $multiprices = array();
169
	public $multiprices_ttc = array();
170
	public $multiprices_base_type = array();
171
	public $multiprices_min = array();
172
	public $multiprices_min_ttc = array();
173
	public $multiprices_tva_tx = array();
174
	public $multiprices_recuperableonly = array();
175
176
	//! Price by quantity arrays
177
	public $price_by_qty;
178
	public $prices_by_qty = array();
179
	public $prices_by_qty_id = array();
180
	public $prices_by_qty_list = array();
181
182
	//! Array for multilangs
183
	public $multilangs = array();
184
185
	//! Default VAT code for product (link to code into llx_c_tva but without foreign keys)
186
	public $default_vat_code;
187
188
	//! Default VAT rate of product
189
	public $tva_tx;
190
191
	//! French VAT NPR (0 or 1)
192
	public $tva_npr = 0;
193
194
	//! Default discount percent
195
	public $remise_percent;
196
197
	//! Other local taxes
198
	public $localtax1_tx;
199
	public $localtax2_tx;
200
	public $localtax1_type;
201
	public $localtax2_type;
202
203
	// Properties set by get_buyprice() for return
204
205
	public $desc_supplier;
206
	public $vatrate_supplier;
207
	public $default_vat_code_supplier;
208
	public $fourn_multicurrency_price;
209
	public $fourn_multicurrency_unitprice;
210
	public $fourn_multicurrency_tx;
211
	public $fourn_multicurrency_id;
212
	public $fourn_multicurrency_code;
213
	public $packaging;
214
215
216
	public $lifetime;
217
218
	public $qc_frequency;
219
220
	/**
221
	 * Stock real
222
	 *
223
	 * @var int
224
	 */
225
	public $stock_reel = 0;
226
227
	/**
228
	 * Stock virtual
229
	 *
230
	 * @var int
231
	 */
232
	public $stock_theorique;
233
234
	/**
235
	 * Cost price
236
	 *
237
	 * @var float
238
	 */
239
	public $cost_price;
240
241
	//! Average price value for product entry into stock (PMP)
242
	public $pmp;
243
244
	/**
245
	 * Stock alert
246
	 *
247
	 * @var float
248
	 */
249
	public $seuil_stock_alerte = 0;
250
251
	/**
252
	 * Ask for replenishment when $desiredstock < $stock_reel
253
	 */
254
	public $desiredstock = 0;
255
256
	/*
257
	 * Service expiration
258
	 */
259
	public $duration_value;
260
261
	/*
262
	 * Service Workstation
263
	 */
264
	public $fk_default_workstation;
265
266
	/**
267
	 * Exoiration unit
268
	 */
269
	public $duration_unit;
270
271
	/**
272
	 * Status indicates whether the product is on sale '1' or not '0'
273
	 *
274
	 * @var int
275
	 */
276
	public $status = 0;
277
278
	/**
279
	 * Status indicates whether the product is on sale '1' or not '0'
280
	 * @var int
281
	 * @deprecated
282
	 * @see $status
283
	 */
284
	public $tosell;
285
286
	/**
287
	 * Status indicate whether the product is available for purchase '1' or not '0'
288
	 *
289
	 * @var int
290
	 */
291
	public $status_buy = 0;
292
293
	/**
294
	 * Status indicate whether the product is available for purchase '1' or not '0'
295
	 * @var int
296
	 * @deprecated
297
	 * @see $status_buy
298
	 */
299
	public $tobuy;
300
301
	/**
302
	 * Status indicates whether the product is a finished product '1' or a raw material '0'
303
	 *
304
	 * @var int
305
	 */
306
	public $finished;
307
308
		/**
309
	 * fk_default_bom indicates the default bom
310
	 *
311
	 * @var int
312
	 */
313
	public $fk_default_bom;
314
315
	/**
316
	 * We must manage lot/batch number, sell-by date and so on : '0':no, '1':yes, '2": yes with unique serial number
317
	 *
318
	 * @var int
319
	 */
320
	public $status_batch = 0;
321
322
	/**
323
	 * If allowed, we can edit batch or serial number mask for each product
324
	 *
325
	 * @var string
326
	 */
327
	public $batch_mask = '';
328
329
	/**
330
	 * Customs code
331
	 *
332
	 * @var string
333
	 */
334
	public $customcode;
335
336
	/**
337
	 * Product URL
338
	 *
339
	 * @var string
340
	 */
341
	public $url;
342
343
	//! Metric of products
344
	public $weight;
345
	public $weight_units;	// scale -3, 0, 3, 6
346
	public $length;
347
	public $length_units;	// scale -3, 0, 3, 6
348
	public $width;
349
	public $width_units;	// scale -3, 0, 3, 6
350
	public $height;
351
	public $height_units;	// scale -3, 0, 3, 6
352
	public $surface;
353
	public $surface_units;	// scale -3, 0, 3, 6
354
	public $volume;
355
	public $volume_units;	// scale -3, 0, 3, 6
356
357
	public $net_measure;
358
	public $net_measure_units;	// scale -3, 0, 3, 6
359
360
	public $accountancy_code_sell;
361
	public $accountancy_code_sell_intra;
362
	public $accountancy_code_sell_export;
363
	public $accountancy_code_buy;
364
	public $accountancy_code_buy_intra;
365
	public $accountancy_code_buy_export;
366
367
	/**
368
	 * Main Barcode value
369
	 *
370
	 * @var string
371
	 */
372
	public $barcode;
373
374
	/**
375
	 * Main Barcode type ID
376
	 *
377
	 * @var int
378
	 */
379
	public $barcode_type;
380
381
	/**
382
	 * Main Barcode type code
383
	 *
384
	 * @var string
385
	 */
386
	public $barcode_type_code;
387
388
	public $stats_propale = array();
389
	public $stats_commande = array();
390
	public $stats_contrat = array();
391
	public $stats_facture = array();
392
	public $stats_commande_fournisseur = array();
393
	public $stats_reception = array();
394
	public $stats_mrptoconsume = array();
395
	public $stats_mrptoproduce = array();
396
397
	//! Size of image
398
	public $imgWidth;
399
	public $imgHeight;
400
401
	/**
402
	 * @var integer|string date_creation
403
	 */
404
	public $date_creation;
405
406
	/**
407
	 * @var integer|string date_modification
408
	 */
409
	public $date_modification;
410
411
	//! Id du fournisseur
412
	public $product_fourn_id;
413
414
	//! Product ID already linked to a reference supplier
415
	public $product_id_already_linked;
416
417
	public $nbphoto = 0;
418
419
	//! Contains detail of stock of product into each warehouse
420
	public $stock_warehouse = array();
421
422
	public $oldcopy;
423
424
	/**
425
	 * @var int Default warehouse Id
426
	 */
427
	public $fk_default_warehouse;
428
	/**
429
	 * @var int ID
430
	 */
431
	public $fk_price_expression;
432
433
	/* To store supplier price found */
434
	public $fourn_qty;
435
	public $fourn_pu;
436
	public $fourn_price_base_type;
437
	public $fourn_socid;
438
439
	/**
440
	 * @deprecated
441
	 * @see        $ref_supplier
442
	 */
443
	public $ref_fourn;
444
445
	/**
446
	 * @var string ref supplier
447
	 */
448
	public $ref_supplier;
449
450
	/**
451
	 * Unit code ('km', 'm', 'l', 'p', ...)
452
	 *
453
	 * @var string
454
	 */
455
	public $fk_unit;
456
457
	/**
458
	 * Price is generated using multiprice rules
459
	 *
460
	 * @var int
461
	 */
462
	public $price_autogen = 0;
463
464
	/**
465
	 * Array with list of supplier prices of product
466
	 *
467
	 * @var array
468
	 */
469
	public $supplierprices;
470
471
	/**
472
	 * Property set to save result of isObjectUsed(). Used for example by Product API.
473
	 *
474
	 * @var boolean
475
	 */
476
	public $is_object_used;
477
478
479
	/**
480
	 *
481
	 *
482
	 *
483
	 */
484
	 public $mandatory_period;
485
486
	/**
487
	 *  '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')
488
	 *         Note: Filter can be a string like "(t.ref:like:'SO-%') or (t.date_creation:<:'20160101') or (t.nature:is:NULL)"
489
	 *  'label' the translation key.
490
	 *  'enabled' is a condition when the field must be managed (Example: 1 or '$conf->global->MY_SETUP_PARAM)
491
	 *  'position' is the sort order of field.
492
	 *  'notnull' is set to 1 if not null in database. Set to -1 if we must set data to null if empty ('' or 0).
493
	 *  '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)
494
	 *  'noteditable' says if field is not editable (1 or 0)
495
	 *  '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.
496
	 *  'index' if we want an index in database.
497
	 *  'foreignkey'=>'tablename.field' if the field is a foreign key (it is recommanded to name the field fk_...).
498
	 *  'searchall' is 1 if we want to search in this field when making a search from the quick search button.
499
	 *  '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).
500
	 *  'css' is the CSS style to use on field. For example: 'maxwidth200'
501
	 *  'help' is a string visible as a tooltip on field
502
	 *  'showoncombobox' if value of the field must be visible into the label of the combobox that list record
503
	 *  '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.
504
	 *  'arrayofkeyval' to set list of value if type is a list of predefined values. For example: array("0"=>"Draft","1"=>"Active","-1"=>"Cancel")
505
	 *  'autofocusoncreate' to have field having the focus on a create form. Only 1 field should have this property set to 1.
506
	 *  'comment' is not used. You can store here any text of your choice. It is not used by application.
507
	 *
508
	 *  Note: To have value dynamic, you can set value to 0 in definition and edit the value on the fly into the constructor.
509
	 */
510
511
	/**
512
	 * @var array fields of object product
513
	 */
514
	public $fields = array(
515
		'rowid' => array('type'=>'integer', 'label'=>'TechnicalID', 'enabled'=>1, 'visible'=>-2, 'notnull'=>1, 'index'=>1, 'position'=>1, 'comment'=>'Id'),
516
		'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'),
517
		'entity'        =>array('type'=>'integer', 'label'=>'Entity', 'enabled'=>1, 'visible'=>0, 'default'=>1, 'notnull'=>1, 'index'=>1, 'position'=>5),
518
		'label'         =>array('type'=>'varchar(255)', 'label'=>'Label', 'enabled'=>1, 'visible'=>1, 'notnull'=>1, 'showoncombobox'=>2, 'position'=>15, 'csslist'=>'tdoverflowmax250'),
519
		'barcode'       =>array('type'=>'varchar(255)', 'label'=>'Barcode', 'enabled'=>'isModEnabled("barcode")', 'position'=>20, 'visible'=>-1, 'showoncombobox'=>3),
520
		'fk_barcode_type' => array('type'=>'integer', 'label'=>'BarcodeType', 'enabled'=>'1', 'position'=>21, 'notnull'=>0, 'visible'=>-1,),
521
		'note_public'   =>array('type'=>'html', 'label'=>'NotePublic', 'enabled'=>1, 'visible'=>0, 'position'=>61),
522
		'note'          =>array('type'=>'html', 'label'=>'NotePrivate', 'enabled'=>1, 'visible'=>0, 'position'=>62),
523
		'datec'         =>array('type'=>'datetime', 'label'=>'DateCreation', 'enabled'=>1, 'visible'=>-2, 'notnull'=>1, 'position'=>500),
524
		'tms'           =>array('type'=>'timestamp', 'label'=>'DateModification', 'enabled'=>1, 'visible'=>-2, 'notnull'=>1, 'position'=>501),
525
		//'date_valid'    =>array('type'=>'datetime',     'label'=>'DateCreation',     'enabled'=>1, 'visible'=>-2, 'position'=>502),
526
		'fk_user_author'=>array('type'=>'integer', 'label'=>'UserAuthor', 'enabled'=>1, 'visible'=>-2, 'notnull'=>1, 'position'=>510, 'foreignkey'=>'llx_user.rowid'),
527
		'fk_user_modif' =>array('type'=>'integer', 'label'=>'UserModif', 'enabled'=>1, 'visible'=>-2, 'notnull'=>-1, 'position'=>511),
528
		//'fk_user_valid' =>array('type'=>'integer',      'label'=>'UserValidation',        'enabled'=>1, 'visible'=>-1, 'position'=>512),
529
		'localtax1_tx' => array('type'=>'double(6,3)', 'label'=>'Localtax1tx', 'enabled'=>'1', 'position'=>150, 'notnull'=>0, 'visible'=>-1,),
530
		'localtax1_type' => array('type'=>'varchar(10)', 'label'=>'Localtax1type', 'enabled'=>'1', 'position'=>155, 'notnull'=>1, 'visible'=>-1,),
531
		'localtax2_tx' => array('type'=>'double(6,3)', 'label'=>'Localtax2tx', 'enabled'=>'1', 'position'=>160, 'notnull'=>0, 'visible'=>-1,),
532
		'localtax2_type' => array('type'=>'varchar(10)', 'label'=>'Localtax2type', 'enabled'=>'1', 'position'=>165, 'notnull'=>1, 'visible'=>-1,),
533
		'import_key'    =>array('type'=>'varchar(14)', 'label'=>'ImportId', 'enabled'=>1, 'visible'=>-2, 'notnull'=>-1, 'index'=>0, 'position'=>1000),
534
		//'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')),
535
		//'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')),
536
		'mandatory_period' => array('type'=>'integer', 'label'=>'mandatory_period', 'enabled'=>1, 'visible'=>1,  'notnull'=>1, 'default'=>0, 'index'=>1,  'position'=>1000),
537
	);
538
539
	/**
540
	 * Regular product
541
	 */
542
	const TYPE_PRODUCT = 0;
543
	/**
544
	 * Service
545
	 */
546
	const TYPE_SERVICE = 1;
547
	/**
548
	 * Advanced feature: assembly kit
549
	 */
550
	const TYPE_ASSEMBLYKIT = 2;
551
	/**
552
	 * Advanced feature: stock kit
553
	 */
554
	const TYPE_STOCKKIT = 3;
555
556
557
	/**
558
	 *  Constructor
559
	 *
560
	 * @param DoliDB $db Database handler
561
	 */
562
	public function __construct($db)
563
	{
564
		$this->db = $db;
565
		$this->canvas = '';
566
	}
567
568
	/**
569
	 *    Check that ref and label are ok
570
	 *
571
	 * @return int         >1 if OK, <=0 if KO
572
	 */
573
	public function check()
574
	{
575
		if (!empty($conf->global->MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $conf seems to be never defined.
Loading history...
576
			$this->ref = trim($this->ref);
577
		} else {
578
			$this->ref = dol_sanitizeFileName(stripslashes($this->ref));
579
		}
580
581
		$err = 0;
582
		if (dol_strlen(trim($this->ref)) == 0) {
583
			$err++;
584
		}
585
586
		if (dol_strlen(trim($this->label)) == 0) {
587
			$err++;
588
		}
589
590
		if ($err > 0) {
591
			return 0;
592
		} else {
593
			return 1;
594
		}
595
	}
596
597
	/**
598
	 *    Insert product into database
599
	 *
600
	 * @param  User $user      User making insert
601
	 * @param  int  $notrigger Disable triggers
602
	 * @return int                         Id of product/service if OK, < 0 if KO
603
	 */
604
	public function create($user, $notrigger = 0)
605
	{
606
		global $conf, $langs;
607
608
		$error = 0;
609
610
		// Clean parameters
611
		if (!empty($conf->global->MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS)) {
612
			$this->ref = trim($this->ref);
613
		} else {
614
			$this->ref = dol_sanitizeFileName(dol_string_nospecial(trim($this->ref)));
615
		}
616
		$this->label = trim($this->label);
617
		$this->price_ttc = price2num($this->price_ttc);
618
		$this->price = price2num($this->price);
619
		$this->price_min_ttc = price2num($this->price_min_ttc);
620
		$this->price_min = price2num($this->price_min);
621
		if (empty($this->tva_tx)) {
622
			$this->tva_tx = 0;
623
		}
624
		if (empty($this->tva_npr)) {
625
			$this->tva_npr = 0;
626
		}
627
		//Local taxes
628
		if (empty($this->localtax1_tx)) {
629
			$this->localtax1_tx = 0;
630
		}
631
		if (empty($this->localtax2_tx)) {
632
			$this->localtax2_tx = 0;
633
		}
634
		if (empty($this->localtax1_type)) {
635
			$this->localtax1_type = '0';
636
		}
637
		if (empty($this->localtax2_type)) {
638
			$this->localtax2_type = '0';
639
		}
640
		if (empty($this->price)) {
641
			$this->price = 0;
642
		}
643
		if (empty($this->price_min)) {
644
			$this->price_min = 0;
645
		}
646
		// Price by quantity
647
		if (empty($this->price_by_qty)) {
648
			$this->price_by_qty = 0;
649
		}
650
651
		if (empty($this->status)) {
652
			$this->status = 0;
653
		}
654
		if (empty($this->status_buy)) {
655
			$this->status_buy = 0;
656
		}
657
658
		$price_ht = 0;
659
		$price_ttc = 0;
660
		$price_min_ht = 0;
661
		$price_min_ttc = 0;
662
663
		//
664
		if ($this->price_base_type == 'TTC' && $this->price_ttc > 0) {
665
			$price_ttc = price2num($this->price_ttc, 'MU');
666
			$price_ht = price2num($this->price_ttc / (1 + ($this->tva_tx / 100)), 'MU');
667
		}
668
669
		//
670
		if ($this->price_base_type != 'TTC' && $this->price > 0) {
671
			$price_ht = price2num($this->price, 'MU');
672
			$price_ttc = price2num($this->price * (1 + ($this->tva_tx / 100)), 'MU');
673
		}
674
675
		//
676
		if (($this->price_min_ttc > 0) && ($this->price_base_type == 'TTC')) {
677
			$price_min_ttc = price2num($this->price_min_ttc, 'MU');
678
			$price_min_ht = price2num($this->price_min_ttc / (1 + ($this->tva_tx / 100)), 'MU');
679
		}
680
681
		//
682
		if (($this->price_min > 0) && ($this->price_base_type != 'TTC')) {
683
			$price_min_ht = price2num($this->price_min, 'MU');
684
			$price_min_ttc = price2num($this->price_min * (1 + ($this->tva_tx / 100)), 'MU');
685
		}
686
687
		$this->accountancy_code_buy = trim($this->accountancy_code_buy);
688
		$this->accountancy_code_buy_intra = trim($this->accountancy_code_buy_intra);
689
		$this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export);
690
		$this->accountancy_code_sell = trim($this->accountancy_code_sell);
691
		$this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra);
692
		$this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export);
693
694
		// Barcode value
695
		$this->barcode = trim($this->barcode);
696
		$this->mandatory_period = empty($this->mandatory_period) ? 0 : $this->mandatory_period;
697
		// Check parameters
698
		if (empty($this->label)) {
699
			$this->error = 'ErrorMandatoryParametersNotProvided';
700
			return -1;
701
		}
702
703
		if (empty($this->ref) || $this->ref == 'auto') {
704
			// Load object modCodeProduct
705
			$module = (!empty($conf->global->PRODUCT_CODEPRODUCT_ADDON) ? $conf->global->PRODUCT_CODEPRODUCT_ADDON : 'mod_codeproduct_leopard');
706
			if ($module != 'mod_codeproduct_leopard') {    // Do not load module file for leopard
707
				if (substr($module, 0, 16) == 'mod_codeproduct_' && substr($module, -3) == 'php') {
708
					$module = substr($module, 0, dol_strlen($module) - 4);
709
				}
710
				dol_include_once('/core/modules/product/'.$module.'.php');
711
				$modCodeProduct = new $module;
712
				if (!empty($modCodeProduct->code_auto)) {
713
					$this->ref = $modCodeProduct->getNextValue($this, $this->type);
714
				}
715
				unset($modCodeProduct);
716
			}
717
718
			if (empty($this->ref)) {
719
				$this->error = 'ProductModuleNotSetupForAutoRef';
720
				return -2;
721
			}
722
		}
723
724
		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);
725
726
		$now = dol_now();
727
728
		$this->db->begin();
729
730
		// For automatic creation during create action (not used by Dolibarr GUI, can be used by scripts)
731
		if ($this->barcode == -1) {
732
			$this->barcode = $this->get_barcode($this, $this->barcode_type_code);
733
		}
734
735
		// Check more parameters
736
		// If error, this->errors[] is filled
737
		$result = $this->verify();
738
739
		if ($result >= 0) {
740
			$sql = "SELECT count(*) as nb";
741
			$sql .= " FROM ".$this->db->prefix()."product";
742
			$sql .= " WHERE entity IN (".getEntity('product').")";
743
			$sql .= " AND ref = '".$this->db->escape($this->ref)."'";
744
745
			$result = $this->db->query($sql);
746
			if ($result) {
747
				$obj = $this->db->fetch_object($result);
748
				if ($obj->nb == 0) {
749
					// Produit non deja existant
750
					$sql = "INSERT INTO ".$this->db->prefix()."product (";
751
					$sql .= "datec";
752
					$sql .= ", entity";
753
					$sql .= ", ref";
754
					$sql .= ", ref_ext";
755
					$sql .= ", price_min";
756
					$sql .= ", price_min_ttc";
757
					$sql .= ", label";
758
					$sql .= ", fk_user_author";
759
					$sql .= ", fk_product_type";
760
					$sql .= ", price";
761
					$sql .= ", price_ttc";
762
					$sql .= ", price_base_type";
763
					$sql .= ", tobuy";
764
					$sql .= ", tosell";
765
					if (empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED)) {
766
						$sql .= ", accountancy_code_buy";
767
						$sql .= ", accountancy_code_buy_intra";
768
						$sql .= ", accountancy_code_buy_export";
769
						$sql .= ", accountancy_code_sell";
770
						$sql .= ", accountancy_code_sell_intra";
771
						$sql .= ", accountancy_code_sell_export";
772
					}
773
					$sql .= ", canvas";
774
					$sql .= ", finished";
775
					$sql .= ", tobatch";
776
					$sql .= ", batch_mask";
777
					$sql .= ", fk_unit";
778
					$sql .= ", mandatory_period";
779
					$sql .= ") VALUES (";
780
					$sql .= "'".$this->db->idate($now)."'";
781
					$sql .= ", ".((int) $conf->entity);
782
					$sql .= ", '".$this->db->escape($this->ref)."'";
783
					$sql .= ", ".(!empty($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null");
784
					$sql .= ", ".price2num($price_min_ht);
785
					$sql .= ", ".price2num($price_min_ttc);
786
					$sql .= ", ".(!empty($this->label) ? "'".$this->db->escape($this->label)."'" : "null");
787
					$sql .= ", ".((int) $user->id);
788
					$sql .= ", ".((int) $this->type);
789
					$sql .= ", ".price2num($price_ht, 'MT');
790
					$sql .= ", ".price2num($price_ttc, 'MT');
791
					$sql .= ", '".$this->db->escape($this->price_base_type)."'";
792
					$sql .= ", ".((int) $this->status);
793
					$sql .= ", ".((int) $this->status_buy);
794
					if (empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED)) {
795
						$sql .= ", '".$this->db->escape($this->accountancy_code_buy)."'";
796
						$sql .= ", '".$this->db->escape($this->accountancy_code_buy_intra)."'";
797
						$sql .= ", '".$this->db->escape($this->accountancy_code_buy_export)."'";
798
						$sql .= ", '".$this->db->escape($this->accountancy_code_sell)."'";
799
						$sql .= ", '".$this->db->escape($this->accountancy_code_sell_intra)."'";
800
						$sql .= ", '".$this->db->escape($this->accountancy_code_sell_export)."'";
801
					}
802
					$sql .= ", '".$this->db->escape($this->canvas)."'";
803
					$sql .= ", ".((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? 'NULL' : (int) $this->finished);
804
					$sql .= ", ".((empty($this->status_batch) || $this->status_batch < 0) ? '0' : ((int) $this->status_batch));
805
					$sql .= ", '".$this->db->escape($this->batch_mask)."'";
806
					$sql .= ", ".($this->fk_unit > 0 ? ((int) $this->fk_unit) : 'NULL');
807
					$sql .= ", '".$this->db->escape($this->mandatory_period)."'";
808
					$sql .= ")";
809
810
					dol_syslog(get_class($this)."::Create", LOG_DEBUG);
811
					$result = $this->db->query($sql);
812
					if ($result) {
813
						$id = $this->db->last_insert_id($this->db->prefix()."product");
814
815
						if ($id > 0) {
816
							$this->id = $id;
817
							$this->price            = $price_ht;
818
							$this->price_ttc        = $price_ttc;
819
							$this->price_min        = $price_min_ht;
820
							$this->price_min_ttc    = $price_min_ttc;
821
822
							$result = $this->_log_price($user);
823
							if ($result > 0) {
824
								if ($this->update($id, $user, true, 'add') <= 0) {
825
									$error++;
826
								}
827
							} else {
828
								$error++;
829
								$this->error = $this->db->lasterror();
830
							}
831
832
							// update accountancy for this entity
833
							if (!$error && !empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED)) {
834
								$this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " .((int) $this->id) . " AND entity = " . ((int) $conf->entity));
835
836
								$sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
837
								$sql .= " fk_product";
838
								$sql .= ", entity";
839
								$sql .= ", accountancy_code_buy";
840
								$sql .= ", accountancy_code_buy_intra";
841
								$sql .= ", accountancy_code_buy_export";
842
								$sql .= ", accountancy_code_sell";
843
								$sql .= ", accountancy_code_sell_intra";
844
								$sql .= ", accountancy_code_sell_export";
845
								$sql .= ") VALUES (";
846
								$sql .= $this->id;
847
								$sql .= ", " . $conf->entity;
848
								$sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
849
								$sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
850
								$sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
851
								$sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
852
								$sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
853
								$sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
854
								$sql .= ")";
855
								$result = $this->db->query($sql);
856
								if (!$result) {
857
									$error++;
858
									$this->error = 'ErrorFailedToInsertAccountancyForEntity';
859
								}
860
							}
861
						} else {
862
							$error++;
863
							$this->error = 'ErrorFailedToGetInsertedId';
864
						}
865
					} else {
866
						$error++;
867
						$this->error = $this->db->lasterror();
868
					}
869
				} else {
870
					// Product already exists with this ref
871
					$langs->load("products");
872
					$error++;
873
					$this->error = "ErrorProductAlreadyExists";
874
				}
875
			} else {
876
				$error++;
877
				$this->error = $this->db->lasterror();
878
			}
879
880
			if (!$error && !$notrigger) {
881
				// Call trigger
882
				$result = $this->call_trigger('PRODUCT_CREATE', $user);
883
				if ($result < 0) {
884
					$error++;
885
				}
886
				// End call triggers
887
			}
888
889
			if (!$error) {
890
				$this->db->commit();
891
				return $this->id;
892
			} else {
893
				$this->db->rollback();
894
				return -$error;
895
			}
896
		} else {
897
			$this->db->rollback();
898
			dol_syslog(get_class($this)."::Create fails verify ".join(',', $this->errors), LOG_WARNING);
899
			return -3;
900
		}
901
	}
902
903
904
	/**
905
	 *    Check properties of product are ok (like name, barcode, ...).
906
	 *    All properties must be already loaded on object (this->barcode, this->barcode_type_code, ...).
907
	 *
908
	 * @return int        0 if OK, <0 if KO
909
	 */
910
	public function verify()
911
	{
912
		global $langs;
913
914
		$this->errors = array();
915
916
		$result = 0;
917
		$this->ref = trim($this->ref);
918
919
		if (!$this->ref) {
920
			$this->errors[] = 'ErrorBadRef';
921
			$result = -2;
922
		}
923
924
		$arrayofnonnegativevalue = array('weight'=>'Weight', 'width'=>'Width', 'height'=>'Height', 'length'=>'Length', 'surface'=>'Surface', 'volume'=>'Volume');
925
		foreach ($arrayofnonnegativevalue as $key => $value) {
926
			if (property_exists($this, $key) && !empty($this->$key) && ($this->$key < 0)) {
927
				$langs->loadLangs(array("main", "other"));
928
				$this->error = $langs->trans("FieldCannotBeNegative", $langs->transnoentitiesnoconv($value));
929
				$this->errors[] = $this->error;
930
				$result = -4;
931
			}
932
		}
933
934
		$rescode = $this->check_barcode($this->barcode, $this->barcode_type_code);
935
		if ($rescode) {
936
			if ($rescode == -1) {
937
				$this->errors[] = 'ErrorBadBarCodeSyntax';
938
			} elseif ($rescode == -2) {
939
				$this->errors[] = 'ErrorBarCodeRequired';
940
			} elseif ($rescode == -3) {
941
				// Note: Common usage is to have barcode unique. For variants, we should have a different barcode.
942
				$this->errors[] = 'ErrorBarCodeAlreadyUsed';
943
			}
944
945
			$result = -3;
946
		}
947
948
		return $result;
949
	}
950
951
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
952
	/**
953
	 *  Check barcode
954
	 *
955
	 * @param  string $valuetotest Value to test
956
	 * @param  string $typefortest Type of barcode (ISBN, EAN, ...)
957
	 * @return int                        0 if OK
958
	 *                                     -1 ErrorBadBarCodeSyntax
959
	 *                                     -2 ErrorBarCodeRequired
960
	 *                                     -3 ErrorBarCodeAlreadyUsed
961
	 */
962
	public function check_barcode($valuetotest, $typefortest)
963
	{
964
		// phpcs:enable
965
		global $conf;
966
		if (isModEnabled('barcode') && !empty($conf->global->BARCODE_PRODUCT_ADDON_NUM)) {
967
			$module = strtolower($conf->global->BARCODE_PRODUCT_ADDON_NUM);
968
969
			$dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
970
			foreach ($dirsociete as $dirroot) {
971
				$res = dol_include_once($dirroot.$module.'.php');
972
				if ($res) {
973
					break;
974
				}
975
			}
976
977
			$mod = new $module();
978
979
			dol_syslog(get_class($this)."::check_barcode value=".$valuetotest." type=".$typefortest." module=".$module);
980
			$result = $mod->verif($this->db, $valuetotest, $this, 0, $typefortest);
981
			return $result;
982
		} else {
983
			return 0;
984
		}
985
	}
986
987
	/**
988
	 *  Update a record into database.
989
	 *  If batch flag is set to on, we create records into llx_product_batch
990
	 *
991
	 * @param  int     $id          Id of product
992
	 * @param  User    $user        Object user making update
993
	 * @param  int     $notrigger   Disable triggers
994
	 * @param  string  $action      Current action for hookmanager ('add' or 'update')
995
	 * @param  boolean $updatetype  Update product type
996
	 * @return int                  1 if OK, -1 if ref already exists, -2 if other error
997
	 */
998
	public function update($id, $user, $notrigger = false, $action = 'update', $updatetype = false)
999
	{
1000
		global $langs, $conf, $hookmanager;
1001
1002
		$error = 0;
1003
1004
		// Check parameters
1005
		if (!$this->label) {
1006
			$this->label = 'MISSING LABEL';
1007
		}
1008
1009
		// Clean parameters
1010
		if (!empty($conf->global->MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS)) {
1011
			$this->ref = trim($this->ref);
1012
		} else {
1013
			$this->ref = dol_string_nospecial(trim($this->ref));
1014
		}
1015
		$this->label = trim($this->label);
1016
		$this->description = trim($this->description);
1017
		$this->note_private = (isset($this->note_private) ? trim($this->note_private) : null);
1018
		$this->note_public = (isset($this->note_public) ? trim($this->note_public) : null);
1019
		$this->net_measure = price2num($this->net_measure);
1020
		$this->net_measure_units = trim($this->net_measure_units);
1021
		$this->weight = price2num($this->weight);
1022
		$this->weight_units = trim($this->weight_units);
1023
		$this->length = price2num($this->length);
1024
		$this->length_units = trim($this->length_units);
1025
		$this->width = price2num($this->width);
1026
		$this->width_units = trim($this->width_units);
1027
		$this->height = price2num($this->height);
1028
		$this->height_units = trim($this->height_units);
1029
		$this->surface = price2num($this->surface);
1030
		$this->surface_units = trim($this->surface_units);
1031
		$this->volume = price2num($this->volume);
1032
		$this->volume_units = trim($this->volume_units);
1033
1034
		// set unit not defined
1035
		if (is_numeric($this->length_units)) {
1036
			$this->width_units = $this->length_units; // Not used yet
1037
		}
1038
		if (is_numeric($this->length_units)) {
1039
			$this->height_units = $this->length_units; // Not used yet
1040
		}
1041
1042
		// Automated compute surface and volume if not filled
1043
		if (empty($this->surface) && !empty($this->length) && !empty($this->width) && $this->length_units == $this->width_units) {
1044
			$this->surface = $this->length * $this->width;
1045
			$this->surface_units = measuring_units_squared($this->length_units);
1046
		}
1047
		if (empty($this->volume) && !empty($this->surface) && !empty($this->height) && $this->length_units == $this->height_units) {
1048
			$this->volume = $this->surface * $this->height;
1049
			$this->volume_units = measuring_units_cubed($this->height_units);
1050
		}
1051
1052
		if (empty($this->tva_tx)) {
1053
			$this->tva_tx = 0;
1054
		}
1055
		if (empty($this->tva_npr)) {
1056
			$this->tva_npr = 0;
1057
		}
1058
		if (empty($this->localtax1_tx)) {
1059
			$this->localtax1_tx = 0;
1060
		}
1061
		if (empty($this->localtax2_tx)) {
1062
			$this->localtax2_tx = 0;
1063
		}
1064
		if (empty($this->localtax1_type)) {
1065
			$this->localtax1_type = '0';
1066
		}
1067
		if (empty($this->localtax2_type)) {
1068
			$this->localtax2_type = '0';
1069
		}
1070
		if (empty($this->status)) {
1071
			$this->status = 0;
1072
		}
1073
		if (empty($this->status_buy)) {
1074
			$this->status_buy = 0;
1075
		}
1076
1077
		if (empty($this->country_id)) {
1078
			$this->country_id = 0;
1079
		}
1080
1081
		if (empty($this->state_id)) {
1082
			$this->state_id = 0;
1083
		}
1084
1085
		// Barcode value
1086
		$this->barcode = trim($this->barcode);
1087
1088
		$this->accountancy_code_buy = trim($this->accountancy_code_buy);
1089
		$this->accountancy_code_buy_intra = trim($this->accountancy_code_buy_intra);
1090
		$this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export);
1091
		$this->accountancy_code_sell = trim($this->accountancy_code_sell);
1092
		$this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra);
1093
		$this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export);
1094
1095
1096
		$this->db->begin();
1097
1098
		$result = 0;
1099
		// Check name is required and codes are ok or unique. If error, this->errors[] is filled
1100
		if ($action != 'add') {
1101
			$result = $this->verify(); // We don't check when update called during a create because verify was already done
1102
		} else {
1103
			// we can continue
1104
			$result = 0;
1105
		}
1106
1107
		if ($result >= 0) {
1108
			// $this->oldcopy should have been set by the caller of update (here properties were already modified)
1109
			if (empty($this->oldcopy)) {
1110
				$this->oldcopy = dol_clone($this);
1111
			}
1112
1113
			// Test if batch management is activated on existing product
1114
			// If yes, we create missing entries into product_batch
1115
			if ($this->hasbatch() && !$this->oldcopy->hasbatch()) {
1116
				//$valueforundefinedlot = 'Undefined';  // In previous version, 39 and lower
1117
				$valueforundefinedlot = '000000';
1118
				if (!empty($conf->global->STOCK_DEFAULT_BATCH)) {
1119
					$valueforundefinedlot = $conf->global->STOCK_DEFAULT_BATCH;
1120
				}
1121
1122
				dol_syslog("Flag batch of product id=".$this->id." is set to ON, so we will create missing records into product_batch");
1123
1124
				$this->load_stock();
1125
				foreach ($this->stock_warehouse as $idW => $ObjW) {   // For each warehouse where we have stocks defined for this product (for each lines in product_stock)
1126
					$qty_batch = 0;
1127
					foreach ($ObjW->detail_batch as $detail) {    // Each lines of detail in product_batch of the current $ObjW = product_stock
1128
						if ($detail->batch == $valueforundefinedlot || $detail->batch == 'Undefined') {
1129
							// We discard this line, we will create it later
1130
							$sqlclean = "DELETE FROM ".$this->db->prefix()."product_batch WHERE batch in('Undefined', '".$this->db->escape($valueforundefinedlot)."') AND fk_product_stock = ".((int) $ObjW->id);
1131
							$result = $this->db->query($sqlclean);
1132
							if (!$result) {
1133
								dol_print_error($this->db);
1134
								exit;
1135
							}
1136
							continue;
1137
						}
1138
1139
						$qty_batch += $detail->qty;
1140
					}
1141
					// Quantities in batch details are not same as stock quantity,
1142
					// so we add a default batch record to complete and get same qty in parent and child table
1143
					if ($ObjW->real <> $qty_batch) {
1144
						$ObjBatch = new Productbatch($this->db);
1145
						$ObjBatch->batch = $valueforundefinedlot;
1146
						$ObjBatch->qty = ($ObjW->real - $qty_batch);
1147
						$ObjBatch->fk_product_stock = $ObjW->id;
1148
1149
						if ($ObjBatch->create($user, 1) < 0) {
1150
							$error++;
1151
							$this->errors = $ObjBatch->errors;
1152
						}
1153
					}
1154
				}
1155
			}
1156
1157
			// For automatic creation
1158
			if ($this->barcode == -1) {
1159
				$this->barcode = $this->get_barcode($this, $this->barcode_type_code);
1160
			}
1161
1162
			$sql = "UPDATE ".$this->db->prefix()."product";
1163
			$sql .= " SET label = '".$this->db->escape($this->label)."'";
1164
1165
			if ($updatetype && ($this->isProduct() || $this->isService())) {
1166
				$sql .= ", fk_product_type = ".((int) $this->type);
1167
			}
1168
1169
			$sql .= ", ref = '".$this->db->escape($this->ref)."'";
1170
			$sql .= ", ref_ext = ".(!empty($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null");
1171
			$sql .= ", default_vat_code = ".($this->default_vat_code ? "'".$this->db->escape($this->default_vat_code)."'" : "null");
1172
			$sql .= ", tva_tx = ".((float) $this->tva_tx);
1173
			$sql .= ", recuperableonly = ".((int) $this->tva_npr);
1174
			$sql .= ", localtax1_tx = ".((float) $this->localtax1_tx);
1175
			$sql .= ", localtax2_tx = ".((float) $this->localtax2_tx);
1176
			$sql .= ", localtax1_type = ".($this->localtax1_type != '' ? "'".$this->db->escape($this->localtax1_type)."'" : "'0'");
1177
			$sql .= ", localtax2_type = ".($this->localtax2_type != '' ? "'".$this->db->escape($this->localtax2_type)."'" : "'0'");
1178
1179
			$sql .= ", barcode = ".(empty($this->barcode) ? "null" : "'".$this->db->escape($this->barcode)."'");
1180
			$sql .= ", fk_barcode_type = ".(empty($this->barcode_type) ? "null" : $this->db->escape($this->barcode_type));
1181
1182
			$sql .= ", tosell = ".(int) $this->status;
1183
			$sql .= ", tobuy = ".(int) $this->status_buy;
1184
			$sql .= ", tobatch = ".((empty($this->status_batch) || $this->status_batch < 0) ? '0' : (int) $this->status_batch);
1185
			$sql .= ", batch_mask = '".$this->db->escape($this->batch_mask)."'";
1186
1187
			$sql .= ", finished = ".((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? "null" : (int) $this->finished);
1188
			$sql .= ", fk_default_bom = ".((!isset($this->fk_default_bom) || $this->fk_default_bom < 0 || $this->fk_default_bom == '') ? "null" : (int) $this->fk_default_bom);
1189
			$sql .= ", net_measure = ".($this->net_measure != '' ? "'".$this->db->escape($this->net_measure)."'" : 'null');
1190
			$sql .= ", net_measure_units = ".($this->net_measure_units != '' ? "'".$this->db->escape($this->net_measure_units)."'" : 'null');
1191
			$sql .= ", weight = ".($this->weight != '' ? "'".$this->db->escape($this->weight)."'" : 'null');
1192
			$sql .= ", weight_units = ".($this->weight_units != '' ? "'".$this->db->escape($this->weight_units)."'" : 'null');
1193
			$sql .= ", length = ".($this->length != '' ? "'".$this->db->escape($this->length)."'" : 'null');
1194
			$sql .= ", length_units = ".($this->length_units != '' ? "'".$this->db->escape($this->length_units)."'" : 'null');
1195
			$sql .= ", width= ".($this->width != '' ? "'".$this->db->escape($this->width)."'" : 'null');
1196
			$sql .= ", width_units = ".($this->width_units != '' ? "'".$this->db->escape($this->width_units)."'" : 'null');
1197
			$sql .= ", height = ".($this->height != '' ? "'".$this->db->escape($this->height)."'" : 'null');
1198
			$sql .= ", height_units = ".($this->height_units != '' ? "'".$this->db->escape($this->height_units)."'" : 'null');
1199
			$sql .= ", surface = ".($this->surface != '' ? "'".$this->db->escape($this->surface)."'" : 'null');
1200
			$sql .= ", surface_units = ".($this->surface_units != '' ? "'".$this->db->escape($this->surface_units)."'" : 'null');
1201
			$sql .= ", volume = ".($this->volume != '' ? "'".$this->db->escape($this->volume)."'" : 'null');
1202
			$sql .= ", volume_units = ".($this->volume_units != '' ? "'".$this->db->escape($this->volume_units)."'" : 'null');
1203
			$sql .= ", fk_default_warehouse = ".($this->fk_default_warehouse > 0 ? $this->db->escape($this->fk_default_warehouse) : 'null');
1204
			$sql .= ", fk_default_workstation = ".($this->fk_default_workstation > 0 ? $this->db->escape($this->fk_default_workstation) : 'null');
1205
			$sql .= ", seuil_stock_alerte = ".((isset($this->seuil_stock_alerte) && is_numeric($this->seuil_stock_alerte)) ? (float) $this->seuil_stock_alerte : 'null');
1206
			$sql .= ", description = '".$this->db->escape($this->description)."'";
1207
			$sql .= ", url = ".($this->url ? "'".$this->db->escape($this->url)."'" : 'null');
1208
			$sql .= ", customcode = '".$this->db->escape($this->customcode)."'";
1209
			$sql .= ", fk_country = ".($this->country_id > 0 ? (int) $this->country_id : 'null');
1210
			$sql .= ", fk_state = ".($this->state_id > 0 ? (int) $this->state_id : 'null');
1211
			$sql .= ", lifetime = ".($this->lifetime > 0 ? (int) $this->lifetime : 'null');
1212
			$sql .= ", qc_frequency = ".($this->qc_frequency > 0 ? (int) $this->qc_frequency : 'null');
1213
			$sql .= ", note = ".(isset($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : 'null');
1214
			$sql .= ", note_public = ".(isset($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : 'null');
1215
			$sql .= ", duration = '".$this->db->escape($this->duration_value.$this->duration_unit)."'";
1216
			if (empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED)) {
1217
				$sql .= ", accountancy_code_buy = '" . $this->db->escape($this->accountancy_code_buy) . "'";
1218
				$sql .= ", accountancy_code_buy_intra = '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1219
				$sql .= ", accountancy_code_buy_export = '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1220
				$sql .= ", accountancy_code_sell= '" . $this->db->escape($this->accountancy_code_sell) . "'";
1221
				$sql .= ", accountancy_code_sell_intra= '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1222
				$sql .= ", accountancy_code_sell_export= '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1223
			}
1224
			$sql .= ", desiredstock = ".((isset($this->desiredstock) && is_numeric($this->desiredstock)) ? (float) $this->desiredstock : "null");
1225
			$sql .= ", cost_price = ".($this->cost_price != '' ? $this->db->escape($this->cost_price) : 'null');
1226
			$sql .= ", fk_unit= ".(!$this->fk_unit ? 'NULL' : (int) $this->fk_unit);
1227
			$sql .= ", price_autogen = ".(!$this->price_autogen ? 0 : 1);
1228
			$sql .= ", fk_price_expression = ".($this->fk_price_expression != 0 ? (int) $this->fk_price_expression : 'NULL');
1229
			$sql .= ", fk_user_modif = ".($user->id > 0 ? $user->id : 'NULL');
1230
			$sql .= ", mandatory_period = ".($this->mandatory_period );
1231
			// stock field is not here because it is a denormalized value from product_stock.
1232
			$sql .= " WHERE rowid = ".((int) $id);
1233
1234
			dol_syslog(get_class($this)."::update", LOG_DEBUG);
1235
1236
			$resql = $this->db->query($sql);
1237
			if ($resql) {
1238
				$this->id = $id;
1239
1240
				// Multilangs
1241
				if (getDolGlobalInt('MAIN_MULTILANGS')) {
1242
					if ($this->setMultiLangs($user) < 0) {
1243
						$this->error = $langs->trans("Error")." : ".$this->db->error()." - ".$sql;
1244
						return -2;
1245
					}
1246
				}
1247
1248
				$action = 'update';
1249
1250
				// update accountancy for this entity
1251
				if (!$error && !empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED)) {
1252
					$this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " . ((int) $this->id) . " AND entity = " . ((int) $conf->entity));
1253
1254
					$sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
1255
					$sql .= " fk_product";
1256
					$sql .= ", entity";
1257
					$sql .= ", accountancy_code_buy";
1258
					$sql .= ", accountancy_code_buy_intra";
1259
					$sql .= ", accountancy_code_buy_export";
1260
					$sql .= ", accountancy_code_sell";
1261
					$sql .= ", accountancy_code_sell_intra";
1262
					$sql .= ", accountancy_code_sell_export";
1263
					$sql .= ") VALUES (";
1264
					$sql .= $this->id;
1265
					$sql .= ", " . $conf->entity;
1266
					$sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
1267
					$sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1268
					$sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1269
					$sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
1270
					$sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1271
					$sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1272
					$sql .= ")";
1273
					$result = $this->db->query($sql);
1274
					if (!$result) {
1275
						$error++;
1276
						$this->error = 'ErrorFailedToUpdateAccountancyForEntity';
1277
					}
1278
				}
1279
1280
				// Actions on extra fields
1281
				if (!$error) {
1282
					$result = $this->insertExtraFields();
1283
					if ($result < 0) {
1284
						$error++;
1285
					}
1286
				}
1287
1288
				if (!$error && !$notrigger) {
1289
					// Call trigger
1290
					$result = $this->call_trigger('PRODUCT_MODIFY', $user);
1291
					if ($result < 0) {
1292
						$error++;
1293
					}
1294
					// End call triggers
1295
				}
1296
1297
				if (!$error && (is_object($this->oldcopy) && $this->oldcopy->ref !== $this->ref)) {
1298
					// We remove directory
1299
					if ($conf->product->dir_output) {
1300
						$olddir = $conf->product->dir_output."/".dol_sanitizeFileName($this->oldcopy->ref);
1301
						$newdir = $conf->product->dir_output."/".dol_sanitizeFileName($this->ref);
1302
						if (file_exists($olddir)) {
1303
							//include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
1304
							//$res = dol_move($olddir, $newdir);
1305
							// do not use dol_move with directory
1306
							$res = @rename($olddir, $newdir);
1307
							if (!$res) {
1308
								$langs->load("errors");
1309
								$this->error = $langs->trans('ErrorFailToRenameDir', $olddir, $newdir);
1310
								$error++;
1311
							}
1312
						}
1313
					}
1314
				}
1315
1316
				if (!$error) {
1317
					if (isModEnabled('variants')) {
1318
						include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
1319
1320
						$comb = new ProductCombination($this->db);
1321
1322
						foreach ($comb->fetchAllByFkProductParent($this->id) as $currcomb) {
1323
							$currcomb->updateProperties($this, $user);
1324
						}
1325
					}
1326
1327
					$this->db->commit();
1328
					return 1;
1329
				} else {
1330
					$this->db->rollback();
1331
					return -$error;
1332
				}
1333
			} else {
1334
				if ($this->db->errno() == 'DB_ERROR_RECORD_ALREADY_EXISTS') {
1335
					$langs->load("errors");
1336
					if (empty($conf->barcode->enabled) || empty($this->barcode)) {
1337
						$this->error = $langs->trans("Error")." : ".$langs->trans("ErrorProductAlreadyExists", $this->ref);
1338
					} else {
1339
						$this->error = $langs->trans("Error")." : ".$langs->trans("ErrorProductBarCodeAlreadyExists", $this->barcode);
1340
					}
1341
					$this->errors[] = $this->error;
1342
					$this->db->rollback();
1343
					return -1;
1344
				} else {
1345
					$this->error = $langs->trans("Error")." : ".$this->db->error()." - ".$sql;
1346
					$this->errors[] = $this->error;
1347
					$this->db->rollback();
1348
					return -2;
1349
				}
1350
			}
1351
		} else {
1352
			$this->db->rollback();
1353
			dol_syslog(get_class($this)."::Update fails verify ".join(',', $this->errors), LOG_WARNING);
1354
			return -3;
1355
		}
1356
	}
1357
1358
	/**
1359
	 *  Delete a product from database (if not used)
1360
	 *
1361
	 * @param  User $user      User (object) deleting product
1362
	 * @param  int  $notrigger Do not execute trigger
1363
	 * @return int                    < 0 if KO, 0 = Not possible, > 0 if OK
1364
	 */
1365
	public function delete(User $user, $notrigger = 0)
1366
	{
1367
		global $conf, $langs;
1368
		include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1369
1370
		$error = 0;
1371
1372
		// Check parameters
1373
		if (empty($this->id)) {
1374
			$this->error = "Object must be fetched before calling delete";
1375
			return -1;
1376
		}
1377
		if (($this->type == Product::TYPE_PRODUCT && empty($user->rights->produit->supprimer)) || ($this->type == Product::TYPE_SERVICE && empty($user->rights->service->supprimer))) {
1378
			$this->error = "ErrorForbidden";
1379
			return 0;
1380
		}
1381
1382
		$objectisused = $this->isObjectUsed($this->id);
1383
		if (empty($objectisused)) {
1384
			$this->db->begin();
1385
1386
			if (!$error && empty($notrigger)) {
1387
				// Call trigger
1388
				$result = $this->call_trigger('PRODUCT_DELETE', $user);
1389
				if ($result < 0) {
1390
					$error++;
1391
				}
1392
				// End call triggers
1393
			}
1394
1395
			// Delete from product_batch on product delete
1396
			if (!$error) {
1397
				$sql = "DELETE FROM ".$this->db->prefix().'product_batch';
1398
				$sql .= " WHERE fk_product_stock IN (";
1399
				$sql .= "SELECT rowid FROM ".$this->db->prefix().'product_stock';
1400
				$sql .= " WHERE fk_product = ".((int) $this->id).")";
1401
1402
				$result = $this->db->query($sql);
1403
				if (!$result) {
1404
					$error++;
1405
					$this->errors[] = $this->db->lasterror();
1406
				}
1407
			}
1408
1409
			// Delete all child tables
1410
			if (!$error) {
1411
				$elements = array('product_fournisseur_price', 'product_price', 'product_lang', 'categorie_product', 'product_stock', 'product_customer_price', 'product_lot'); // product_batch is done before
1412
				foreach ($elements as $table) {
1413
					if (!$error) {
1414
						$sql = "DELETE FROM ".$this->db->prefix().$table;
1415
						$sql .= " WHERE fk_product = ".(int) $this->id;
1416
1417
						$result = $this->db->query($sql);
1418
						if (!$result) {
1419
							$error++;
1420
							$this->errors[] = $this->db->lasterror();
1421
						}
1422
					}
1423
				}
1424
			}
1425
1426
			if (!$error) {
1427
				include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
1428
				include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination2ValuePair.class.php';
1429
1430
				//If it is a parent product, then we remove the association with child products
1431
				$prodcomb = new ProductCombination($this->db);
1432
1433
				if ($prodcomb->deleteByFkProductParent($user, $this->id) < 0) {
1434
					$error++;
1435
					$this->errors[] = 'Error deleting combinations';
1436
				}
1437
1438
				//We also check if it is a child product
1439
				if (!$error && ($prodcomb->fetchByFkProductChild($this->id) > 0) && ($prodcomb->delete($user) < 0)) {
1440
					$error++;
1441
					$this->errors[] = 'Error deleting child combination';
1442
				}
1443
			}
1444
1445
			// Delete from product_association
1446
			if (!$error) {
1447
				$sql = "DELETE FROM ".$this->db->prefix()."product_association";
1448
				$sql .= " WHERE fk_product_pere = ".(int) $this->id." OR fk_product_fils = ".(int) $this->id;
1449
1450
				$result = $this->db->query($sql);
1451
				if (!$result) {
1452
					$error++;
1453
					$this->errors[] = $this->db->lasterror();
1454
				}
1455
			}
1456
1457
			// Remove extrafields
1458
			if (!$error) {
1459
				$result = $this->deleteExtraFields();
1460
				if ($result < 0) {
1461
					$error++;
1462
					dol_syslog(get_class($this)."::delete error -4 ".$this->error, LOG_ERR);
1463
				}
1464
			}
1465
1466
			// Delete product
1467
			if (!$error) {
1468
				$sqlz = "DELETE FROM ".$this->db->prefix()."product";
1469
				$sqlz .= " WHERE rowid = ".(int) $this->id;
1470
1471
				$resultz = $this->db->query($sqlz);
1472
				if (!$resultz) {
1473
					$error++;
1474
					$this->errors[] = $this->db->lasterror();
1475
				}
1476
			}
1477
1478
			// Delete record into ECM index and physically
1479
			if (!$error) {
1480
				$res = $this->deleteEcmFiles(0); // Deleting files physically is done later with the dol_delete_dir_recursive
1481
				if (!$res) {
1482
					$error++;
1483
				}
1484
			}
1485
1486
			if (!$error) {
1487
				// We remove directory
1488
				$ref = dol_sanitizeFileName($this->ref);
1489
				if ($conf->product->dir_output) {
1490
					$dir = $conf->product->dir_output."/".$ref;
1491
					if (file_exists($dir)) {
1492
						$res = @dol_delete_dir_recursive($dir);
1493
						if (!$res) {
1494
							$this->errors[] = 'ErrorFailToDeleteDir';
1495
							$error++;
1496
						}
1497
					}
1498
				}
1499
			}
1500
1501
			if (!$error) {
1502
				$this->db->commit();
1503
				return 1;
1504
			} else {
1505
				foreach ($this->errors as $errmsg) {
1506
					dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR);
1507
					$this->error .= ($this->error ? ', '.$errmsg : $errmsg);
1508
				}
1509
				$this->db->rollback();
1510
				return -$error;
1511
			}
1512
		} else {
1513
			$this->error = "ErrorRecordIsUsedCantDelete";
1514
			return 0;
1515
		}
1516
	}
1517
1518
	/**
1519
	 *    Update or add a translation for a product
1520
	 *
1521
	 * @param  User $user Object user making update
1522
	 * @return int        <0 if KO, >0 if OK
1523
	 */
1524
	public function setMultiLangs($user)
1525
	{
1526
		global $conf, $langs;
1527
1528
		$langs_available = $langs->get_available_languages(DOL_DOCUMENT_ROOT, 0, 2);
1529
		$current_lang = $langs->getDefaultLang();
1530
1531
		foreach ($langs_available as $key => $value) {
1532
			if ($key == $current_lang) {
1533
				$sql = "SELECT rowid";
1534
				$sql .= " FROM ".$this->db->prefix()."product_lang";
1535
				$sql .= " WHERE fk_product = ".((int) $this->id);
1536
				$sql .= " AND lang = '".$this->db->escape($key)."'";
1537
1538
				$result = $this->db->query($sql);
1539
1540
				if ($this->db->num_rows($result)) { // if there is already a description line for this language
1541
					$sql2 = "UPDATE ".$this->db->prefix()."product_lang";
1542
					$sql2 .= " SET ";
1543
					$sql2 .= " label='".$this->db->escape($this->label)."',";
1544
					$sql2 .= " description='".$this->db->escape($this->description)."'";
1545
					if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) {
1546
						$sql2 .= ", note='".$this->db->escape($this->other)."'";
1547
					}
1548
					$sql2 .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($key)."'";
1549
				} else {
1550
					$sql2 = "INSERT INTO ".$this->db->prefix()."product_lang (fk_product, lang, label, description";
1551
					if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) {
1552
						$sql2 .= ", note";
1553
					}
1554
					$sql2 .= ")";
1555
					$sql2 .= " VALUES(".$this->id.",'".$this->db->escape($key)."','".$this->db->escape($this->label)."',";
1556
					$sql2 .= " '".$this->db->escape($this->description)."'";
1557
					if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) {
1558
						$sql2 .= ", '".$this->db->escape($this->other)."'";
1559
					}
1560
					$sql2 .= ")";
1561
				}
1562
				dol_syslog(get_class($this).'::setMultiLangs key = current_lang = '.$key);
1563
				if (!$this->db->query($sql2)) {
1564
					$this->error = $this->db->lasterror();
1565
					return -1;
1566
				}
1567
			} elseif (isset($this->multilangs[$key])) {
1568
				if (empty($this->multilangs["$key"]["label"])) {
1569
					$this->errors[] = $key . ' : ' . $langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("Label"));
1570
					return -1;
1571
				}
1572
1573
				$sql = "SELECT rowid";
1574
				$sql .= " FROM ".$this->db->prefix()."product_lang";
1575
				$sql .= " WHERE fk_product = ".((int) $this->id);
1576
				$sql .= " AND lang = '".$this->db->escape($key)."'";
1577
1578
				$result = $this->db->query($sql);
1579
1580
				if ($this->db->num_rows($result)) { // if there is already a description line for this language
1581
					$sql2 = "UPDATE ".$this->db->prefix()."product_lang";
1582
					$sql2 .= " SET ";
1583
					$sql2 .= " label = '".$this->db->escape($this->multilangs["$key"]["label"])."',";
1584
					$sql2 .= " description = '".$this->db->escape($this->multilangs["$key"]["description"])."'";
1585
					if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) {
1586
						$sql2 .= ", note = '".$this->db->escape($this->multilangs["$key"]["other"])."'";
1587
					}
1588
					$sql2 .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($key)."'";
1589
				} else {
1590
					$sql2 = "INSERT INTO ".$this->db->prefix()."product_lang (fk_product, lang, label, description";
1591
					if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) {
1592
						$sql2 .= ", note";
1593
					}
1594
					$sql2 .= ")";
1595
					$sql2 .= " VALUES(".$this->id.",'".$this->db->escape($key)."','".$this->db->escape($this->multilangs["$key"]["label"])."',";
1596
					$sql2 .= " '".$this->db->escape($this->multilangs["$key"]["description"])."'";
1597
					if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) {
1598
						$sql2 .= ", '".$this->db->escape($this->multilangs["$key"]["other"])."'";
1599
					}
1600
					$sql2 .= ")";
1601
				}
1602
1603
				// We do not save if main fields are empty
1604
				if ($this->multilangs["$key"]["label"] || $this->multilangs["$key"]["description"]) {
1605
					if (!$this->db->query($sql2)) {
1606
						$this->error = $this->db->lasterror();
1607
						return -1;
1608
					}
1609
				}
1610
			} else {
1611
				// language is not current language and we didn't provide a multilang description for this language
1612
			}
1613
		}
1614
1615
		// Call trigger
1616
		$result = $this->call_trigger('PRODUCT_SET_MULTILANGS', $user);
1617
		if ($result < 0) {
1618
			$this->error = $this->db->lasterror();
1619
			return -1;
1620
		}
1621
		// End call triggers
1622
1623
		return 1;
1624
	}
1625
1626
	/**
1627
	 *    Delete a language for this product
1628
	 *
1629
	 * @param string $langtodelete Language code to delete
1630
	 * @param User   $user         Object user making delete
1631
	 *
1632
	 * @return int                            <0 if KO, >0 if OK
1633
	 */
1634
	public function delMultiLangs($langtodelete, $user)
1635
	{
1636
		$sql = "DELETE FROM ".$this->db->prefix()."product_lang";
1637
		$sql .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($langtodelete)."'";
1638
1639
		dol_syslog(get_class($this).'::delMultiLangs', LOG_DEBUG);
1640
		$result = $this->db->query($sql);
1641
		if ($result) {
1642
			// Call trigger
1643
			$result = $this->call_trigger('PRODUCT_DEL_MULTILANGS', $user);
1644
			if ($result < 0) {
1645
				$this->error = $this->db->lasterror();
1646
				dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
1647
				return -1;
1648
			}
1649
			// End call triggers
1650
			return 1;
1651
		} else {
1652
			$this->error = $this->db->lasterror();
1653
			dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
1654
			return -1;
1655
		}
1656
	}
1657
1658
	/**
1659
	 * Sets an accountancy code for a product.
1660
	 * Also calls PRODUCT_MODIFY trigger when modified
1661
	 *
1662
	 * @param 	string $type 	It can be 'buy', 'buy_intra', 'buy_export', 'sell', 'sell_intra' or 'sell_export'
1663
	 * @param 	string $value 	Accountancy code
1664
	 * @return 	int 			<0 KO >0 OK
1665
	 */
1666
	public function setAccountancyCode($type, $value)
1667
	{
1668
		global $user, $langs, $conf;
1669
1670
		$error = 0;
1671
1672
		$this->db->begin();
1673
1674
		if ($type == 'buy') {
1675
			$field = 'accountancy_code_buy';
1676
		} elseif ($type == 'buy_intra') {
1677
			$field = 'accountancy_code_buy_intra';
1678
		} elseif ($type == 'buy_export') {
1679
			$field = 'accountancy_code_buy_export';
1680
		} elseif ($type == 'sell') {
1681
			$field = 'accountancy_code_sell';
1682
		} elseif ($type == 'sell_intra') {
1683
			$field = 'accountancy_code_sell_intra';
1684
		} elseif ($type == 'sell_export') {
1685
			$field = 'accountancy_code_sell_export';
1686
		} else {
1687
			return -1;
1688
		}
1689
1690
		$sql = "UPDATE ".$this->db->prefix().$this->table_element." SET ";
1691
		$sql .= "$field = '".$this->db->escape($value)."'";
1692
		$sql .= " WHERE rowid = ".((int) $this->id);
1693
1694
		dol_syslog(__METHOD__, LOG_DEBUG);
1695
		$resql = $this->db->query($sql);
1696
1697
		if ($resql) {
1698
			// Call trigger
1699
			$result = $this->call_trigger('PRODUCT_MODIFY', $user);
1700
			if ($result < 0) {
1701
				$error++;
1702
			}
1703
			// End call triggers
1704
1705
			if ($error) {
1706
				$this->db->rollback();
1707
				return -1;
1708
			}
1709
1710
			$this->$field = $value;
1711
1712
			$this->db->commit();
1713
			return 1;
1714
		} else {
1715
			$this->error = $this->db->lasterror();
1716
			$this->db->rollback();
1717
			return -1;
1718
		}
1719
	}
1720
1721
	/**
1722
	 *    Load array this->multilangs
1723
	 *
1724
	 * @return int        <0 if KO, >0 if OK
1725
	 */
1726
	public function getMultiLangs()
1727
	{
1728
		global $langs;
1729
1730
		$current_lang = $langs->getDefaultLang();
1731
1732
		$sql = "SELECT lang, label, description, note as other";
1733
		$sql .= " FROM ".$this->db->prefix()."product_lang";
1734
		$sql .= " WHERE fk_product = ".((int) $this->id);
1735
1736
		$result = $this->db->query($sql);
1737
		if ($result) {
1738
			while ($obj = $this->db->fetch_object($result)) {
1739
				//print 'lang='.$obj->lang.' current='.$current_lang.'<br>';
1740
				if ($obj->lang == $current_lang) {  // si on a les traduct. dans la langue courante on les charge en infos principales.
1741
					$this->label        = $obj->label;
1742
					$this->description = $obj->description;
1743
					$this->other        = $obj->other;
1744
				}
1745
				$this->multilangs["$obj->lang"]["label"]        = $obj->label;
1746
				$this->multilangs["$obj->lang"]["description"] = $obj->description;
1747
				$this->multilangs["$obj->lang"]["other"]        = $obj->other;
1748
			}
1749
			return 1;
1750
		} else {
1751
			$this->error = "Error: ".$this->db->lasterror()." - ".$sql;
1752
			return -1;
1753
		}
1754
	}
1755
1756
	/**
1757
	 *  used to check if price have really change to avoid log pollution
1758
	 *
1759
	 * @param  int  $level price level to change
1760
	 * @return array
1761
	 */
1762
	private function getArrayForPriceCompare($level = 0)
1763
	{
1764
1765
		$testExit = array('multiprices','multiprices_ttc','multiprices_base_type','multiprices_min','multiprices_min_ttc','multiprices_tva_tx','multiprices_recuperableonly');
1766
1767
		foreach ($testExit as $field) {
1768
			if (!isset($this->$field)) {
1769
				return array();
1770
			}
1771
			$tmparray = $this->$field;
1772
			if (!isset($tmparray[$level])) {
1773
				return array();
1774
			}
1775
		}
1776
1777
		$lastPrice = array(
1778
			'level' => $level ? $level : 1,
1779
			'multiprices' => doubleval($this->multiprices[$level]),
1780
			'multiprices_ttc' => doubleval($this->multiprices_ttc[$level]),
1781
			'multiprices_base_type' => $this->multiprices_base_type[$level],
1782
			'multiprices_min' => doubleval($this->multiprices_min[$level]),
1783
			'multiprices_min_ttc' => doubleval($this->multiprices_min_ttc[$level]),
1784
			'multiprices_tva_tx' => doubleval($this->multiprices_tva_tx[$level]),
1785
			'multiprices_recuperableonly' => doubleval($this->multiprices_recuperableonly[$level]),
1786
		);
1787
1788
		return $lastPrice;
1789
	}
1790
1791
1792
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1793
	/**
1794
	 *  Insert a track that we changed a customer price
1795
	 *
1796
	 * @param  User $user  User making change
1797
	 * @param  int  $level price level to change
1798
	 * @return int                    <0 if KO, >0 if OK
1799
	 */
1800
	private function _log_price($user, $level = 0)
1801
	{
1802
		// phpcs:enable
1803
		global $conf;
1804
1805
		$now = dol_now();
1806
1807
		// Clean parameters
1808
		if (empty($this->price_by_qty)) {
1809
			$this->price_by_qty = 0;
1810
		}
1811
1812
		// Add new price
1813
		$sql = "INSERT INTO ".$this->db->prefix()."product_price(price_level,date_price, fk_product, fk_user_author, price, price_ttc, price_base_type,tosell, tva_tx, default_vat_code, recuperableonly,";
1814
		$sql .= " localtax1_tx, localtax2_tx, localtax1_type, localtax2_type, price_min,price_min_ttc,price_by_qty,entity,fk_price_expression) ";
1815
		$sql .= " VALUES(".($level ? ((int) $level) : 1).", '".$this->db->idate($now)."', ".((int) $this->id).", ".((int) $user->id).", ".((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).",";
1816
		$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');
1817
		$sql .= ")";
1818
1819
		dol_syslog(get_class($this)."::_log_price", LOG_DEBUG);
1820
		$resql = $this->db->query($sql);
1821
		if (!$resql) {
1822
			$this->error = $this->db->lasterror();
1823
			dol_print_error($this->db);
1824
			return -1;
1825
		} else {
1826
			return 1;
1827
		}
1828
	}
1829
1830
1831
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1832
	/**
1833
	 *  Delete a price line
1834
	 *
1835
	 * @param  User $user  Object user
1836
	 * @param  int  $rowid Line id to delete
1837
	 * @return int                <0 if KO, >0 if OK
1838
	 */
1839
	public function log_price_delete($user, $rowid)
1840
	{
1841
		// phpcs:enable
1842
		$sql = "DELETE FROM ".$this->db->prefix()."product_price_by_qty";
1843
		$sql .= " WHERE fk_product_price = ".((int) $rowid);
1844
		$resql = $this->db->query($sql);
1845
1846
		$sql = "DELETE FROM ".$this->db->prefix()."product_price";
1847
		$sql .= " WHERE rowid=".((int) $rowid);
1848
		$resql = $this->db->query($sql);
1849
		if ($resql) {
1850
			return 1;
1851
		} else {
1852
			$this->error = $this->db->lasterror();
1853
			return -1;
1854
		}
1855
	}
1856
1857
1858
	/**
1859
	 * Return price of sell of a product for a seller/buyer/product.
1860
	 *
1861
	 * @param	Societe		$thirdparty_seller		Seller
1862
	 * @param	Societe		$thirdparty_buyer		Buyer
1863
	 * @param	int			$pqp					Id of product price per quantity if a selection was done of such a price
1864
	 * @return	array								Array of price information array('pu_ht'=> , 'pu_ttc'=> , 'tva_tx'=>'X.Y (code)', ...), 'tva_npr'=>0, ...)
1865
	 * @see get_buyprice(), find_min_price_product_fournisseur()
1866
	 */
1867
	public function getSellPrice($thirdparty_seller, $thirdparty_buyer, $pqp = 0)
1868
	{
1869
		global $conf, $db, $hookmanager, $action;
1870
1871
		// Call hook if any
1872
		if (is_object($hookmanager)) {
1873
			$parameters = array('thirdparty_seller'=>$thirdparty_seller, 'thirdparty_buyer' => $thirdparty_buyer, 'pqp' => $pqp);
1874
			// Note that $action and $object may have been modified by some hooks
1875
			$reshook = $hookmanager->executeHooks('getSellPrice', $parameters, $this, $action);
1876
			if ($reshook > 0) {
1877
				return $hookmanager->resArray;
1878
			}
1879
		}
1880
1881
		// Update if prices fields are defined
1882
		$tva_tx = get_default_tva($thirdparty_seller, $thirdparty_buyer, $this->id);
1883
		$tva_npr = get_default_npr($thirdparty_seller, $thirdparty_buyer, $this->id);
1884
		if (empty($tva_tx)) {
1885
			$tva_npr = 0;
1886
		}
1887
1888
		$pu_ht = $this->price;
1889
		$pu_ttc = $this->price_ttc;
1890
		$price_min = $this->price_min;
1891
		$price_base_type = $this->price_base_type;
1892
1893
		// If price per segment
1894
		if (!empty($conf->global->PRODUIT_MULTIPRICES) && !empty($thirdparty_buyer->price_level)) {
1895
			$pu_ht = $this->multiprices[$thirdparty_buyer->price_level];
1896
			$pu_ttc = $this->multiprices_ttc[$thirdparty_buyer->price_level];
1897
			$price_min = $this->multiprices_min[$thirdparty_buyer->price_level];
1898
			$price_base_type = $this->multiprices_base_type[$thirdparty_buyer->price_level];
1899
			if (!empty($conf->global->PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL)) {  // using this option is a bug. kept for backward compatibility
1900
				if (isset($this->multiprices_tva_tx[$thirdparty_buyer->price_level])) {
1901
					$tva_tx = $this->multiprices_tva_tx[$thirdparty_buyer->price_level];
1902
				}
1903
				if (isset($this->multiprices_recuperableonly[$thirdparty_buyer->price_level])) {
1904
					$tva_npr = $this->multiprices_recuperableonly[$thirdparty_buyer->price_level];
1905
				}
1906
				if (empty($tva_tx)) {
1907
					$tva_npr = 0;
1908
				}
1909
			}
1910
		} elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES)) {
1911
			// If price per customer
1912
			require_once DOL_DOCUMENT_ROOT.'/product/class/productcustomerprice.class.php';
1913
1914
			$prodcustprice = new Productcustomerprice($this->db);
1915
1916
			$filter = array('t.fk_product' => $this->id, 't.fk_soc' => $thirdparty_buyer->id);
1917
1918
			$result = $prodcustprice->fetchAll('', '', 0, 0, $filter);
1919
			if ($result) {
1920
				if (count($prodcustprice->lines) > 0) {
1921
					$pu_ht = price($prodcustprice->lines[0]->price);
1922
					$price_min = price($prodcustprice->lines[0]->price_min);
1923
					$pu_ttc = price($prodcustprice->lines[0]->price_ttc);
1924
					$price_base_type = $prodcustprice->lines[0]->price_base_type;
1925
					$tva_tx = $prodcustprice->lines[0]->tva_tx;
1926
					if ($prodcustprice->lines[0]->default_vat_code && !preg_match('/\(.*\)/', $tva_tx)) {
1927
						$tva_tx .= ' ('.$prodcustprice->lines[0]->default_vat_code.')';
1928
					}
1929
					$tva_npr = $prodcustprice->lines[0]->recuperableonly;
1930
					if (empty($tva_tx)) {
1931
						$tva_npr = 0;
1932
					}
1933
				}
1934
			}
1935
		} elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY)) {
1936
			// If price per quantity
1937
			if ($this->prices_by_qty[0]) {
1938
				// yes, this product has some prices per quantity
1939
				// Search price into product_price_by_qty from $this->id
1940
				foreach ($this->prices_by_qty_list[0] as $priceforthequantityarray) {
1941
					if ($priceforthequantityarray['rowid'] != $pqp) {
1942
						continue;
1943
					}
1944
					// We found the price
1945
					if ($priceforthequantityarray['price_base_type'] == 'HT') {
1946
						$pu_ht = $priceforthequantityarray['unitprice'];
1947
					} else {
1948
						$pu_ttc = $priceforthequantityarray['unitprice'];
1949
					}
1950
					break;
1951
				}
1952
			}
1953
		} elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES)) {
1954
			// If price per quantity and customer
1955
			if ($this->prices_by_qty[$thirdparty_buyer->price_level]) {
1956
				// yes, this product has some prices per quantity
1957
				// Search price into product_price_by_qty from $this->id
1958
				foreach ($this->prices_by_qty_list[$thirdparty_buyer->price_level] as $priceforthequantityarray) {
1959
					if ($priceforthequantityarray['rowid'] != $pqp) {
1960
						continue;
1961
					}
1962
					// We found the price
1963
					if ($priceforthequantityarray['price_base_type'] == 'HT') {
1964
						$pu_ht = $priceforthequantityarray['unitprice'];
1965
					} else {
1966
						$pu_ttc = $priceforthequantityarray['unitprice'];
1967
					}
1968
					break;
1969
				}
1970
			}
1971
		}
1972
1973
		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);
1974
	}
1975
1976
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1977
	/**
1978
	 * Read price used by a provider.
1979
	 * We enter as input couple prodfournprice/qty or triplet qty/product_id/fourn_ref.
1980
	 * This also set some properties on product like ->buyprice, ->fourn_pu, ...
1981
	 *
1982
	 * @param  int    $prodfournprice Id du tarif = rowid table product_fournisseur_price
1983
	 * @param  double $qty            Quantity asked or -1 to get first entry found
1984
	 * @param  int    $product_id     Filter on a particular product id
1985
	 * @param  string $fourn_ref      Filter on a supplier price ref. 'none' to exclude ref in search.
1986
	 * @param  int    $fk_soc         If of supplier
1987
	 * @return int                    <-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...)
1988
	 * @see getSellPrice(), find_min_price_product_fournisseur()
1989
	 */
1990
	public function get_buyprice($prodfournprice, $qty, $product_id = 0, $fourn_ref = '', $fk_soc = 0)
1991
	{
1992
		// phpcs:enable
1993
		global $conf;
1994
		$result = 0;
1995
1996
		// 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)
1997
		$sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
1998
		$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,";
1999
		$sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2000
		$sql .= " pfp.packaging";
2001
		$sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp";
2002
		$sql .= " WHERE pfp.rowid = ".((int) $prodfournprice);
2003
		if ($qty > 0) {
2004
			$sql .= " AND pfp.quantity <= ".((float) $qty);
2005
		}
2006
		$sql .= " ORDER BY pfp.quantity DESC";
2007
2008
		dol_syslog(get_class($this)."::get_buyprice first search by prodfournprice/qty", LOG_DEBUG);
2009
		$resql = $this->db->query($sql);
2010
		if ($resql) {
2011
			$obj = $this->db->fetch_object($resql);
2012
			if ($obj && $obj->quantity > 0) {        // If we found a supplier prices from the id of supplier price
2013
				if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2014
					$prod_supplier = new ProductFournisseur($this->db);
2015
					$prod_supplier->product_fourn_price_id = $obj->rowid;
2016
					$prod_supplier->id = $obj->fk_product;
2017
					$prod_supplier->fourn_qty = $obj->quantity;
2018
					$prod_supplier->fourn_tva_tx = $obj->tva_tx;
2019
					$prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2020
2021
					include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2022
					$priceparser = new PriceParser($this->db);
2023
					$price_result = $priceparser->parseProductSupplier($prod_supplier);
2024
					if ($price_result >= 0) {
2025
						$obj->price = $price_result;
2026
					}
2027
				}
2028
				$this->product_fourn_price_id = $obj->rowid;
2029
				$this->buyprice = $obj->price; // deprecated
2030
				$this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product of supplier
2031
				$this->fourn_price_base_type = 'HT'; // Price base type
2032
				$this->fourn_socid = $obj->fk_soc; // Company that offer this price
2033
				$this->ref_fourn = $obj->ref_supplier; // deprecated
2034
				$this->ref_supplier = $obj->ref_supplier; // Ref supplier
2035
				$this->desc_supplier = $obj->desc_supplier; // desc supplier
2036
				$this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2037
				$this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2038
				$this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2039
				$this->fourn_multicurrency_price = $obj->multicurrency_price;
2040
				$this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2041
				$this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2042
				$this->fourn_multicurrency_id = $obj->fk_multicurrency;
2043
				$this->fourn_multicurrency_code = $obj->multicurrency_code;
2044
				if (!empty($conf->global->PRODUCT_USE_SUPPLIER_PACKAGING)) {
2045
					$this->packaging = $obj->packaging;
2046
				}
2047
				$result = $obj->fk_product;
2048
				return $result;
2049
			} else { // If not found
2050
				// 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.
2051
				$sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2052
				$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,";
2053
				$sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2054
				$sql .= " pfp.packaging";
2055
				$sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp";
2056
				$sql .= " WHERE 1 = 1";
2057
				if ($product_id > 0) {
2058
					$sql .= " AND pfp.fk_product = ".((int) $product_id);
2059
				}
2060
				if ($fourn_ref != 'none') {
2061
					$sql .= " AND pfp.ref_fourn = '".$this->db->escape($fourn_ref)."'";
2062
				}
2063
				if ($fk_soc > 0) {
2064
					$sql .= " AND pfp.fk_soc = ".((int) $fk_soc);
2065
				}
2066
				if ($qty > 0) {
2067
					$sql .= " AND pfp.quantity <= ".((float) $qty);
2068
				}
2069
				$sql .= " ORDER BY pfp.quantity DESC";
2070
				$sql .= " LIMIT 1";
2071
2072
				dol_syslog(get_class($this)."::get_buyprice second search from qty/ref/product_id", LOG_DEBUG);
2073
				$resql = $this->db->query($sql);
2074
				if ($resql) {
2075
					$obj = $this->db->fetch_object($resql);
2076
					if ($obj && $obj->quantity > 0) {        // If found
2077
						if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2078
							$prod_supplier = new ProductFournisseur($this->db);
2079
							$prod_supplier->product_fourn_price_id = $obj->rowid;
2080
							$prod_supplier->id = $obj->fk_product;
2081
							$prod_supplier->fourn_qty = $obj->quantity;
2082
							$prod_supplier->fourn_tva_tx = $obj->tva_tx;
2083
							$prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2084
2085
							include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2086
							$priceparser = new PriceParser($this->db);
2087
							$price_result = $priceparser->parseProductSupplier($prod_supplier);
2088
							if ($result >= 0) {
2089
								$obj->price = $price_result;
2090
							}
2091
						}
2092
						$this->product_fourn_price_id = $obj->rowid;
2093
						$this->buyprice = $obj->price; // deprecated
2094
						$this->fourn_qty = $obj->quantity; // min quantity for price for a virtual supplier
2095
						$this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product for a virtual supplier
2096
						$this->fourn_price_base_type = 'HT'; // Price base type for a virtual supplier
2097
						$this->fourn_socid = $obj->fk_soc; // Company that offer this price
2098
						$this->ref_fourn = $obj->ref_supplier; // deprecated
2099
						$this->ref_supplier = $obj->ref_supplier; // Ref supplier
2100
						$this->desc_supplier = $obj->desc_supplier; // desc supplier
2101
						$this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2102
						$this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2103
						$this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2104
						$this->fourn_multicurrency_price = $obj->multicurrency_price;
2105
						$this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2106
						$this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2107
						$this->fourn_multicurrency_id = $obj->fk_multicurrency;
2108
						$this->fourn_multicurrency_code = $obj->multicurrency_code;
2109
						if (!empty($conf->global->PRODUCT_USE_SUPPLIER_PACKAGING)) {
2110
							$this->packaging = $obj->packaging;
2111
						}
2112
						$result = $obj->fk_product;
2113
						return $result;
2114
					} else {
2115
						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é.
2116
					}
2117
				} else {
2118
					$this->error = $this->db->lasterror();
2119
					return -3;
2120
				}
2121
			}
2122
		} else {
2123
			$this->error = $this->db->lasterror();
2124
			return -2;
2125
		}
2126
	}
2127
2128
2129
	/**
2130
	 * Modify customer price of a product/Service for a given level
2131
	 *
2132
	 * @param  double $newprice          New price
2133
	 * @param  string $newpricebase      HT or TTC
2134
	 * @param  User   $user              Object user that make change
2135
	 * @param  double $newvat            New VAT Rate (For example 8.5. Should not be a string)
2136
	 * @param  double $newminprice       New price min
2137
	 * @param  int    $level             0=standard, >0 = level if multilevel prices
2138
	 * @param  int    $newnpr            0=Standard vat rate, 1=Special vat rate for French NPR VAT
2139
	 * @param  int    $newpbq            1 if it has price by quantity
2140
	 * @param  int    $ignore_autogen    Used to avoid infinite loops
2141
	 * @param  array  $localtaxes_array  Array with localtaxes info array('0'=>type1,'1'=>rate1,'2'=>type2,'3'=>rate2) (loaded by getLocalTaxesFromRate(vatrate, 0, ...) function).
2142
	 * @param  string $newdefaultvatcode Default vat code
2143
	 * @return int                            <0 if KO, >0 if OK
2144
	 */
2145
	public function updatePrice($newprice, $newpricebase, $user, $newvat = '', $newminprice = 0, $level = 0, $newnpr = 0, $newpbq = 0, $ignore_autogen = 0, $localtaxes_array = array(), $newdefaultvatcode = '')
2146
	{
2147
		global $conf, $langs;
2148
2149
		$lastPriceData = $this->getArrayForPriceCompare($level); // temporary store current price before update
2150
2151
		$id = $this->id;
2152
2153
		dol_syslog(get_class($this)."::update_price id=".$id." newprice=".$newprice." newpricebase=".$newpricebase." newminprice=".$newminprice." level=".$level." npr=".$newnpr." newdefaultvatcode=".$newdefaultvatcode);
2154
2155
		// Clean parameters
2156
		if (empty($this->tva_tx)) {
2157
			$this->tva_tx = 0;
2158
		}
2159
		if (empty($newnpr)) {
2160
			$newnpr = 0;
2161
		}
2162
		if (empty($newminprice)) {
2163
			$newminprice = 0;
2164
		}
2165
		if (empty($newminprice)) {
2166
			$newminprice = 0;
2167
		}
2168
2169
		// Check parameters
2170
		if ($newvat == '') {
2171
			$newvat = $this->tva_tx;
2172
		}
2173
2174
		// If multiprices are enabled, then we check if the current product is subject to price autogeneration
2175
		// Price will be modified ONLY when the first one is the one that is being modified
2176
		if ((!empty($conf->global->PRODUIT_MULTIPRICES) || !empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES)) && !$ignore_autogen && $this->price_autogen && ($level == 1)) {
2177
			return $this->generateMultiprices($user, $newprice, $newpricebase, $newvat, $newnpr, $newpbq);
2178
		}
2179
2180
		if (!empty($newminprice) && ($newminprice > $newprice)) {
2181
			$this->error = 'ErrorPriceCantBeLowerThanMinPrice';
2182
			return -1;
2183
		}
2184
2185
		if ($newprice !== '' || $newprice === 0) {
2186
			if ($newpricebase == 'TTC') {
2187
				$price_ttc = price2num($newprice, 'MU');
2188
				$price = price2num($newprice) / (1 + ($newvat / 100));
2189
				$price = price2num($price, 'MU');
2190
2191
				if ($newminprice != '' || $newminprice == 0) {
2192
					$price_min_ttc = price2num($newminprice, 'MU');
2193
					$price_min = price2num($newminprice) / (1 + ($newvat / 100));
2194
					$price_min = price2num($price_min, 'MU');
2195
				} else {
2196
					$price_min = 0;
2197
					$price_min_ttc = 0;
2198
				}
2199
			} else {
2200
				$price = price2num($newprice, 'MU');
2201
				$price_ttc = ($newnpr != 1) ? (float) price2num($newprice) * (1 + ($newvat / 100)) : $price;
2202
				$price_ttc = price2num($price_ttc, 'MU');
2203
2204
				if ($newminprice !== '' || $newminprice === 0) {
2205
					$price_min = price2num($newminprice, 'MU');
2206
					$price_min_ttc = price2num($newminprice) * (1 + ($newvat / 100));
2207
					$price_min_ttc = price2num($price_min_ttc, 'MU');
2208
					//print 'X'.$newminprice.'-'.$price_min;
2209
				} else {
2210
					$price_min = 0;
2211
					$price_min_ttc = 0;
2212
				}
2213
			}
2214
			//print 'x'.$id.'-'.$newprice.'-'.$newpricebase.'-'.$price.'-'.$price_ttc.'-'.$price_min.'-'.$price_min_ttc;
2215
2216
			if (count($localtaxes_array) > 0) {
2217
				$localtaxtype1 = $localtaxes_array['0'];
2218
				$localtax1 = $localtaxes_array['1'];
2219
				$localtaxtype2 = $localtaxes_array['2'];
2220
				$localtax2 = $localtaxes_array['3'];
2221
			} else {
2222
				// if array empty, we try to use the vat code
2223
				if (!empty($newdefaultvatcode)) {
2224
					global $mysoc;
2225
					// Get record from code
2226
					$sql = "SELECT t.rowid, t.code, t.recuperableonly, t.localtax1, t.localtax2, t.localtax1_type, t.localtax2_type";
2227
					$sql .= " FROM ".MAIN_DB_PREFIX."c_tva as t, ".MAIN_DB_PREFIX."c_country as c";
2228
					$sql .= " WHERE t.fk_pays = c.rowid AND c.code = '".$this->db->escape($mysoc->country_code)."'";
2229
					$sql .= " AND t.taux = ".((float) $newdefaultvatcode)." AND t.active = 1";
2230
					$sql .= " AND t.code = '".$this->db->escape($newdefaultvatcode)."'";
2231
					$resql = $this->db->query($sql);
2232
					if ($resql) {
2233
						$obj = $this->db->fetch_object($resql);
2234
						if ($obj) {
2235
							$npr = $obj->recuperableonly;
2236
							$localtax1 = $obj->localtax1;
2237
							$localtax2 = $obj->localtax2;
2238
							$localtaxtype1 = $obj->localtax1_type;
2239
							$localtaxtype2 = $obj->localtax2_type;
2240
						}
2241
					}
2242
				} else {
2243
					// old method. deprecated because we can't retrieve type
2244
					$localtaxtype1 = '0';
2245
					$localtax1 = get_localtax($newvat, 1);
2246
					$localtaxtype2 = '0';
2247
					$localtax2 = get_localtax($newvat, 2);
2248
				}
2249
			}
2250
			if (empty($localtax1)) {
2251
				$localtax1 = 0; // If = '' then = 0
2252
			}
2253
			if (empty($localtax2)) {
2254
				$localtax2 = 0; // If = '' then = 0
2255
			}
2256
2257
			$this->db->begin();
2258
2259
			// Ne pas mettre de quote sur les numeriques decimaux.
2260
			// Ceci provoque des stockages avec arrondis en base au lieu des valeurs exactes.
2261
			$sql = "UPDATE ".$this->db->prefix()."product SET";
2262
			$sql .= " price_base_type='".$this->db->escape($newpricebase)."',";
2263
			$sql .= " price=".$price.",";
2264
			$sql .= " price_ttc=".$price_ttc.",";
2265
			$sql .= " price_min=".$price_min.",";
2266
			$sql .= " price_min_ttc=".$price_min_ttc.",";
2267
			$sql .= " localtax1_tx=".($localtax1 >= 0 ? $localtax1 : 'NULL').",";
2268
			$sql .= " localtax2_tx=".($localtax2 >= 0 ? $localtax2 : 'NULL').",";
2269
			$sql .= " localtax1_type=".($localtaxtype1 != '' ? "'".$this->db->escape($localtaxtype1)."'" : "'0'").",";
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $localtaxtype1 does not seem to be defined for all execution paths leading up to this point.
Loading history...
2270
			$sql .= " localtax2_type=".($localtaxtype2 != '' ? "'".$this->db->escape($localtaxtype2)."'" : "'0'").",";
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $localtaxtype2 does not seem to be defined for all execution paths leading up to this point.
Loading history...
2271
			$sql .= " default_vat_code=".($newdefaultvatcode ? "'".$this->db->escape($newdefaultvatcode)."'" : "null").",";
2272
			$sql .= " tva_tx='".price2num($newvat)."',";
2273
			$sql .= " recuperableonly='".$this->db->escape($newnpr)."'";
2274
			$sql .= " WHERE rowid = ".((int) $id);
2275
2276
			dol_syslog(get_class($this)."::update_price", LOG_DEBUG);
2277
			$resql = $this->db->query($sql);
2278
			if ($resql) {
2279
				$this->multiprices[$level] = $price;
2280
				$this->multiprices_ttc[$level] = $price_ttc;
2281
				$this->multiprices_min[$level] = $price_min;
2282
				$this->multiprices_min_ttc[$level] = $price_min_ttc;
2283
				$this->multiprices_base_type[$level] = $newpricebase;
2284
				$this->multiprices_default_vat_code[$level] = $newdefaultvatcode;
2285
				$this->multiprices_tva_tx[$level] = $newvat;
2286
				$this->multiprices_recuperableonly[$level] = $newnpr;
2287
2288
				$this->price = $price;
2289
				$this->price_ttc = $price_ttc;
2290
				$this->price_min = $price_min;
2291
				$this->price_min_ttc = $price_min_ttc;
2292
				$this->price_base_type = $newpricebase;
2293
				$this->default_vat_code = $newdefaultvatcode;
2294
				$this->tva_tx = $newvat;
2295
				$this->tva_npr = $newnpr;
2296
				//Local taxes
2297
				$this->localtax1_tx = $localtax1;
2298
				$this->localtax2_tx = $localtax2;
2299
				$this->localtax1_type = $localtaxtype1;
2300
				$this->localtax2_type = $localtaxtype2;
2301
2302
				// Price by quantity
2303
				$this->price_by_qty = $newpbq;
2304
2305
				// check if price have really change before log
2306
				$newPriceData = $this->getArrayForPriceCompare($level);
2307
				if (!empty(array_diff_assoc($newPriceData, $lastPriceData)) || empty($conf->global->PRODUIT_MULTIPRICES)) {
2308
					$this->_log_price($user, $level); // Save price for level into table product_price
2309
				}
2310
2311
				$this->level = $level; // Store level of price edited for trigger
2312
2313
				// Call trigger
2314
				$result = $this->call_trigger('PRODUCT_PRICE_MODIFY', $user);
2315
				if ($result < 0) {
2316
					$this->db->rollback();
2317
					return -1;
2318
				}
2319
				// End call triggers
2320
2321
				$this->db->commit();
2322
			} else {
2323
				$this->db->rollback();
2324
				$this->error = $this->db->lasterror();
2325
				return -1;
2326
			}
2327
		}
2328
2329
		return 1;
2330
	}
2331
2332
	/**
2333
	 *  Sets the supplier price expression
2334
	 *
2335
	 * @param      int $expression_id Expression
2336
	 * @return     int                     <0 if KO, >0 if OK
2337
	 * @deprecated Use Product::update instead
2338
	 */
2339
	public function setPriceExpression($expression_id)
2340
	{
2341
		global $user;
2342
2343
		$this->fk_price_expression = $expression_id;
2344
2345
		return $this->update($this->id, $user);
2346
	}
2347
2348
	/**
2349
	 *  Load a product in memory from database
2350
	 *
2351
	 * @param  int    $id                Id of product/service to load
2352
	 * @param  string $ref               Ref of product/service to load
2353
	 * @param  string $ref_ext           Ref ext of product/service to load
2354
	 * @param  string $barcode           Barcode of product/service to load
2355
	 * @param  int    $ignore_expression When module dynamicprices is on, ignores the math expression for calculating price and uses the db value instead
2356
	 * @param  int    $ignore_price_load Load product without loading $this->multiprices... array (when we are sure we don't need them)
2357
	 * @param  int    $ignore_lang_load  Load product without loading $this->multilangs language arrays (when we are sure we don't need them)
2358
	 * @return int                       <0 if KO, 0 if not found, >0 if OK
2359
	 */
2360
	public function fetch($id = '', $ref = '', $ref_ext = '', $barcode = '', $ignore_expression = 0, $ignore_price_load = 0, $ignore_lang_load = 0)
2361
	{
2362
		include_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
2363
2364
		global $langs, $conf;
2365
2366
		dol_syslog(get_class($this)."::fetch id=".$id." ref=".$ref." ref_ext=".$ref_ext);
2367
2368
		// Check parameters
2369
		if (!$id && !$ref && !$ref_ext && !$barcode) {
2370
			$this->error = 'ErrorWrongParameters';
2371
			dol_syslog(get_class($this)."::fetch ".$this->error, LOG_ERR);
2372
			return -1;
2373
		}
2374
2375
		$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,";
2376
		$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,";
2377
		$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,";
2378
		$sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units,";
2379
		$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,";
2380
		if (empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED)) {
2381
			$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,";
2382
		} else {
2383
			$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,";
2384
		}
2385
2386
		//For MultiCompany
2387
		//PMP per entity & Stocks Sharings stock_reel includes only stocks shared with this entity
2388
		$separatedEntityPMP = false;	// Set to true to get the AWP from table llx_product_perentity instead of field 'pmp' into llx_product.
2389
		$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.
2390
		$visibleWarehousesEntities = $conf->entity;
2391
		if (!empty($conf->global->MULTICOMPANY_PRODUCT_SHARING_ENABLED)) {
2392
			if (!empty($conf->global->MULTICOMPANY_PMP_PER_ENTITY_ENABLED)) {
2393
				$checkPMPPerEntity = $this->db->query("SELECT pmp FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = ".((int) $id)." AND entity = ".(int) $conf->entity);
2394
				if ($this->db->num_rows($checkPMPPerEntity)>0) {
2395
					$separatedEntityPMP = true;
2396
				}
2397
			}
2398
			global $mc;
2399
			$separatedStock = true;
2400
			if (isset($mc->sharings['stock']) && !empty($mc->sharings['stock'])) {
2401
				$visibleWarehousesEntities .= "," . implode(",", $mc->sharings['stock']);
2402
			}
2403
		}
2404
		if ($separatedEntityPMP) {
2405
			$sql .= " ppe.pmp,";
2406
		} else {
2407
			$sql .= " p.pmp,";
2408
		}
2409
		$sql .= " p.datec, p.tms, p.import_key, p.entity, p.desiredstock, p.tobatch, p.batch_mask, p.fk_unit,";
2410
		$sql .= " p.fk_price_expression, p.price_autogen, p.model_pdf,";
2411
		if ($separatedStock) {
2412
			$sql .= " SUM(sp.reel) as stock";
2413
		} else {
2414
			$sql .= " p.stock";
2415
		}
2416
		$sql .= " FROM ".$this->db->prefix()."product as p";
2417
		if (!empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED) || $separatedEntityPMP) {
2418
			$sql .= " LEFT JOIN " . $this->db->prefix() . "product_perentity as ppe ON ppe.fk_product = p.rowid AND ppe.entity = " . ((int) $conf->entity);
2419
		}
2420
		if ($separatedStock) {
2421
			$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)."))";
2422
		}
2423
2424
		if ($id) {
2425
			$sql .= " WHERE p.rowid = ".((int) $id);
2426
		} else {
2427
			$sql .= " WHERE p.entity IN (".getEntity($this->element).")";
2428
			if ($ref) {
2429
				$sql .= " AND p.ref = '".$this->db->escape($ref)."'";
2430
			} elseif ($ref_ext) {
2431
				$sql .= " AND p.ref_ext = '".$this->db->escape($ref_ext)."'";
2432
			} elseif ($barcode) {
2433
				$sql .= " AND p.barcode = '".$this->db->escape($barcode)."'";
2434
			}
2435
		}
2436
		if ($separatedStock) {
2437
			$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,";
2438
			$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,";
2439
			$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,";
2440
			$sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units,";
2441
			$sql .= " p.surface, p.surface_units, p.volume, p.volume_units, p.barcode, p.fk_barcode_type, p.finished,";
2442
			if (empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED)) {
2443
				$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,";
2444
			} else {
2445
				$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,";
2446
			}
2447
			if ($separatedEntityPMP) {
2448
				$sql .= " ppe.pmp,";
2449
			} else {
2450
				$sql .= " p.pmp,";
2451
			}
2452
			$sql .= " p.datec, p.tms, p.import_key, p.entity, p.desiredstock, p.tobatch, p.batch_mask, p.fk_unit,";
2453
			$sql .= " p.fk_price_expression, p.price_autogen, p.model_pdf";
2454
			if (!$separatedStock) {
2455
				$sql .= ", p.stock";
2456
			}
2457
		}
2458
2459
		$resql = $this->db->query($sql);
2460
		if ($resql) {
2461
			unset($this->oldcopy);
2462
2463
			if ($this->db->num_rows($resql) > 0) {
2464
				$obj = $this->db->fetch_object($resql);
2465
2466
				$this->id = $obj->rowid;
2467
				$this->ref = $obj->ref;
2468
				$this->ref_ext = $obj->ref_ext;
2469
				$this->label = $obj->label;
2470
				$this->description = $obj->description;
2471
				$this->url = $obj->url;
2472
				$this->note_public = $obj->note_public;
2473
				$this->note_private = $obj->note_private;
2474
				$this->note = $obj->note_private; // deprecated
2475
2476
				$this->type = $obj->fk_product_type;
2477
				$this->status = $obj->tosell;
2478
				$this->status_buy = $obj->tobuy;
2479
				$this->status_batch = $obj->tobatch;
2480
				$this->batch_mask = $obj->batch_mask;
2481
2482
				$this->customcode = $obj->customcode;
2483
				$this->country_id = $obj->fk_country;
2484
				$this->country_code = getCountry($this->country_id, 2, $this->db);
2485
				$this->state_id = $obj->fk_state;
2486
				$this->lifetime = $obj->lifetime;
2487
				$this->qc_frequency = $obj->qc_frequency;
2488
				$this->price = $obj->price;
2489
				$this->price_ttc = $obj->price_ttc;
2490
				$this->price_min = $obj->price_min;
2491
				$this->price_min_ttc = $obj->price_min_ttc;
2492
				$this->price_base_type = $obj->price_base_type;
2493
				$this->cost_price = $obj->cost_price;
2494
				$this->default_vat_code = $obj->default_vat_code;
2495
				$this->tva_tx = $obj->tva_tx;
2496
				//! French VAT NPR
2497
				$this->tva_npr = $obj->tva_npr;
2498
				$this->recuperableonly = $obj->tva_npr; // For backward compatibility
2499
				//! Local taxes
2500
				$this->localtax1_tx = $obj->localtax1_tx;
2501
				$this->localtax2_tx = $obj->localtax2_tx;
2502
				$this->localtax1_type = $obj->localtax1_type;
2503
				$this->localtax2_type = $obj->localtax2_type;
2504
2505
				$this->finished = $obj->finished;
2506
				$this->fk_default_bom = $obj->fk_default_bom;
2507
2508
				$this->duration = $obj->duration;
2509
				$this->duration_value = substr($obj->duration, 0, dol_strlen($obj->duration) - 1);
2510
				$this->duration_unit = substr($obj->duration, -1);
2511
				$this->canvas = $obj->canvas;
2512
				$this->net_measure = $obj->net_measure;
2513
				$this->net_measure_units = $obj->net_measure_units;
2514
				$this->weight = $obj->weight;
2515
				$this->weight_units = $obj->weight_units;
2516
				$this->length = $obj->length;
2517
				$this->length_units = $obj->length_units;
2518
				$this->width = $obj->width;
2519
				$this->width_units = $obj->width_units;
2520
				$this->height = $obj->height;
2521
				$this->height_units = $obj->height_units;
2522
2523
				$this->surface = $obj->surface;
2524
				$this->surface_units = $obj->surface_units;
2525
				$this->volume = $obj->volume;
2526
				$this->volume_units = $obj->volume_units;
2527
				$this->barcode = $obj->barcode;
2528
				$this->barcode_type = $obj->fk_barcode_type;
2529
2530
				$this->accountancy_code_buy = $obj->accountancy_code_buy;
2531
				$this->accountancy_code_buy_intra = $obj->accountancy_code_buy_intra;
2532
				$this->accountancy_code_buy_export = $obj->accountancy_code_buy_export;
2533
				$this->accountancy_code_sell = $obj->accountancy_code_sell;
2534
				$this->accountancy_code_sell_intra = $obj->accountancy_code_sell_intra;
2535
				$this->accountancy_code_sell_export = $obj->accountancy_code_sell_export;
2536
2537
				$this->fk_default_warehouse = $obj->fk_default_warehouse;
2538
				$this->fk_default_workstation = $obj->fk_default_workstation;
2539
				$this->seuil_stock_alerte = $obj->seuil_stock_alerte;
2540
				$this->desiredstock = $obj->desiredstock;
2541
				$this->stock_reel = $obj->stock;
2542
				$this->pmp = $obj->pmp;
2543
2544
				$this->date_creation = $obj->datec;
2545
				$this->date_modification = $obj->tms;
2546
				$this->import_key = $obj->import_key;
2547
				$this->entity = $obj->entity;
2548
2549
				$this->ref_ext = $obj->ref_ext;
2550
				$this->fk_price_expression = $obj->fk_price_expression;
2551
				$this->fk_unit = $obj->fk_unit;
2552
				$this->price_autogen = $obj->price_autogen;
2553
				$this->model_pdf = $obj->model_pdf;
2554
2555
				$this->mandatory_period = $obj->mandatory_period;
2556
2557
				$this->db->free($resql);
2558
2559
				// fetch optionals attributes and labels
2560
				$this->fetch_optionals();
2561
2562
				// Multilangs
2563
				if (getDolGlobalInt('MAIN_MULTILANGS') && empty($ignore_lang_load)) {
2564
					$this->getMultiLangs();
2565
				}
2566
2567
				// Load multiprices array
2568
				if (!empty($conf->global->PRODUIT_MULTIPRICES) && empty($ignore_price_load)) {                // prices per segment
2569
					for ($i = 1; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++) {
2570
						$sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
2571
						$sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
2572
						$sql .= " FROM ".$this->db->prefix()."product_price";
2573
						$sql .= " WHERE entity IN (".getEntity('productprice').")";
2574
						$sql .= " AND price_level=".((int) $i);
2575
						$sql .= " AND fk_product = ".((int) $this->id);
2576
						$sql .= " ORDER BY date_price DESC, rowid DESC";	// Get the most recent line
2577
						$sql .= " LIMIT 1";									// Only the first one
2578
						$resql = $this->db->query($sql);
2579
						if ($resql) {
2580
							$result = $this->db->fetch_array($resql);
2581
2582
							$this->multiprices[$i] = $result ? $result["price"] : null;
2583
							$this->multiprices_ttc[$i] = $result ? $result["price_ttc"] : null;
2584
							$this->multiprices_min[$i] =  $result ? $result["price_min"] : null;
2585
							$this->multiprices_min_ttc[$i] = $result ? $result["price_min_ttc"] : null;
2586
							$this->multiprices_base_type[$i] = $result ? $result["price_base_type"] : null;
2587
							// Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
2588
							$this->multiprices_tva_tx[$i] = $result ? $result["tva_tx"].($result ? ' ('.$result['default_vat_code'].')' : '') : null;
2589
							$this->multiprices_recuperableonly[$i] = $result ? $result["recuperableonly"] : null;
2590
2591
							// Price by quantity
2592
							/*
2593
							 $this->prices_by_qty[$i]=$result["price_by_qty"];
2594
							 $this->prices_by_qty_id[$i]=$result["rowid"];
2595
							 // Récuperation de la liste des prix selon qty si flag positionné
2596
							 if ($this->prices_by_qty[$i] == 1)
2597
							 {
2598
							 $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
2599
							 $sql.= " FROM ".$this->db->prefix()."product_price_by_qty";
2600
							 $sql.= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
2601
							 $sql.= " ORDER BY quantity ASC";
2602
							 $resultat=array();
2603
							 $resql = $this->db->query($sql);
2604
							 if ($resql)
2605
							 {
2606
							 $ii=0;
2607
							 while ($result= $this->db->fetch_array($resql)) {
2608
							 $resultat[$ii]=array();
2609
							 $resultat[$ii]["rowid"]=$result["rowid"];
2610
							 $resultat[$ii]["price"]= $result["price"];
2611
							 $resultat[$ii]["unitprice"]= $result["unitprice"];
2612
							 $resultat[$ii]["quantity"]= $result["quantity"];
2613
							 $resultat[$ii]["remise_percent"]= $result["remise_percent"];
2614
							 $resultat[$ii]["remise"]= $result["remise"];                    // deprecated
2615
							 $resultat[$ii]["price_base_type"]= $result["price_base_type"];
2616
							 $ii++;
2617
							 }
2618
							 $this->prices_by_qty_list[$i]=$resultat;
2619
							 }
2620
							 else
2621
							 {
2622
							 dol_print_error($this->db);
2623
							 return -1;
2624
							 }
2625
							 }*/
2626
						} else {
2627
							$this->error = $this->db->lasterror;
2628
							return -1;
2629
						}
2630
					}
2631
				} elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES) && empty($ignore_price_load)) {            // prices per customers
2632
					// Nothing loaded by default. List may be very long.
2633
				} elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY) && empty($ignore_price_load)) {    // prices per quantity
2634
					$sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
2635
					$sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid";
2636
					$sql .= " FROM ".$this->db->prefix()."product_price";
2637
					$sql .= " WHERE fk_product = ".((int) $this->id);
2638
					$sql .= " ORDER BY date_price DESC, rowid DESC";
2639
					$sql .= " LIMIT 1";
2640
					$resql = $this->db->query($sql);
2641
					if ($resql) {
2642
						$result = $this->db->fetch_array($resql);
2643
2644
						// Price by quantity
2645
						$this->prices_by_qty[0] = $result["price_by_qty"];
2646
						$this->prices_by_qty_id[0] = $result["rowid"];
2647
						// Récuperation de la liste des prix selon qty si flag positionné
2648
						if ($this->prices_by_qty[0] == 1) {
2649
							$sql = "SELECT rowid,price, unitprice, quantity, remise_percent, remise, remise, price_base_type";
2650
							$sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
2651
							$sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[0]);
2652
							$sql .= " ORDER BY quantity ASC";
2653
							$resultat = array();
2654
							$resql = $this->db->query($sql);
2655
							if ($resql) {
2656
								$ii = 0;
2657
								while ($result = $this->db->fetch_array($resql)) {
2658
									$resultat[$ii] = array();
2659
									$resultat[$ii]["rowid"] = $result["rowid"];
2660
									$resultat[$ii]["price"] = $result["price"];
2661
									$resultat[$ii]["unitprice"] = $result["unitprice"];
2662
									$resultat[$ii]["quantity"] = $result["quantity"];
2663
									$resultat[$ii]["remise_percent"] = $result["remise_percent"];
2664
									//$resultat[$ii]["remise"]= $result["remise"];                    // deprecated
2665
									$resultat[$ii]["price_base_type"] = $result["price_base_type"];
2666
									$ii++;
2667
								}
2668
								$this->prices_by_qty_list[0] = $resultat;
2669
							} else {
2670
								$this->error = $this->db->lasterror;
2671
								return -1;
2672
							}
2673
						}
2674
					} else {
2675
						$this->error = $this->db->lasterror;
2676
						return -1;
2677
					}
2678
				} elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES) && empty($ignore_price_load)) {    // prices per customer and quantity
2679
					for ($i = 1; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++) {
2680
						$sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
2681
						$sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
2682
						$sql .= " FROM ".$this->db->prefix()."product_price";
2683
						$sql .= " WHERE entity IN (".getEntity('productprice').")";
2684
						$sql .= " AND price_level=".((int) $i);
2685
						$sql .= " AND fk_product = ".((int) $this->id);
2686
						$sql .= " ORDER BY date_price DESC, rowid DESC";
2687
						$sql .= " LIMIT 1";
2688
						$resql = $this->db->query($sql);
2689
						if ($resql) {
2690
							$result = $this->db->fetch_array($resql);
2691
2692
							$this->multiprices[$i] = $result["price"];
2693
							$this->multiprices_ttc[$i] = $result["price_ttc"];
2694
							$this->multiprices_min[$i] = $result["price_min"];
2695
							$this->multiprices_min_ttc[$i] = $result["price_min_ttc"];
2696
							$this->multiprices_base_type[$i] = $result["price_base_type"];
2697
							// Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
2698
							$this->multiprices_tva_tx[$i] = $result["tva_tx"]; // TODO Add ' ('.$result['default_vat_code'].')'
2699
							$this->multiprices_recuperableonly[$i] = $result["recuperableonly"];
2700
2701
							// Price by quantity
2702
							$this->prices_by_qty[$i] = $result["price_by_qty"];
2703
							$this->prices_by_qty_id[$i] = $result["rowid"];
2704
							// Récuperation de la liste des prix selon qty si flag positionné
2705
							if ($this->prices_by_qty[$i] == 1) {
2706
								$sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
2707
								$sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
2708
								$sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
2709
								$sql .= " ORDER BY quantity ASC";
2710
								$resultat = array();
2711
								$resql = $this->db->query($sql);
2712
								if ($resql) {
2713
									$ii = 0;
2714
									while ($result = $this->db->fetch_array($resql)) {
2715
										$resultat[$ii] = array();
2716
										$resultat[$ii]["rowid"] = $result["rowid"];
2717
										$resultat[$ii]["price"] = $result["price"];
2718
										$resultat[$ii]["unitprice"] = $result["unitprice"];
2719
										$resultat[$ii]["quantity"] = $result["quantity"];
2720
										$resultat[$ii]["remise_percent"] = $result["remise_percent"];
2721
										$resultat[$ii]["remise"] = $result["remise"]; // deprecated
2722
										$resultat[$ii]["price_base_type"] = $result["price_base_type"];
2723
										$ii++;
2724
									}
2725
									$this->prices_by_qty_list[$i] = $resultat;
2726
								} else {
2727
									$this->error = $this->db->lasterror;
2728
									return -1;
2729
								}
2730
							}
2731
						} else {
2732
							$this->error = $this->db->lasterror;
2733
							return -1;
2734
						}
2735
					}
2736
				}
2737
2738
				if (isModEnabled('dynamicprices') && !empty($this->fk_price_expression) && empty($ignore_expression)) {
2739
					include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2740
					$priceparser = new PriceParser($this->db);
2741
					$price_result = $priceparser->parseProduct($this);
2742
					if ($price_result >= 0) {
2743
						$this->price = $price_result;
2744
						// Calculate the VAT
2745
						$this->price_ttc = price2num($this->price) * (1 + ($this->tva_tx / 100));
2746
						$this->price_ttc = price2num($this->price_ttc, 'MU');
2747
					}
2748
				}
2749
2750
				// We should not load stock during the fetch. If someone need stock of product, he must call load_stock after fetching product.
2751
				// Instead we just init the stock_warehouse array
2752
				$this->stock_warehouse = array();
2753
2754
				return 1;
2755
			} else {
2756
				return 0;
2757
			}
2758
		} else {
2759
			$this->error = $this->db->lasterror();
2760
			return -1;
2761
		}
2762
	}
2763
2764
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2765
	/**
2766
	 *  Charge tableau des stats OF pour le produit/service
2767
	 *
2768
	 * @param  int $socid Id societe
2769
	 * @return int                     Array of stats in $this->stats_mo, <0 if ko or >0 if ok
2770
	 */
2771
	public function load_stats_mo($socid = 0)
2772
	{
2773
		// phpcs:enable
2774
		global $user, $hookmanager, $action;
2775
2776
		$error = 0;
2777
2778
		foreach (array('toconsume', 'consumed', 'toproduce', 'produced') as $role) {
2779
			$this->stats_mo['customers_'.$role] = 0;
2780
			$this->stats_mo['nb_'.$role] = 0;
2781
			$this->stats_mo['qty_'.$role] = 0;
2782
2783
			$sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
2784
			$sql .= " SUM(mp.qty) as qty";
2785
			$sql .= " FROM ".$this->db->prefix()."mrp_mo as c";
2786
			$sql .= " INNER JOIN ".$this->db->prefix()."mrp_production as mp ON mp.fk_mo=c.rowid";
2787
			if (empty($user->rights->societe->client->voir) && !$socid) {
2788
				$sql .= "INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc=c.fk_soc AND sc.fk_user = ".((int) $user->id);
2789
			}
2790
			$sql .= " WHERE ";
2791
			$sql .= " c.entity IN (".getEntity('mo').")";
2792
2793
			$sql .= " AND mp.fk_product = ".((int) $this->id);
2794
			$sql .= " AND mp.role ='".$this->db->escape($role)."'";
2795
			if ($socid > 0) {
2796
				$sql .= " AND c.fk_soc = ".((int) $socid);
2797
			}
2798
2799
			$result = $this->db->query($sql);
2800
			if ($result) {
2801
				$obj = $this->db->fetch_object($result);
2802
				$this->stats_mo['customers_'.$role] = $obj->nb_customers ? $obj->nb_customers : 0;
2803
				$this->stats_mo['nb_'.$role] = $obj->nb ? $obj->nb : 0;
2804
				$this->stats_mo['qty_'.$role] = $obj->qty ? price2num($obj->qty, 'MS') : 0;		// qty may be a float due to the SUM()
2805
			} else {
2806
				$this->error = $this->db->error();
2807
				$error++;
2808
			}
2809
		}
2810
2811
		if (!empty($error)) {
2812
			return -1;
2813
		}
2814
2815
		$parameters = array('socid' => $socid);
2816
		$reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
2817
		if ($reshook > 0) {
2818
			$this->stats_mo = $hookmanager->resArray['stats_mo'];
2819
		}
2820
2821
		return 1;
2822
	}
2823
2824
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2825
	/**
2826
	 *  Charge tableau des stats OF pour le produit/service
2827
	 *
2828
	 * @param  int $socid Id societe
2829
	 * @return int                     Array of stats in $this->stats_bom, <0 if ko or >0 if ok
2830
	 */
2831
	public function load_stats_bom($socid = 0)
2832
	{
2833
		// phpcs:enable
2834
		global $user, $hookmanager, $action;
2835
2836
		$error = 0;
2837
2838
		$this->stats_bom['nb_toproduce'] = 0;
2839
		$this->stats_bom['nb_toconsume'] = 0;
2840
		$this->stats_bom['qty_toproduce'] = 0;
2841
		$this->stats_bom['qty_toconsume'] = 0;
2842
2843
		$sql = "SELECT COUNT(DISTINCT b.rowid) as nb_toproduce,";
2844
		$sql .= " SUM(b.qty) as qty_toproduce";
2845
		$sql .= " FROM ".$this->db->prefix()."bom_bom as b";
2846
		$sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom=b.rowid";
2847
		$sql .= " WHERE ";
2848
		$sql .= " b.entity IN (".getEntity('bom').")";
2849
		$sql .= " AND b.fk_product =".((int) $this->id);
2850
		$sql .= " GROUP BY b.rowid";
2851
2852
		$result = $this->db->query($sql);
2853
		if ($result) {
2854
			$obj = $this->db->fetch_object($result);
2855
			$this->stats_bom['nb_toproduce'] = !empty($obj->nb_toproduce) ? $obj->nb_toproduce : 0;
2856
			$this->stats_bom['qty_toproduce'] = !empty($obj->qty_toproduce) ? price2num($obj->qty_toproduce) : 0;
2857
		} else {
2858
			$this->error = $this->db->error();
2859
			$error++;
2860
		}
2861
2862
		$sql = "SELECT COUNT(DISTINCT bl.rowid) as nb_toconsume,";
2863
		$sql .= " SUM(bl.qty) as qty_toconsume";
2864
		$sql .= " FROM ".$this->db->prefix()."bom_bom as b";
2865
		$sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom=b.rowid";
2866
		$sql .= " WHERE ";
2867
		$sql .= " b.entity IN (".getEntity('bom').")";
2868
		$sql .= " AND bl.fk_product =".((int) $this->id);
2869
2870
		$result = $this->db->query($sql);
2871
		if ($result) {
2872
			$obj = $this->db->fetch_object($result);
2873
			$this->stats_bom['nb_toconsume'] = !empty($obj->nb_toconsume) ? $obj->nb_toconsume : 0;
2874
			$this->stats_bom['qty_toconsume'] = !empty($obj->qty_toconsume) ? price2num($obj->qty_toconsume) : 0;
2875
		} else {
2876
			$this->error = $this->db->error();
2877
			$error++;
2878
		}
2879
2880
		if (!empty($error)) {
2881
			return -1;
2882
		}
2883
2884
		$parameters = array('socid' => $socid);
2885
		$reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
2886
		if ($reshook > 0) {
2887
			$this->stats_bom = $hookmanager->resArray['stats_bom'];
2888
		}
2889
2890
		return 1;
2891
	}
2892
2893
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2894
	/**
2895
	 *  Charge tableau des stats propale pour le produit/service
2896
	 *
2897
	 * @param  int $socid Id societe
2898
	 * @return int                     Array of stats in $this->stats_propale, <0 if ko or >0 if ok
2899
	 */
2900
	public function load_stats_propale($socid = 0)
2901
	{
2902
		// phpcs:enable
2903
		global $conf, $user, $hookmanager, $action;
2904
2905
		$sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_customers, COUNT(DISTINCT p.rowid) as nb,";
2906
		$sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
2907
		$sql .= " FROM ".$this->db->prefix()."propaldet as pd";
2908
		$sql .= ", ".$this->db->prefix()."propal as p";
2909
		$sql .= ", ".$this->db->prefix()."societe as s";
2910
		if (empty($user->rights->societe->client->voir) && !$socid) {
2911
			$sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
2912
		}
2913
		$sql .= " WHERE p.rowid = pd.fk_propal";
2914
		$sql .= " AND p.fk_soc = s.rowid";
2915
		$sql .= " AND p.entity IN (".getEntity('propal').")";
2916
		$sql .= " AND pd.fk_product = ".((int) $this->id);
2917
		if (empty($user->rights->societe->client->voir) && !$socid) {
2918
			$sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
2919
		}
2920
		//$sql.= " AND pr.fk_statut != 0";
2921
		if ($socid > 0) {
2922
			$sql .= " AND p.fk_soc = ".((int) $socid);
2923
		}
2924
2925
		$result = $this->db->query($sql);
2926
		if ($result) {
2927
			$obj = $this->db->fetch_object($result);
2928
			$this->stats_propale['customers'] = $obj->nb_customers;
2929
			$this->stats_propale['nb'] = $obj->nb;
2930
			$this->stats_propale['rows'] = $obj->nb_rows;
2931
			$this->stats_propale['qty'] = $obj->qty ? $obj->qty : 0;
2932
2933
			// if it's a virtual product, maybe it is in proposal by extension
2934
			if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) {
2935
				$TFather = $this->getFather();
2936
				if (is_array($TFather) && !empty($TFather)) {
2937
					foreach ($TFather as &$fatherData) {
2938
						$pFather = new Product($this->db);
2939
						$pFather->id = $fatherData['id'];
2940
						$qtyCoef = $fatherData['qty'];
2941
2942
						if ($fatherData['incdec']) {
2943
							$pFather->load_stats_propale($socid);
2944
2945
							$this->stats_propale['customers'] += $pFather->stats_propale['customers'];
2946
							$this->stats_propale['nb'] += $pFather->stats_propale['nb'];
2947
							$this->stats_propale['rows'] += $pFather->stats_propale['rows'];
2948
							$this->stats_propale['qty'] += $pFather->stats_propale['qty'] * $qtyCoef;
2949
						}
2950
					}
2951
				}
2952
			}
2953
2954
			$parameters = array('socid' => $socid);
2955
			$reshook = $hookmanager->executeHooks('loadStatsCustomerProposal', $parameters, $this, $action);
2956
			if ($reshook > 0) {
2957
				$this->stats_propale = $hookmanager->resArray['stats_propale'];
2958
			}
2959
2960
			return 1;
2961
		} else {
2962
			$this->error = $this->db->error();
2963
			return -1;
2964
		}
2965
	}
2966
2967
2968
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2969
	/**
2970
	 *  Charge tableau des stats propale pour le produit/service
2971
	 *
2972
	 * @param  int $socid Id thirdparty
2973
	 * @return int                     Array of stats in $this->stats_proposal_supplier, <0 if ko or >0 if ok
2974
	 */
2975
	public function load_stats_proposal_supplier($socid = 0)
2976
	{
2977
		// phpcs:enable
2978
		global $conf, $user, $hookmanager, $action;
2979
2980
		$sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_suppliers, COUNT(DISTINCT p.rowid) as nb,";
2981
		$sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
2982
		$sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as pd";
2983
		$sql .= ", ".$this->db->prefix()."supplier_proposal as p";
2984
		$sql .= ", ".$this->db->prefix()."societe as s";
2985
		if (empty($user->rights->societe->client->voir) && !$socid) {
2986
			$sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
2987
		}
2988
		$sql .= " WHERE p.rowid = pd.fk_supplier_proposal";
2989
		$sql .= " AND p.fk_soc = s.rowid";
2990
		$sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
2991
		$sql .= " AND pd.fk_product = ".((int) $this->id);
2992
		if (empty($user->rights->societe->client->voir) && !$socid) {
2993
			$sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
2994
		}
2995
		//$sql.= " AND pr.fk_statut != 0";
2996
		if ($socid > 0) {
2997
			$sql .= " AND p.fk_soc = ".((int) $socid);
2998
		}
2999
3000
		$result = $this->db->query($sql);
3001
		if ($result) {
3002
			$obj = $this->db->fetch_object($result);
3003
			$this->stats_proposal_supplier['suppliers'] = $obj->nb_suppliers;
3004
			$this->stats_proposal_supplier['nb'] = $obj->nb;
3005
			$this->stats_proposal_supplier['rows'] = $obj->nb_rows;
3006
			$this->stats_proposal_supplier['qty'] = $obj->qty ? $obj->qty : 0;
3007
3008
			$parameters = array('socid' => $socid);
3009
			$reshook = $hookmanager->executeHooks('loadStatsSupplierProposal', $parameters, $this, $action);
3010
			if ($reshook > 0) {
3011
				$this->stats_proposal_supplier = $hookmanager->resArray['stats_proposal_supplier'];
3012
			}
3013
3014
			return 1;
3015
		} else {
3016
			$this->error = $this->db->error();
3017
			return -1;
3018
		}
3019
	}
3020
3021
3022
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3023
	/**
3024
	 *  Charge tableau des stats commande client pour le produit/service
3025
	 *
3026
	 * @param  int    $socid           Id societe pour filtrer sur une societe
3027
	 * @param  string $filtrestatut    Id statut pour filtrer sur un statut
3028
	 * @param  int    $forVirtualStock Ignore rights filter for virtual stock calculation. Set when load_stats_commande is used for virtual stock calculation.
3029
	 * @return integer                 Array of stats in $this->stats_commande (nb=nb of order, qty=qty ordered), <0 if ko or >0 if ok
3030
	 */
3031
	public function load_stats_commande($socid = 0, $filtrestatut = '', $forVirtualStock = 0)
3032
	{
3033
		// phpcs:enable
3034
		global $conf, $user, $hookmanager, $action;
3035
3036
		$sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3037
		$sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3038
		$sql .= " FROM ".$this->db->prefix()."commandedet as cd";
3039
		$sql .= ", ".$this->db->prefix()."commande as c";
3040
		$sql .= ", ".$this->db->prefix()."societe as s";
3041
		if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3042
			$sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3043
		}
3044
		$sql .= " WHERE c.rowid = cd.fk_commande";
3045
		$sql .= " AND c.fk_soc = s.rowid";
3046
		$sql .= " AND c.entity IN (".getEntity($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE) ? 'stock' : 'commande').")";
3047
		$sql .= " AND cd.fk_product = ".((int) $this->id);
3048
		if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3049
			$sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3050
		}
3051
		if ($socid > 0) {
3052
			$sql .= " AND c.fk_soc = ".((int) $socid);
3053
		}
3054
		if ($filtrestatut <> '') {
3055
			$sql .= " AND c.fk_statut in (".$this->db->sanitize($filtrestatut).")";
3056
		}
3057
3058
		$result = $this->db->query($sql);
3059
		if ($result) {
3060
			$obj = $this->db->fetch_object($result);
3061
			$this->stats_commande['customers'] = $obj->nb_customers;
3062
			$this->stats_commande['nb'] = $obj->nb;
3063
			$this->stats_commande['rows'] = $obj->nb_rows;
3064
			$this->stats_commande['qty'] = $obj->qty ? $obj->qty : 0;
3065
3066
			// if it's a virtual product, maybe it is in order by extension
3067
			if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) {
3068
				$TFather = $this->getFather();
3069
				if (is_array($TFather) && !empty($TFather)) {
3070
					foreach ($TFather as &$fatherData) {
3071
						$pFather = new Product($this->db);
3072
						$pFather->id = $fatherData['id'];
3073
						$qtyCoef = $fatherData['qty'];
3074
3075
						if ($fatherData['incdec']) {
3076
							$pFather->load_stats_commande($socid, $filtrestatut);
3077
3078
							$this->stats_commande['customers'] += $pFather->stats_commande['customers'];
3079
							$this->stats_commande['nb'] += $pFather->stats_commande['nb'];
3080
							$this->stats_commande['rows'] += $pFather->stats_commande['rows'];
3081
							$this->stats_commande['qty'] += $pFather->stats_commande['qty'] * $qtyCoef;
3082
						}
3083
					}
3084
				}
3085
			}
3086
3087
			// If stock decrease is on invoice validation, the theorical stock continue to
3088
			// count the orders to ship in theorical stock when some are already removed by invoice validation.
3089
			if ($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_ON_BILL)) {
3090
				if (!empty($conf->global->DECREASE_ONLY_UNINVOICEDPRODUCTS)) {
3091
					// If option DECREASE_ONLY_UNINVOICEDPRODUCTS is on, we make a compensation but only if order not yet invoice.
3092
					$adeduire = 0;
3093
					$sql = "SELECT sum(fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3094
					$sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid ";
3095
					$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'))";
3096
					$sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_source = c.rowid ";
3097
					$sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND c.facture = 0 AND fd.fk_product = ".((int) $this->id);
3098
					dol_syslog(__METHOD__.":: sql $sql", LOG_NOTICE);
3099
					$resql = $this->db->query($sql);
3100
					if ($resql) {
3101
						if ($this->db->num_rows($resql) > 0) {
3102
							$obj = $this->db->fetch_object($resql);
3103
							$adeduire += $obj->count;
3104
						}
3105
					}
3106
3107
					$this->stats_commande['qty'] -= $adeduire;
3108
				} else {
3109
					// If option DECREASE_ONLY_UNINVOICEDPRODUCTS is off, we make a compensation with lines of invoices linked to the order
3110
					include_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
3111
3112
					// For every order having invoice already validated we need to decrease stock cause it's in physical stock
3113
					$adeduire = 0;
3114
					$sql = 'SELECT sum(fd.qty) as count FROM '.MAIN_DB_PREFIX.'facturedet as fd ';
3115
					$sql .= ' JOIN '.MAIN_DB_PREFIX.'facture as f ON fd.fk_facture = f.rowid ';
3116
					$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'))";
3117
					$sql .= ' JOIN '.MAIN_DB_PREFIX.'commande as c ON el.fk_source = c.rowid ';
3118
					$sql .= ' WHERE c.fk_statut IN ('.$this->db->sanitize($filtrestatut).') AND f.fk_statut > '.Facture::STATUS_DRAFT.' AND fd.fk_product = '.((int) $this->id);
3119
					dol_syslog(__METHOD__.":: sql $sql", LOG_NOTICE);
3120
					$resql = $this->db->query($sql);
3121
					if ($resql) {
3122
						if ($this->db->num_rows($resql) > 0) {
3123
							$obj = $this->db->fetch_object($resql);
3124
							$adeduire += $obj->count;
3125
						}
3126
					}
3127
3128
					$this->stats_commande['qty'] -= $adeduire;
3129
				}
3130
			}
3131
3132
			$parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3133
			$reshook = $hookmanager->executeHooks('loadStatsCustomerOrder', $parameters, $this, $action);
3134
			if ($reshook > 0) {
3135
				$this->stats_commande = $hookmanager->resArray['stats_commande'];
3136
			}
3137
			return 1;
3138
		} else {
3139
			$this->error = $this->db->error();
3140
			return -1;
3141
		}
3142
	}
3143
3144
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3145
	/**
3146
	 *  Charge tableau des stats commande fournisseur pour le produit/service
3147
	 *
3148
	 * @param	int		$socid				Id societe pour filtrer sur une societe
3149
	 * @param	string	$filtrestatut		Id des statuts pour filtrer sur des statuts
3150
	 * @param	int		$forVirtualStock	Ignore rights filter for virtual stock calculation.
3151
	 * @param	int		$dateofvirtualstock	Date of virtual stock
3152
	 * @return	int							Array of stats in $this->stats_commande_fournisseur, <0 if ko or >0 if ok
3153
	 */
3154
	public function load_stats_commande_fournisseur($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3155
	{
3156
		// phpcs:enable
3157
		global $conf, $user, $hookmanager, $action;
3158
3159
		$sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_suppliers, COUNT(DISTINCT c.rowid) as nb,";
3160
		$sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3161
		$sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as cd";
3162
		$sql .= ", ".$this->db->prefix()."commande_fournisseur as c";
3163
		$sql .= ", ".$this->db->prefix()."societe as s";
3164
		if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3165
			$sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3166
		}
3167
		$sql .= " WHERE c.rowid = cd.fk_commande";
3168
		$sql .= " AND c.fk_soc = s.rowid";
3169
		$sql .= " AND c.entity IN (".getEntity($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE) ? 'stock' : 'supplier_order').")";
3170
		$sql .= " AND cd.fk_product = ".((int) $this->id);
3171
		if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3172
			$sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3173
		}
3174
		if ($socid > 0) {
3175
			$sql .= " AND c.fk_soc = ".((int) $socid);
3176
		}
3177
		if ($filtrestatut != '') {
3178
			$sql .= " AND c.fk_statut in (".$this->db->sanitize($filtrestatut).")"; // Peut valoir 0
3179
		}
3180
		if (!empty($dateofvirtualstock)) {
3181
			$sql .= " AND c.date_livraison <= '".$this->db->idate($dateofvirtualstock)."'";
3182
		}
3183
3184
		$result = $this->db->query($sql);
3185
		if ($result) {
3186
			$obj = $this->db->fetch_object($result);
3187
			$this->stats_commande_fournisseur['suppliers'] = $obj->nb_suppliers;
3188
			$this->stats_commande_fournisseur['nb'] = $obj->nb;
3189
			$this->stats_commande_fournisseur['rows'] = $obj->nb_rows;
3190
			$this->stats_commande_fournisseur['qty'] = $obj->qty ? $obj->qty : 0;
3191
3192
			$parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3193
			$reshook = $hookmanager->executeHooks('loadStatsSupplierOrder', $parameters, $this, $action);
3194
			if ($reshook > 0) {
3195
				$this->stats_commande_fournisseur = $hookmanager->resArray['stats_commande_fournisseur'];
3196
			}
3197
3198
			return 1;
3199
		} else {
3200
			$this->error = $this->db->error().' sql='.$sql;
3201
			return -1;
3202
		}
3203
	}
3204
3205
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3206
	/**
3207
	 *  Charge tableau des stats expedition client pour le produit/service
3208
	 *
3209
	 * @param   int         $socid                  Id societe pour filtrer sur une societe
3210
	 * @param   string      $filtrestatut           [=''] Ids order status separated by comma
3211
	 * @param   int         $forVirtualStock        Ignore rights filter for virtual stock calculation.
3212
	 * @param   string      $filterShipmentStatus   [=''] Ids shipment status separated by comma
3213
	 * @return  int                                 Array of stats in $this->stats_expedition, <0 if ko or >0 if ok
3214
	 */
3215
	public function load_stats_sending($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $filterShipmentStatus = '')
3216
	{
3217
		// phpcs:enable
3218
		global $conf, $user, $hookmanager, $action;
3219
3220
		$sql = "SELECT COUNT(DISTINCT e.fk_soc) as nb_customers, COUNT(DISTINCT e.rowid) as nb,";
3221
		$sql .= " COUNT(ed.rowid) as nb_rows, SUM(ed.qty) as qty";
3222
		$sql .= " FROM ".$this->db->prefix()."expeditiondet as ed";
3223
		$sql .= ", ".$this->db->prefix()."commandedet as cd";
3224
		$sql .= ", ".$this->db->prefix()."commande as c";
3225
		$sql .= ", ".$this->db->prefix()."expedition as e";
3226
		$sql .= ", ".$this->db->prefix()."societe as s";
3227
		if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3228
			$sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3229
		}
3230
		$sql .= " WHERE e.rowid = ed.fk_expedition";
3231
		$sql .= " AND c.rowid = cd.fk_commande";
3232
		$sql .= " AND e.fk_soc = s.rowid";
3233
		$sql .= " AND e.entity IN (".getEntity($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE) ? 'stock' : 'expedition').")";
3234
		$sql .= " AND ed.fk_origin_line = cd.rowid";
3235
		$sql .= " AND cd.fk_product = ".((int) $this->id);
3236
		if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3237
			$sql .= " AND e.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3238
		}
3239
		if ($socid > 0) {
3240
			$sql .= " AND e.fk_soc = ".((int) $socid);
3241
		}
3242
		if ($filtrestatut <> '') {
3243
			$sql .= " AND c.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3244
		}
3245
		if (!empty($filterShipmentStatus)) {
3246
			$sql .= " AND e.fk_statut IN (".$this->db->sanitize($filterShipmentStatus).")";
3247
		}
3248
3249
		$result = $this->db->query($sql);
3250
		if ($result) {
3251
			$obj = $this->db->fetch_object($result);
3252
			$this->stats_expedition['customers'] = $obj->nb_customers;
3253
			$this->stats_expedition['nb'] = $obj->nb;
3254
			$this->stats_expedition['rows'] = $obj->nb_rows;
3255
			$this->stats_expedition['qty'] = $obj->qty ? $obj->qty : 0;
3256
3257
			// if it's a virtual product, maybe it is in sending by extension
3258
			if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) {
3259
				$TFather = $this->getFather();
3260
				if (is_array($TFather) && !empty($TFather)) {
3261
					foreach ($TFather as &$fatherData) {
3262
						$pFather = new Product($this->db);
3263
						$pFather->id = $fatherData['id'];
3264
						$qtyCoef = $fatherData['qty'];
3265
3266
						if ($fatherData['incdec']) {
3267
							$pFather->load_stats_sending($socid, $filtrestatut, $forVirtualStock);
3268
3269
							$this->stats_expedition['customers'] += $pFather->stats_expedition['customers'];
3270
							$this->stats_expedition['nb'] += $pFather->stats_expedition['nb'];
3271
							$this->stats_expedition['rows'] += $pFather->stats_expedition['rows'];
3272
							$this->stats_expedition['qty'] += $pFather->stats_expedition['qty'] * $qtyCoef;
3273
						}
3274
					}
3275
				}
3276
			}
3277
3278
			$parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock, 'filterShipmentStatus' => $filterShipmentStatus);
3279
			$reshook = $hookmanager->executeHooks('loadStatsSending', $parameters, $this, $action);
3280
			if ($reshook > 0) {
3281
				$this->stats_expedition = $hookmanager->resArray['stats_expedition'];
3282
			}
3283
3284
			return 1;
3285
		} else {
3286
			$this->error = $this->db->error();
3287
			return -1;
3288
		}
3289
	}
3290
3291
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3292
	/**
3293
	 *  Charge tableau des stats réception fournisseur pour le produit/service
3294
	 *
3295
	 * @param  int    	$socid           Id societe pour filtrer sur une societe
3296
	 * @param  string 	$filtrestatut    Id statut pour filtrer sur un statut
3297
	 * @param  int    	$forVirtualStock Ignore rights filter for virtual stock calculation.
3298
	 * @param	int		$dateofvirtualstock	Date of virtual stock
3299
	 * @return int                     Array of stats in $this->stats_reception, <0 if ko or >0 if ok
3300
	 */
3301
	public function load_stats_reception($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3302
	{
3303
		// phpcs:enable
3304
		global $conf, $user, $hookmanager, $action;
3305
3306
		$sql = "SELECT COUNT(DISTINCT cf.fk_soc) as nb_suppliers, COUNT(DISTINCT cf.rowid) as nb,";
3307
		$sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
3308
		$sql .= " FROM ".$this->db->prefix()."commande_fournisseur_dispatch as fd";
3309
		$sql .= ", ".$this->db->prefix()."commande_fournisseur as cf";
3310
		$sql .= ", ".$this->db->prefix()."societe as s";
3311
		if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3312
			$sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3313
		}
3314
		$sql .= " WHERE cf.rowid = fd.fk_commande";
3315
		$sql .= " AND cf.fk_soc = s.rowid";
3316
		$sql .= " AND cf.entity IN (".getEntity($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE) ? 'stock' : 'supplier_order').")";
3317
		$sql .= " AND fd.fk_product = ".((int) $this->id);
3318
		if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3319
			$sql .= " AND cf.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3320
		}
3321
		if ($socid > 0) {
3322
			$sql .= " AND cf.fk_soc = ".((int) $socid);
3323
		}
3324
		if ($filtrestatut <> '') {
3325
			$sql .= " AND cf.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3326
		}
3327
		if (!empty($dateofvirtualstock)) {
3328
			$sql .= " AND fd.datec <= '".$this->db->idate($dateofvirtualstock)."'";
3329
		}
3330
3331
		$result = $this->db->query($sql);
3332
		if ($result) {
3333
			$obj = $this->db->fetch_object($result);
3334
			$this->stats_reception['suppliers'] = $obj->nb_suppliers;
3335
			$this->stats_reception['nb'] = $obj->nb;
3336
			$this->stats_reception['rows'] = $obj->nb_rows;
3337
			$this->stats_reception['qty'] = $obj->qty ? $obj->qty : 0;
3338
3339
			$parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3340
			$reshook = $hookmanager->executeHooks('loadStatsReception', $parameters, $this, $action);
3341
			if ($reshook > 0) {
3342
				$this->stats_reception = $hookmanager->resArray['stats_reception'];
3343
			}
3344
3345
			return 1;
3346
		} else {
3347
			$this->error = $this->db->error();
3348
			return -1;
3349
		}
3350
	}
3351
3352
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3353
	/**
3354
	 *  Charge tableau des stats production pour le produit/service
3355
	 *
3356
	 * @param  int    	$socid           Id societe pour filtrer sur une societe
3357
	 * @param  string 	$filtrestatut    Id statut pour filtrer sur un statut
3358
	 * @param  int    	$forVirtualStock Ignore rights filter for virtual stock calculation.
3359
	 * @param	int		$dateofvirtualstock	Date of virtual stock
3360
	 * @return integer                 Array of stats in $this->stats_mrptoproduce (nb=nb of order, qty=qty ordered), <0 if ko or >0 if ok
3361
	 */
3362
	public function load_stats_inproduction($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3363
	{
3364
		// phpcs:enable
3365
		global $conf, $user, $hookmanager, $action;
3366
3367
		$sql = "SELECT COUNT(DISTINCT m.fk_soc) as nb_customers, COUNT(DISTINCT m.rowid) as nb,";
3368
		$sql .= " COUNT(mp.rowid) as nb_rows, SUM(mp.qty) as qty, role";
3369
		$sql .= " FROM ".$this->db->prefix()."mrp_production as mp";
3370
		$sql .= ", ".$this->db->prefix()."mrp_mo as m";
3371
		$sql .= " LEFT JOIN ".$this->db->prefix()."societe as s ON s.rowid = m.fk_soc";
3372
		if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3373
			$sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3374
		}
3375
		$sql .= " WHERE m.rowid = mp.fk_mo";
3376
		$sql .= " AND m.entity IN (".getEntity($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE) ? 'stock' : 'mrp').")";
3377
		$sql .= " AND mp.fk_product = ".((int) $this->id);
3378
		if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3379
			$sql .= " AND m.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3380
		}
3381
		if ($socid > 0) {
3382
			$sql .= " AND m.fk_soc = ".((int) $socid);
3383
		}
3384
		if ($filtrestatut <> '') {
3385
			$sql .= " AND m.status IN (".$this->db->sanitize($filtrestatut).")";
3386
		}
3387
		if (!empty($dateofvirtualstock)) {
3388
			$sql .= " AND m.date_valid <= '".$this->db->idate($dateofvirtualstock)."'"; // better date to code ? end of production ?
3389
		}
3390
		$sql .= " GROUP BY role";
3391
3392
		$this->stats_mrptoconsume['customers'] = 0;
3393
		$this->stats_mrptoconsume['nb'] = 0;
3394
		$this->stats_mrptoconsume['rows'] = 0;
3395
		$this->stats_mrptoconsume['qty'] = 0;
3396
		$this->stats_mrptoproduce['customers'] = 0;
3397
		$this->stats_mrptoproduce['nb'] = 0;
3398
		$this->stats_mrptoproduce['rows'] = 0;
3399
		$this->stats_mrptoproduce['qty'] = 0;
3400
3401
		$result = $this->db->query($sql);
3402
		if ($result) {
3403
			while ($obj = $this->db->fetch_object($result)) {
3404
				if ($obj->role == 'toconsume') {
3405
					$this->stats_mrptoconsume['customers'] += $obj->nb_customers;
3406
					$this->stats_mrptoconsume['nb'] += $obj->nb;
3407
					$this->stats_mrptoconsume['rows'] += $obj->nb_rows;
3408
					$this->stats_mrptoconsume['qty'] += ($obj->qty ? $obj->qty : 0);
3409
				}
3410
				if ($obj->role == 'consumed') {
3411
					//$this->stats_mrptoconsume['customers'] += $obj->nb_customers;
3412
					//$this->stats_mrptoconsume['nb'] += $obj->nb;
3413
					//$this->stats_mrptoconsume['rows'] += $obj->nb_rows;
3414
					$this->stats_mrptoconsume['qty'] -= ($obj->qty ? $obj->qty : 0);
3415
				}
3416
				if ($obj->role == 'toproduce') {
3417
					$this->stats_mrptoproduce['customers'] += $obj->nb_customers;
3418
					$this->stats_mrptoproduce['nb'] += $obj->nb;
3419
					$this->stats_mrptoproduce['rows'] += $obj->nb_rows;
3420
					$this->stats_mrptoproduce['qty'] += ($obj->qty ? $obj->qty : 0);
3421
				}
3422
				if ($obj->role == 'produced') {
3423
					//$this->stats_mrptoproduce['customers'] += $obj->nb_customers;
3424
					//$this->stats_mrptoproduce['nb'] += $obj->nb;
3425
					//$this->stats_mrptoproduce['rows'] += $obj->nb_rows;
3426
					$this->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
3427
				}
3428
			}
3429
3430
			// Clean data
3431
			if ($this->stats_mrptoconsume['qty'] < 0) {
3432
				$this->stats_mrptoconsume['qty'] = 0;
3433
			}
3434
			if ($this->stats_mrptoproduce['qty'] < 0) {
3435
				$this->stats_mrptoproduce['qty'] = 0;
3436
			}
3437
3438
			$parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3439
			$reshook = $hookmanager->executeHooks('loadStatsInProduction', $parameters, $this, $action);
3440
			if ($reshook > 0) {
3441
				$this->stats_mrptoproduce = $hookmanager->resArray['stats_mrptoproduce'];
3442
			}
3443
3444
			return 1;
3445
		} else {
3446
			$this->error = $this->db->error();
3447
			return -1;
3448
		}
3449
	}
3450
3451
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3452
	/**
3453
	 *  Charge tableau des stats contrat pour le produit/service
3454
	 *
3455
	 * @param  int $socid Id societe
3456
	 * @return int                     Array of stats in $this->stats_contrat, <0 if ko or >0 if ok
3457
	 */
3458
	public function load_stats_contrat($socid = 0)
3459
	{
3460
		// phpcs:enable
3461
		global $conf, $user, $hookmanager, $action;
3462
3463
		$sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3464
		$sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3465
		$sql .= " FROM ".$this->db->prefix()."contratdet as cd";
3466
		$sql .= ", ".$this->db->prefix()."contrat as c";
3467
		$sql .= ", ".$this->db->prefix()."societe as s";
3468
		if (empty($user->rights->societe->client->voir) && !$socid) {
3469
			$sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3470
		}
3471
		$sql .= " WHERE c.rowid = cd.fk_contrat";
3472
		$sql .= " AND c.fk_soc = s.rowid";
3473
		$sql .= " AND c.entity IN (".getEntity('contract').")";
3474
		$sql .= " AND cd.fk_product = ".((int) $this->id);
3475
		if (empty($user->rights->societe->client->voir) && !$socid) {
3476
			$sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3477
		}
3478
		//$sql.= " AND c.statut != 0";
3479
		if ($socid > 0) {
3480
			$sql .= " AND c.fk_soc = ".((int) $socid);
3481
		}
3482
3483
		$result = $this->db->query($sql);
3484
		if ($result) {
3485
			$obj = $this->db->fetch_object($result);
3486
			$this->stats_contrat['customers'] = $obj->nb_customers;
3487
			$this->stats_contrat['nb'] = $obj->nb;
3488
			$this->stats_contrat['rows'] = $obj->nb_rows;
3489
			$this->stats_contrat['qty'] = $obj->qty ? $obj->qty : 0;
3490
3491
			// if it's a virtual product, maybe it is in contract by extension
3492
			if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) {
3493
				$TFather = $this->getFather();
3494
				if (is_array($TFather) && !empty($TFather)) {
3495
					foreach ($TFather as &$fatherData) {
3496
						$pFather = new Product($this->db);
3497
						$pFather->id = $fatherData['id'];
3498
						$qtyCoef = $fatherData['qty'];
3499
3500
						if ($fatherData['incdec']) {
3501
							$pFather->load_stats_contrat($socid);
3502
3503
							$this->stats_contrat['customers'] += $pFather->stats_contrat['customers'];
3504
							$this->stats_contrat['nb'] += $pFather->stats_contrat['nb'];
3505
							$this->stats_contrat['rows'] += $pFather->stats_contrat['rows'];
3506
							$this->stats_contrat['qty'] += $pFather->stats_contrat['qty'] * $qtyCoef;
3507
						}
3508
					}
3509
				}
3510
			}
3511
3512
			$parameters = array('socid' => $socid);
3513
			$reshook = $hookmanager->executeHooks('loadStatsContract', $parameters, $this, $action);
3514
			if ($reshook > 0) {
3515
				$this->stats_contrat = $hookmanager->resArray['stats_contrat'];
3516
			}
3517
3518
			return 1;
3519
		} else {
3520
			$this->error = $this->db->error().' sql='.$sql;
3521
			return -1;
3522
		}
3523
	}
3524
3525
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3526
	/**
3527
	 *  Charge tableau des stats facture pour le produit/service
3528
	 *
3529
	 * @param  int $socid Id societe
3530
	 * @return int                     Array of stats in $this->stats_facture, <0 if ko or >0 if ok
3531
	 */
3532
	public function load_stats_facture($socid = 0)
3533
	{
3534
		// phpcs:enable
3535
		global $db, $conf, $user, $hookmanager, $action;
3536
3537
		$sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
3538
		$sql .= " COUNT(fd.rowid) as nb_rows, SUM(".$this->db->ifsql('f.type != 2', 'fd.qty', 'fd.qty * -1').") as qty";
3539
		$sql .= " FROM ".$this->db->prefix()."facturedet as fd";
3540
		$sql .= ", ".$this->db->prefix()."facture as f";
3541
		$sql .= ", ".$this->db->prefix()."societe as s";
3542
		if (empty($user->rights->societe->client->voir) && !$socid) {
3543
			$sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3544
		}
3545
		$sql .= " WHERE f.rowid = fd.fk_facture";
3546
		$sql .= " AND f.fk_soc = s.rowid";
3547
		$sql .= " AND f.entity IN (".getEntity('invoice').")";
3548
		$sql .= " AND fd.fk_product = ".((int) $this->id);
3549
		if (empty($user->rights->societe->client->voir) && !$socid) {
3550
			$sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3551
		}
3552
		//$sql.= " AND f.fk_statut != 0";
3553
		if ($socid > 0) {
3554
			$sql .= " AND f.fk_soc = ".((int) $socid);
3555
		}
3556
3557
		$result = $this->db->query($sql);
3558
		if ($result) {
3559
			$obj = $this->db->fetch_object($result);
3560
			$this->stats_facture['customers'] = $obj->nb_customers;
3561
			$this->stats_facture['nb'] = $obj->nb;
3562
			$this->stats_facture['rows'] = $obj->nb_rows;
3563
			$this->stats_facture['qty'] = $obj->qty ? $obj->qty : 0;
3564
3565
			// if it's a virtual product, maybe it is in invoice by extension
3566
			if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) {
3567
				$TFather = $this->getFather();
3568
				if (is_array($TFather) && !empty($TFather)) {
3569
					foreach ($TFather as &$fatherData) {
3570
						$pFather = new Product($this->db);
3571
						$pFather->id = $fatherData['id'];
3572
						$qtyCoef = $fatherData['qty'];
3573
3574
						if ($fatherData['incdec']) {
3575
							$pFather->load_stats_facture($socid);
3576
3577
							$this->stats_facture['customers'] += $pFather->stats_facture['customers'];
3578
							$this->stats_facture['nb'] += $pFather->stats_facture['nb'];
3579
							$this->stats_facture['rows'] += $pFather->stats_facture['rows'];
3580
							$this->stats_facture['qty'] += $pFather->stats_facture['qty'] * $qtyCoef;
3581
						}
3582
					}
3583
				}
3584
			}
3585
3586
			$parameters = array('socid' => $socid);
3587
			$reshook = $hookmanager->executeHooks('loadStatsCustomerInvoice', $parameters, $this, $action);
3588
			if ($reshook > 0) {
3589
				$this->stats_facture = $hookmanager->resArray['stats_facture'];
3590
			}
3591
3592
			return 1;
3593
		} else {
3594
			$this->error = $this->db->error();
3595
			return -1;
3596
		}
3597
	}
3598
3599
3600
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3601
	/**
3602
	 *  Charge tableau des stats facture recurrentes pour le produit/service
3603
	 *
3604
	 * @param  int $socid Id societe
3605
	 * @return int                     Array of stats in $this->stats_facture, <0 if ko or >0 if ok
3606
	 */
3607
	public function load_stats_facturerec($socid = 0)
3608
	{
3609
		// phpcs:enable
3610
		global $db, $conf, $user, $hookmanager;
3611
3612
		$sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
3613
		$sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
3614
		$sql .= " FROM ".MAIN_DB_PREFIX."facturedet_rec as fd";
3615
		$sql .= ", ".MAIN_DB_PREFIX."facture_rec as f";
3616
		$sql .= ", ".MAIN_DB_PREFIX."societe as s";
3617
		if (empty($user->rights->societe->client->voir) && !$socid) {
3618
			$sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
3619
		}
3620
		$sql .= " WHERE f.rowid = fd.fk_facture";
3621
		$sql .= " AND f.fk_soc = s.rowid";
3622
		$sql .= " AND f.entity IN (".getEntity('invoice').")";
3623
		$sql .= " AND fd.fk_product = ".((int) $this->id);
3624
		if (empty($user->rights->societe->client->voir) && !$socid) {
3625
			$sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3626
		}
3627
		//$sql.= " AND f.fk_statut != 0";
3628
		if ($socid > 0) {
3629
			$sql .= " AND f.fk_soc = ".((int) $socid);
3630
		}
3631
3632
		$result = $this->db->query($sql);
3633
		if ($result) {
3634
			$obj = $this->db->fetch_object($result);
3635
			$this->stats_facturerec['customers'] = $obj->nb_customers;
3636
			$this->stats_facturerec['nb'] = $obj->nb;
3637
			$this->stats_facturerec['rows'] = $obj->nb_rows;
3638
			$this->stats_facturerec['qty'] = $obj->qty ? $obj->qty : 0;
3639
3640
			// if it's a virtual product, maybe it is in invoice by extension
3641
			if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) {
3642
				$TFather = $this->getFather();
3643
				if (is_array($TFather) && !empty($TFather)) {
3644
					foreach ($TFather as &$fatherData) {
3645
						$pFather = new Product($this->db);
3646
						$pFather->id = $fatherData['id'];
3647
						$qtyCoef = $fatherData['qty'];
3648
3649
						if ($fatherData['incdec']) {
3650
							$pFather->load_stats_facture($socid);
3651
3652
							$this->stats_facturerec['customers'] += $pFather->stats_facturerec['customers'];
3653
							$this->stats_facturerec['nb'] += $pFather->stats_facturerec['nb'];
3654
							$this->stats_facturerec['rows'] += $pFather->stats_facturerec['rows'];
3655
							$this->stats_facturerec['qty'] += $pFather->stats_facturerec['qty'] * $qtyCoef;
3656
						}
3657
					}
3658
				}
3659
			}
3660
3661
			$parameters = array('socid' => $socid);
3662
			$reshook = $hookmanager->executeHooks('loadStatsCustomerInvoiceRec', $parameters, $this, $action);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $action seems to be never defined.
Loading history...
3663
			if ($reshook > 0) {
3664
				$this->stats_facturerec = $hookmanager->resArray['stats_facturerec'];
3665
			}
3666
3667
			return 1;
3668
		} else {
3669
			$this->error = $this->db->error();
3670
			return -1;
3671
		}
3672
	}
3673
3674
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3675
	/**
3676
	 *  Charge tableau des stats facture pour le produit/service
3677
	 *
3678
	 * @param  int $socid Id societe
3679
	 * @return int                     Array of stats in $this->stats_facture_fournisseur, <0 if ko or >0 if ok
3680
	 */
3681
	public function load_stats_facture_fournisseur($socid = 0)
3682
	{
3683
		// phpcs:enable
3684
		global $conf, $user, $hookmanager, $action;
3685
3686
		$sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,";
3687
		$sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
3688
		$sql .= " FROM ".$this->db->prefix()."facture_fourn_det as fd";
3689
		$sql .= ", ".$this->db->prefix()."facture_fourn as f";
3690
		$sql .= ", ".$this->db->prefix()."societe as s";
3691
		if (empty($user->rights->societe->client->voir) && !$socid) {
3692
			$sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3693
		}
3694
		$sql .= " WHERE f.rowid = fd.fk_facture_fourn";
3695
		$sql .= " AND f.fk_soc = s.rowid";
3696
		$sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
3697
		$sql .= " AND fd.fk_product = ".((int) $this->id);
3698
		if (empty($user->rights->societe->client->voir) && !$socid) {
3699
			$sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3700
		}
3701
		//$sql.= " AND f.fk_statut != 0";
3702
		if ($socid > 0) {
3703
			$sql .= " AND f.fk_soc = ".((int) $socid);
3704
		}
3705
3706
		$result = $this->db->query($sql);
3707
		if ($result) {
3708
			$obj = $this->db->fetch_object($result);
3709
			$this->stats_facture_fournisseur['suppliers'] = $obj->nb_suppliers;
3710
			$this->stats_facture_fournisseur['nb'] = $obj->nb;
3711
			$this->stats_facture_fournisseur['rows'] = $obj->nb_rows;
3712
			$this->stats_facture_fournisseur['qty'] = $obj->qty ? $obj->qty : 0;
3713
3714
			$parameters = array('socid' => $socid);
3715
			$reshook = $hookmanager->executeHooks('loadStatsSupplierInvoice', $parameters, $this, $action);
3716
			if ($reshook > 0) {
3717
				$this->stats_facture_fournisseur = $hookmanager->resArray['stats_facture_fournisseur'];
3718
			}
3719
3720
			return 1;
3721
		} else {
3722
			$this->error = $this->db->error();
3723
			return -1;
3724
		}
3725
	}
3726
3727
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3728
	/**
3729
	 *  Return an array formated for showing graphs
3730
	 *
3731
	 * @param  string $sql  		Request to execute
3732
	 * @param  string $mode 		'byunit'=number of unit, 'bynumber'=nb of entities
3733
	 * @param  int    $year 		Year (0=current year, -1=all years)
3734
	 * @return array|int           	<0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
3735
	 */
3736
	private function _get_stats($sql, $mode, $year = 0)
3737
	{
3738
		// phpcs:enable
3739
		$tab = array();
3740
3741
		$resql = $this->db->query($sql);
3742
		if ($resql) {
3743
			$num = $this->db->num_rows($resql);
3744
			$i = 0;
3745
			while ($i < $num) {
3746
				$arr = $this->db->fetch_array($resql);
3747
				$keyfortab = (string) $arr[1];
3748
				if ($year == -1) {
3749
					$keyfortab = substr($keyfortab, -2);
3750
				}
3751
3752
				if ($mode == 'byunit') {
3753
					$tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[0]; // 1st field
3754
				} elseif ($mode == 'bynumber') {
3755
					$tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
3756
				}
3757
				$i++;
3758
			}
3759
		} else {
3760
			$this->error = $this->db->error().' sql='.$sql;
3761
			return -1;
3762
		}
3763
3764
		if (empty($year)) {
3765
			$year = strftime('%Y', time());
3766
			$month = strftime('%m', time());
3767
		} elseif ($year == -1) {
3768
			$year = '';
3769
			$month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
3770
		} else {
3771
			$month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
3772
		}
3773
3774
		$result = array();
3775
3776
		for ($j = 0; $j < 12; $j++) {
3777
			// $ids is 'D', 'N', 'O', 'S', ... (First letter of month in user language)
3778
			$idx = ucfirst(dol_trunc(dol_print_date(dol_mktime(12, 0, 0, $month, 1, 1970), "%b"), 1, 'right', 'UTF-8', 1));
3779
3780
			//print $idx.'-'.$year.'-'.$month.'<br>';
3781
			$result[$j] = array($idx, isset($tab[$year.$month]) ? $tab[$year.$month] : 0);
3782
			//            $result[$j] = array($monthnum,isset($tab[$year.$month])?$tab[$year.$month]:0);
3783
3784
			$month = "0".($month - 1);
3785
			if (dol_strlen($month) == 3) {
3786
				$month = substr($month, 1);
3787
			}
3788
			if ($month == 0) {
3789
				$month = 12;
3790
				$year = $year - 1;
3791
			}
3792
		}
3793
3794
		return array_reverse($result);
3795
	}
3796
3797
3798
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3799
	/**
3800
	 *  Return nb of units or customers invoices in which product is included
3801
	 *
3802
	 * @param  int    $socid               Limit count on a particular third party id
3803
	 * @param  string $mode                'byunit'=number of unit, 'bynumber'=nb of entities
3804
	 * @param  int    $filteronproducttype 0=To filter on product only, 1=To filter on services only
3805
	 * @param  int    $year                Year (0=last 12 month, -1=all years)
3806
	 * @param  string $morefilter          More sql filters
3807
	 * @return array                       <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
3808
	 */
3809
	public function get_nb_vente($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
3810
	{
3811
		// phpcs:enable
3812
		global $conf;
3813
		global $user;
3814
3815
		$sql = "SELECT sum(d.qty), date_format(f.datef, '%Y%m')";
3816
		if ($mode == 'bynumber') {
3817
			$sql .= ", count(DISTINCT f.rowid)";
3818
		}
3819
		$sql .= " FROM ".$this->db->prefix()."facturedet as d, ".$this->db->prefix()."facture as f, ".$this->db->prefix()."societe as s";
3820
		if ($filteronproducttype >= 0) {
3821
			$sql .= ", ".$this->db->prefix()."product as p";
3822
		}
3823
		if (empty($user->rights->societe->client->voir) && !$socid) {
3824
			$sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3825
		}
3826
		$sql .= " WHERE f.rowid = d.fk_facture";
3827
		if ($this->id > 0) {
3828
			$sql .= " AND d.fk_product = ".((int) $this->id);
3829
		} else {
3830
			$sql .= " AND d.fk_product > 0";
3831
		}
3832
		if ($filteronproducttype >= 0) {
3833
			$sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
3834
		}
3835
		$sql .= " AND f.fk_soc = s.rowid";
3836
		$sql .= " AND f.entity IN (".getEntity('invoice').")";
3837
		if (empty($user->rights->societe->client->voir) && !$socid) {
3838
			$sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3839
		}
3840
		if ($socid > 0) {
3841
			$sql .= " AND f.fk_soc = $socid";
3842
		}
3843
		$sql .= $morefilter;
3844
		$sql .= " GROUP BY date_format(f.datef,'%Y%m')";
3845
		$sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
3846
3847
		return $this->_get_stats($sql, $mode, $year);
3848
	}
3849
3850
3851
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3852
	/**
3853
	 *  Return nb of units or supplier invoices in which product is included
3854
	 *
3855
	 * @param  int    $socid               Limit count on a particular third party id
3856
	 * @param  string $mode                'byunit'=number of unit, 'bynumber'=nb of entities
3857
	 * @param  int    $filteronproducttype 0=To filter on product only, 1=To filter on services only
3858
	 * @param  int    $year                Year (0=last 12 month, -1=all years)
3859
	 * @param  string $morefilter          More sql filters
3860
	 * @return array                       <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
3861
	 */
3862
	public function get_nb_achat($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
3863
	{
3864
		// phpcs:enable
3865
		global $conf;
3866
		global $user;
3867
3868
		$sql = "SELECT sum(d.qty), date_format(f.datef, '%Y%m')";
3869
		if ($mode == 'bynumber') {
3870
			$sql .= ", count(DISTINCT f.rowid)";
3871
		}
3872
		$sql .= " FROM ".$this->db->prefix()."facture_fourn_det as d, ".$this->db->prefix()."facture_fourn as f, ".$this->db->prefix()."societe as s";
3873
		if ($filteronproducttype >= 0) {
3874
			$sql .= ", ".$this->db->prefix()."product as p";
3875
		}
3876
		if (empty($user->rights->societe->client->voir) && !$socid) {
3877
			$sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3878
		}
3879
		$sql .= " WHERE f.rowid = d.fk_facture_fourn";
3880
		if ($this->id > 0) {
3881
			$sql .= " AND d.fk_product = ".((int) $this->id);
3882
		} else {
3883
			$sql .= " AND d.fk_product > 0";
3884
		}
3885
		if ($filteronproducttype >= 0) {
3886
			$sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
3887
		}
3888
		$sql .= " AND f.fk_soc = s.rowid";
3889
		$sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
3890
		if (empty($user->rights->societe->client->voir) && !$socid) {
3891
			$sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3892
		}
3893
		if ($socid > 0) {
3894
			$sql .= " AND f.fk_soc = $socid";
3895
		}
3896
		$sql .= $morefilter;
3897
		$sql .= " GROUP BY date_format(f.datef,'%Y%m')";
3898
		$sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
3899
3900
		return $this->_get_stats($sql, $mode, $year);
3901
	}
3902
3903
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3904
	/**
3905
	 * Return nb of units in proposals in which product is included
3906
	 *
3907
	 * @param  int    $socid               Limit count on a particular third party id
3908
	 * @param  string $mode                'byunit'=number of unit, 'bynumber'=nb of entities
3909
	 * @param  int    $filteronproducttype 0=To filter on product only, 1=To filter on services only
3910
	 * @param  int    $year                Year (0=last 12 month, -1=all years)
3911
	 * @param  string $morefilter          More sql filters
3912
	 * @return array                       <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
3913
	 */
3914
	public function get_nb_propal($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
3915
	{
3916
		// phpcs:enable
3917
		global $conf, $user;
3918
3919
		$sql = "SELECT sum(d.qty), date_format(p.datep, '%Y%m')";
3920
		if ($mode == 'bynumber') {
3921
			$sql .= ", count(DISTINCT p.rowid)";
3922
		}
3923
		$sql .= " FROM ".$this->db->prefix()."propaldet as d, ".$this->db->prefix()."propal as p, ".$this->db->prefix()."societe as s";
3924
		if ($filteronproducttype >= 0) {
3925
			$sql .= ", ".$this->db->prefix()."product as prod";
3926
		}
3927
		if (empty($user->rights->societe->client->voir) && !$socid) {
3928
			$sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3929
		}
3930
		$sql .= " WHERE p.rowid = d.fk_propal";
3931
		if ($this->id > 0) {
3932
			$sql .= " AND d.fk_product = ".((int) $this->id);
3933
		} else {
3934
			$sql .= " AND d.fk_product > 0";
3935
		}
3936
		if ($filteronproducttype >= 0) {
3937
			$sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
3938
		}
3939
		$sql .= " AND p.fk_soc = s.rowid";
3940
		$sql .= " AND p.entity IN (".getEntity('propal').")";
3941
		if (empty($user->rights->societe->client->voir) && !$socid) {
3942
			$sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3943
		}
3944
		if ($socid > 0) {
3945
			$sql .= " AND p.fk_soc = ".((int) $socid);
3946
		}
3947
		$sql .= $morefilter;
3948
		$sql .= " GROUP BY date_format(p.datep,'%Y%m')";
3949
		$sql .= " ORDER BY date_format(p.datep,'%Y%m') DESC";
3950
3951
		return $this->_get_stats($sql, $mode, $year);
3952
	}
3953
3954
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3955
	/**
3956
	 *  Return nb of units in proposals in which product is included
3957
	 *
3958
	 * @param  int    $socid               Limit count on a particular third party id
3959
	 * @param  string $mode                'byunit'=number of unit, 'bynumber'=nb of entities
3960
	 * @param  int    $filteronproducttype 0=To filter on product only, 1=To filter on services only
3961
	 * @param  int    $year                Year (0=last 12 month, -1=all years)
3962
	 * @param  string $morefilter          More sql filters
3963
	 * @return array                       <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
3964
	 */
3965
	public function get_nb_propalsupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
3966
	{
3967
		// phpcs:enable
3968
		global $conf;
3969
		global $user;
3970
3971
		$sql = "SELECT sum(d.qty), date_format(p.date_valid, '%Y%m')";
3972
		if ($mode == 'bynumber') {
3973
			$sql .= ", count(DISTINCT p.rowid)";
3974
		}
3975
		$sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as d, ".$this->db->prefix()."supplier_proposal as p, ".$this->db->prefix()."societe as s";
3976
		if ($filteronproducttype >= 0) {
3977
			$sql .= ", ".$this->db->prefix()."product as prod";
3978
		}
3979
		if (empty($user->rights->societe->client->voir) && !$socid) {
3980
			$sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3981
		}
3982
		$sql .= " WHERE p.rowid = d.fk_supplier_proposal";
3983
		if ($this->id > 0) {
3984
			$sql .= " AND d.fk_product = ".((int) $this->id);
3985
		} else {
3986
			$sql .= " AND d.fk_product > 0";
3987
		}
3988
		if ($filteronproducttype >= 0) {
3989
			$sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
3990
		}
3991
		$sql .= " AND p.fk_soc = s.rowid";
3992
		$sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
3993
		if (empty($user->rights->societe->client->voir) && !$socid) {
3994
			$sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3995
		}
3996
		if ($socid > 0) {
3997
			$sql .= " AND p.fk_soc = ".((int) $socid);
3998
		}
3999
		$sql .= $morefilter;
4000
		$sql .= " GROUP BY date_format(p.date_valid,'%Y%m')";
4001
		$sql .= " ORDER BY date_format(p.date_valid,'%Y%m') DESC";
4002
4003
		return $this->_get_stats($sql, $mode, $year);
4004
	}
4005
4006
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4007
	/**
4008
	 *  Return nb of units in orders in which product is included
4009
	 *
4010
	 * @param  int    $socid               Limit count on a particular third party id
4011
	 * @param  string $mode                'byunit'=number of unit, 'bynumber'=nb of entities
4012
	 * @param  int    $filteronproducttype 0=To filter on product only, 1=To filter on services only
4013
	 * @param  int    $year                Year (0=last 12 month, -1=all years)
4014
	 * @param  string $morefilter          More sql filters
4015
	 * @return array                       <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
4016
	 */
4017
	public function get_nb_order($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4018
	{
4019
		// phpcs:enable
4020
		global $conf, $user;
4021
4022
		$sql = "SELECT sum(d.qty), date_format(c.date_commande, '%Y%m')";
4023
		if ($mode == 'bynumber') {
4024
			$sql .= ", count(DISTINCT c.rowid)";
4025
		}
4026
		$sql .= " FROM ".$this->db->prefix()."commandedet as d, ".$this->db->prefix()."commande as c, ".$this->db->prefix()."societe as s";
4027
		if ($filteronproducttype >= 0) {
4028
			$sql .= ", ".$this->db->prefix()."product as p";
4029
		}
4030
		if (empty($user->rights->societe->client->voir) && !$socid) {
4031
			$sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4032
		}
4033
		$sql .= " WHERE c.rowid = d.fk_commande";
4034
		if ($this->id > 0) {
4035
			$sql .= " AND d.fk_product = ".((int) $this->id);
4036
		} else {
4037
			$sql .= " AND d.fk_product > 0";
4038
		}
4039
		if ($filteronproducttype >= 0) {
4040
			$sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4041
		}
4042
		$sql .= " AND c.fk_soc = s.rowid";
4043
		$sql .= " AND c.entity IN (".getEntity('commande').")";
4044
		if (empty($user->rights->societe->client->voir) && !$socid) {
4045
			$sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4046
		}
4047
		if ($socid > 0) {
4048
			$sql .= " AND c.fk_soc = ".((int) $socid);
4049
		}
4050
		$sql .= $morefilter;
4051
		$sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4052
		$sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4053
4054
		return $this->_get_stats($sql, $mode, $year);
4055
	}
4056
4057
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4058
	/**
4059
	 *  Return nb of units in orders in which product is included
4060
	 *
4061
	 * @param  int    $socid               Limit count on a particular third party id
4062
	 * @param  string $mode                'byunit'=number of unit, 'bynumber'=nb of entities
4063
	 * @param  int    $filteronproducttype 0=To filter on product only, 1=To filter on services only
4064
	 * @param  int    $year                Year (0=last 12 month, -1=all years)
4065
	 * @param  string $morefilter          More sql filters
4066
	 * @return array                       <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
4067
	 */
4068
	public function get_nb_ordersupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4069
	{
4070
		// phpcs:enable
4071
		global $conf, $user;
4072
4073
		$sql = "SELECT sum(d.qty), date_format(c.date_commande, '%Y%m')";
4074
		if ($mode == 'bynumber') {
4075
			$sql .= ", count(DISTINCT c.rowid)";
4076
		}
4077
		$sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as d, ".$this->db->prefix()."commande_fournisseur as c, ".$this->db->prefix()."societe as s";
4078
		if ($filteronproducttype >= 0) {
4079
			$sql .= ", ".$this->db->prefix()."product as p";
4080
		}
4081
		if (empty($user->rights->societe->client->voir) && !$socid) {
4082
			$sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4083
		}
4084
		$sql .= " WHERE c.rowid = d.fk_commande";
4085
		if ($this->id > 0) {
4086
			$sql .= " AND d.fk_product = ".((int) $this->id);
4087
		} else {
4088
			$sql .= " AND d.fk_product > 0";
4089
		}
4090
		if ($filteronproducttype >= 0) {
4091
			$sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4092
		}
4093
		$sql .= " AND c.fk_soc = s.rowid";
4094
		$sql .= " AND c.entity IN (".getEntity('supplier_order').")";
4095
		if (empty($user->rights->societe->client->voir) && !$socid) {
4096
			$sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4097
		}
4098
		if ($socid > 0) {
4099
			$sql .= " AND c.fk_soc = ".((int) $socid);
4100
		}
4101
		$sql .= $morefilter;
4102
		$sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4103
		$sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4104
4105
		return $this->_get_stats($sql, $mode, $year);
4106
	}
4107
4108
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4109
	/**
4110
	 *  Return nb of units in orders in which product is included
4111
	 *
4112
	 * @param  int    $socid               Limit count on a particular third party id
4113
	 * @param  string $mode                'byunit'=number of unit, 'bynumber'=nb of entities
4114
	 * @param  int    $filteronproducttype 0=To filter on product only, 1=To filter on services only
4115
	 * @param  int    $year                Year (0=last 12 month, -1=all years)
4116
	 * @param  string $morefilter          More sql filters
4117
	 * @return array                       <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
4118
	 */
4119
	public function get_nb_contract($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4120
	{
4121
		// phpcs:enable
4122
		global $conf, $user;
4123
4124
		$sql = "SELECT sum(d.qty), date_format(c.date_contrat, '%Y%m')";
4125
		if ($mode == 'bynumber') {
4126
			$sql .= ", count(DISTINCT c.rowid)";
4127
		}
4128
		$sql .= " FROM ".$this->db->prefix()."contratdet as d, ".$this->db->prefix()."contrat as c, ".$this->db->prefix()."societe as s";
4129
		if ($filteronproducttype >= 0) {
4130
			$sql .= ", ".$this->db->prefix()."product as p";
4131
		}
4132
		if (empty($user->rights->societe->client->voir) && !$socid) {
4133
			$sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4134
		}
4135
4136
		$sql .= " WHERE c.entity IN (".getEntity('contract').")";
4137
		$sql .= " AND c.rowid = d.fk_contrat";
4138
4139
		if ($this->id > 0) {
4140
			$sql .= " AND d.fk_product = ".((int) $this->id);
4141
		} else {
4142
			$sql .= " AND d.fk_product > 0";
4143
		}
4144
		if ($filteronproducttype >= 0) {
4145
			$sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4146
		}
4147
		$sql .= " AND c.fk_soc = s.rowid";
4148
4149
		if (empty($user->rights->societe->client->voir) && !$socid) {
4150
			$sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4151
		}
4152
		if ($socid > 0) {
4153
			$sql .= " AND c.fk_soc = ".((int) $socid);
4154
		}
4155
		$sql .= $morefilter;
4156
		$sql .= " GROUP BY date_format(c.date_contrat,'%Y%m')";
4157
		$sql .= " ORDER BY date_format(c.date_contrat,'%Y%m') DESC";
4158
4159
		return $this->_get_stats($sql, $mode, $year);
4160
	}
4161
4162
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4163
	/**
4164
	 *  Return nb of units in orders in which product is included
4165
	 *
4166
	 * @param  int    $socid               Limit count on a particular third party id
4167
	 * @param  string $mode                'byunit'=number of unit, 'bynumber'=nb of entities
4168
	 * @param  int    $filteronproducttype 0=To filter on product only, 1=To filter on services only
4169
	 * @param  int    $year                Year (0=last 12 month, -1=all years)
4170
	 * @param  string $morefilter          More sql filters
4171
	 * @return array                       <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
4172
	 */
4173
	public function get_nb_mos($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4174
	{
4175
		// phpcs:enable
4176
		global $conf, $user;
4177
4178
		$sql = "SELECT sum(d.qty), date_format(d.date_valid, '%Y%m')";
4179
		if ($mode == 'bynumber') {
4180
			$sql .= ", count(DISTINCT d.rowid)";
4181
		}
4182
		$sql .= " FROM ".$this->db->prefix()."mrp_mo as d LEFT JOIN  ".$this->db->prefix()."societe as s ON d.fk_soc = s.rowid";
4183
		if ($filteronproducttype >= 0) {
4184
			$sql .= ", ".$this->db->prefix()."product as p";
4185
		}
4186
		if (empty($user->rights->societe->client->voir) && !$socid) {
4187
			$sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4188
		}
4189
4190
		$sql .= " WHERE d.entity IN (".getEntity('mo').")";
4191
		$sql .= " AND d.status > 0";
4192
4193
		if ($this->id > 0) {
4194
			$sql .= " AND d.fk_product = ".((int) $this->id);
4195
		} else {
4196
			$sql .= " AND d.fk_product > 0";
4197
		}
4198
		if ($filteronproducttype >= 0) {
4199
			$sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4200
		}
4201
4202
		if (empty($user->rights->societe->client->voir) && !$socid) {
4203
			$sql .= " AND d.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4204
		}
4205
		if ($socid > 0) {
4206
			$sql .= " AND d.fk_soc = ".((int) $socid);
4207
		}
4208
		$sql .= $morefilter;
4209
		$sql .= " GROUP BY date_format(d.date_valid,'%Y%m')";
4210
		$sql .= " ORDER BY date_format(d.date_valid,'%Y%m') DESC";
4211
4212
		return $this->_get_stats($sql, $mode, $year);
4213
	}
4214
4215
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4216
	/**
4217
	 *  Link a product/service to a parent product/service
4218
	 *
4219
	 * @param  int $id_pere Id of parent product/service
4220
	 * @param  int $id_fils Id of child product/service
4221
	 * @param  int $qty     Quantity
4222
	 * @param  int $incdec  1=Increase/decrease stock of child when parent stock increase/decrease
4223
	 * @return int                < 0 if KO, > 0 if OK
4224
	 */
4225
	public function add_sousproduit($id_pere, $id_fils, $qty, $incdec = 1)
4226
	{
4227
		// phpcs:enable
4228
		// Clean parameters
4229
		if (!is_numeric($id_pere)) {
4230
			$id_pere = 0;
4231
		}
4232
		if (!is_numeric($id_fils)) {
4233
			$id_fils = 0;
4234
		}
4235
		if (!is_numeric($incdec)) {
4236
			$incdec = 0;
4237
		}
4238
4239
		$result = $this->del_sousproduit($id_pere, $id_fils);
4240
		if ($result < 0) {
4241
			return $result;
4242
		}
4243
4244
		// Check not already father of id_pere (to avoid father -> child -> father links)
4245
		$sql = "SELECT fk_product_pere from ".$this->db->prefix()."product_association";
4246
		$sql .= " WHERE fk_product_pere = ".((int) $id_fils)." AND fk_product_fils = ".((int) $id_pere);
4247
		if (!$this->db->query($sql)) {
4248
			dol_print_error($this->db);
4249
			return -1;
4250
		} else {
4251
			//Selection of the highest row
4252
			$sql = "SELECT MAX(rang) as max_rank FROM ".$this->db->prefix()."product_association";
4253
			$sql .= " WHERE fk_product_pere  = ".((int) $id_pere);
4254
			$resql = $this->db->query($sql);
4255
			if ($resql > 0) {
4256
				$obj = $this->db->fetch_object($resql);
4257
				$rank = $obj->max_rank + 1;
4258
				//Addition of a product with the highest rank +1
4259
				$sql = "INSERT INTO ".$this->db->prefix()."product_association(fk_product_pere,fk_product_fils,qty,incdec,rang)";
4260
				$sql .= " VALUES (".((int) $id_pere).", ".((int) $id_fils).", ".price2num($qty, 'MS').", ".price2num($incdec, 'MS').", ".((int) $rank).")";
4261
				if (! $this->db->query($sql)) {
4262
					dol_print_error($this->db);
4263
					return -1;
4264
				} else {
4265
					return 1;
4266
				}
4267
			} else {
4268
				dol_print_error($this->db);
4269
				return -1;
4270
			}
4271
		}
4272
	}
4273
4274
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4275
	/**
4276
	 *  Modify composed product
4277
	 *
4278
	 * @param  int $id_pere Id of parent product/service
4279
	 * @param  int $id_fils Id of child product/service
4280
	 * @param  int $qty     Quantity
4281
	 * @param  int $incdec  1=Increase/decrease stock of child when parent stock increase/decrease
4282
	 * @return int                < 0 if KO, > 0 if OK
4283
	 */
4284
	public function update_sousproduit($id_pere, $id_fils, $qty, $incdec = 1)
4285
	{
4286
		// phpcs:enable
4287
		// Clean parameters
4288
		if (!is_numeric($id_pere)) {
4289
			$id_pere = 0;
4290
		}
4291
		if (!is_numeric($id_fils)) {
4292
			$id_fils = 0;
4293
		}
4294
		if (!is_numeric($incdec)) {
4295
			$incdec = 1;
4296
		}
4297
		if (!is_numeric($qty)) {
4298
			$qty = 1;
4299
		}
4300
4301
		$sql = 'UPDATE '.$this->db->prefix().'product_association SET ';
4302
		$sql .= 'qty = '.price2num($qty, 'MS');
4303
		$sql .= ',incdec = '.price2num($incdec, 'MS');
4304
		$sql .= ' WHERE fk_product_pere = '.((int) $id_pere).' AND fk_product_fils = '.((int) $id_fils);
4305
4306
		if (!$this->db->query($sql)) {
4307
			dol_print_error($this->db);
4308
			return -1;
4309
		} else {
4310
			return 1;
4311
		}
4312
	}
4313
4314
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4315
	/**
4316
	 *  Remove a link between a subproduct and a parent product/service
4317
	 *
4318
	 * @param  int $fk_parent Id of parent product (child will no more be linked to it)
4319
	 * @param  int $fk_child  Id of child product
4320
	 * @return int            < 0 if KO, > 0 if OK
4321
	 */
4322
	public function del_sousproduit($fk_parent, $fk_child)
4323
	{
4324
		// phpcs:enable
4325
		if (!is_numeric($fk_parent)) {
4326
			$fk_parent = 0;
4327
		}
4328
		if (!is_numeric($fk_child)) {
4329
			$fk_child = 0;
4330
		}
4331
4332
		$sql = "DELETE FROM ".$this->db->prefix()."product_association";
4333
		$sql .= " WHERE fk_product_pere  = ".((int) $fk_parent);
4334
		$sql .= " AND fk_product_fils = ".((int) $fk_child);
4335
4336
		dol_syslog(get_class($this).'::del_sousproduit', LOG_DEBUG);
4337
		if (!$this->db->query($sql)) {
4338
			dol_print_error($this->db);
4339
			return -1;
4340
		}
4341
4342
		// Updated ranks so that none are missing
4343
		$sqlrank = "SELECT rowid, rang FROM ".$this->db->prefix()."product_association";
4344
		$sqlrank.= " WHERE fk_product_pere = ".((int) $fk_parent);
4345
		$sqlrank.= " ORDER BY rang";
4346
		$resqlrank = $this->db->query($sqlrank);
4347
		if ($resqlrank) {
4348
			$cpt = 0;
4349
			while ($objrank = $this->db->fetch_object($resqlrank)) {
4350
				$cpt++;
4351
				$sql = "UPDATE ".$this->db->prefix()."product_association";
4352
				$sql.= " SET rang = ".((int) $cpt);
4353
				$sql.= " WHERE rowid = ".((int) $objrank->rowid);
4354
				if (! $this->db->query($sql)) {
4355
					dol_print_error($this->db);
4356
					return -1;
4357
				}
4358
			}
4359
		}
4360
		return 1;
4361
	}
4362
4363
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4364
	/**
4365
	 *  Check if it is a sub-product into a kit
4366
	 *
4367
	 * @param  int 	$fk_parent 		Id of parent kit product
4368
	 * @param  int 	$fk_child  		Id of child product
4369
	 * @return int                  <0 if KO, >0 if OK
4370
	 */
4371
	public function is_sousproduit($fk_parent, $fk_child)
4372
	{
4373
		// phpcs:enable
4374
		$sql = "SELECT fk_product_pere, qty, incdec";
4375
		$sql .= " FROM ".$this->db->prefix()."product_association";
4376
		$sql .= " WHERE fk_product_pere  = ".((int) $fk_parent);
4377
		$sql .= " AND fk_product_fils = ".((int) $fk_child);
4378
4379
		$result = $this->db->query($sql);
4380
		if ($result) {
4381
			$num = $this->db->num_rows($result);
4382
4383
			if ($num > 0) {
4384
				$obj = $this->db->fetch_object($result);
4385
4386
				$this->is_sousproduit_qty = $obj->qty;
4387
				$this->is_sousproduit_incdec = $obj->incdec;
4388
4389
				return true;
4390
			} else {
4391
				return false;
4392
			}
4393
		} else {
4394
			dol_print_error($this->db);
4395
			return -1;
4396
		}
4397
	}
4398
4399
4400
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4401
	/**
4402
	 *  Add a supplier price for the product.
4403
	 *  Note: Duplicate ref is accepted for different quantity only, or for different companies.
4404
	 *
4405
	 * @param  User   $user      User that make link
4406
	 * @param  int    $id_fourn  Supplier id
4407
	 * @param  string $ref_fourn Supplier ref
4408
	 * @param  float  $quantity  Quantity minimum for price
4409
	 * @return int               < 0 if KO, 0 if link already exists for this product, > 0 if OK
4410
	 */
4411
	public function add_fournisseur($user, $id_fourn, $ref_fourn, $quantity)
4412
	{
4413
		// phpcs:enable
4414
		global $conf;
4415
4416
		$now = dol_now();
4417
4418
		dol_syslog(get_class($this)."::add_fournisseur id_fourn = ".$id_fourn." ref_fourn=".$ref_fourn." quantity=".$quantity, LOG_DEBUG);
4419
4420
		// Clean parameters
4421
		$quantity = price2num($quantity, 'MS');
4422
4423
		if ($ref_fourn) {
4424
			$sql = "SELECT rowid, fk_product";
4425
			$sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
4426
			$sql .= " WHERE fk_soc = ".((int) $id_fourn);
4427
			$sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
4428
			$sql .= " AND fk_product <> ".((int) $this->id);
4429
			$sql .= " AND entity IN (".getEntity('productsupplierprice').")";
4430
4431
			$resql = $this->db->query($sql);
4432
			if ($resql) {
4433
				$obj = $this->db->fetch_object($resql);
4434
				if ($obj) {
4435
					// If the supplier ref already exists but for another product (duplicate ref is accepted for different quantity only or different companies)
4436
					$this->product_id_already_linked = $obj->fk_product;
4437
					return -3;
4438
				}
4439
				$this->db->free($resql);
4440
			}
4441
		}
4442
4443
		$sql = "SELECT rowid";
4444
		$sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
4445
		$sql .= " WHERE fk_soc = ".((int) $id_fourn);
4446
		if ($ref_fourn) {
4447
			$sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
4448
		} else {
4449
			$sql .= " AND (ref_fourn = '' OR ref_fourn IS NULL)";
4450
		}
4451
		$sql .= " AND quantity = ".((float) $quantity);
4452
		$sql .= " AND fk_product = ".((int) $this->id);
4453
		$sql .= " AND entity IN (".getEntity('productsupplierprice').")";
4454
4455
		$resql = $this->db->query($sql);
4456
		if ($resql) {
4457
			$obj = $this->db->fetch_object($resql);
4458
4459
			// The reference supplier does not exist, we create it for this product.
4460
			if (empty($obj)) {
4461
				$sql = "INSERT INTO ".$this->db->prefix()."product_fournisseur_price(";
4462
				$sql .= "datec";
4463
				$sql .= ", entity";
4464
				$sql .= ", fk_product";
4465
				$sql .= ", fk_soc";
4466
				$sql .= ", ref_fourn";
4467
				$sql .= ", quantity";
4468
				$sql .= ", fk_user";
4469
				$sql .= ", tva_tx";
4470
				$sql .= ") VALUES (";
4471
				$sql .= "'".$this->db->idate($now)."'";
4472
				$sql .= ", ".$conf->entity;
4473
				$sql .= ", ".$this->id;
4474
				$sql .= ", ".$id_fourn;
4475
				$sql .= ", '".$this->db->escape($ref_fourn)."'";
4476
				$sql .= ", ".$quantity;
4477
				$sql .= ", ".$user->id;
4478
				$sql .= ", 0";
4479
				$sql .= ")";
4480
4481
				if ($this->db->query($sql)) {
4482
					$this->product_fourn_price_id = $this->db->last_insert_id($this->db->prefix()."product_fournisseur_price");
4483
					return 1;
4484
				} else {
4485
					$this->error = $this->db->lasterror();
4486
					return -1;
4487
				}
4488
			} else {
4489
				// If the supplier price already exists for this product and quantity
4490
				$this->product_fourn_price_id = $obj->rowid;
4491
				return 0;
4492
			}
4493
		} else {
4494
			$this->error = $this->db->lasterror();
4495
			return -2;
4496
		}
4497
	}
4498
4499
4500
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4501
	/**
4502
	 * Return list of suppliers providing the product or service
4503
	 *
4504
	 * @return array        Array of vendor ids
4505
	 */
4506
	public function list_suppliers()
4507
	{
4508
		// phpcs:enable
4509
		global $conf;
4510
4511
		$list = array();
4512
4513
		$sql = "SELECT DISTINCT p.fk_soc";
4514
		$sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as p";
4515
		$sql .= " WHERE p.fk_product = ".((int) $this->id);
4516
		$sql .= " AND p.entity = ".((int) $conf->entity);
4517
4518
		$result = $this->db->query($sql);
4519
		if ($result) {
4520
			$num = $this->db->num_rows($result);
4521
			$i = 0;
4522
			while ($i < $num) {
4523
				$obj = $this->db->fetch_object($result);
4524
				$list[$i] = $obj->fk_soc;
4525
				$i++;
4526
			}
4527
		}
4528
4529
		return $list;
4530
	}
4531
4532
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4533
	/**
4534
	 *  Recopie les prix d'un produit/service sur un autre
4535
	 *
4536
	 * @param  int $fromId Id product source
4537
	 * @param  int $toId   Id product target
4538
	 * @return int                     < 0 if KO, > 0 if OK
4539
	 */
4540
	public function clone_price($fromId, $toId)
4541
	{
4542
		global $conf, $user;
4543
4544
		$now = dol_now();
4545
4546
		$this->db->begin();
4547
4548
		// prices
4549
		$sql  = "INSERT INTO ".$this->db->prefix()."product_price (";
4550
		$sql .= " entity";
4551
		$sql .= ", fk_product";
4552
		$sql .= ", date_price";
4553
		$sql .= ", price_level";
4554
		$sql .= ", price";
4555
		$sql .= ", price_ttc";
4556
		$sql .= ", price_min";
4557
		$sql .= ", price_min_ttc";
4558
		$sql .= ", price_base_type";
4559
		$sql .= ", default_vat_code";
4560
		$sql .= ", tva_tx";
4561
		$sql .= ", recuperableonly";
4562
		$sql .= ", localtax1_tx";
4563
		$sql .= ", localtax1_type";
4564
		$sql .= ", localtax2_tx";
4565
		$sql .= ", localtax2_type";
4566
		$sql .= ", fk_user_author";
4567
		$sql .= ", tosell";
4568
		$sql .= ", price_by_qty";
4569
		$sql .= ", fk_price_expression";
4570
		$sql .= ", fk_multicurrency";
4571
		$sql .= ", multicurrency_code";
4572
		$sql .= ", multicurrency_tx";
4573
		$sql .= ", multicurrency_price";
4574
		$sql .= ", multicurrency_price_ttc";
4575
		$sql .= ")";
4576
		$sql .= " SELECT";
4577
		$sql .= " entity";
4578
		$sql .= ", ".$toId;
4579
		$sql .= ", '".$this->db->idate($now)."'";
4580
		$sql .= ", price_level";
4581
		$sql .= ", price";
4582
		$sql .= ", price_ttc";
4583
		$sql .= ", price_min";
4584
		$sql .= ", price_min_ttc";
4585
		$sql .= ", price_base_type";
4586
		$sql .= ", default_vat_code";
4587
		$sql .= ", tva_tx";
4588
		$sql .= ", recuperableonly";
4589
		$sql .= ", localtax1_tx";
4590
		$sql .= ", localtax1_type";
4591
		$sql .= ", localtax2_tx";
4592
		$sql .= ", localtax2_type";
4593
		$sql .= ", ".$user->id;
4594
		$sql .= ", tosell";
4595
		$sql .= ", price_by_qty";
4596
		$sql .= ", fk_price_expression";
4597
		$sql .= ", fk_multicurrency";
4598
		$sql .= ", multicurrency_code";
4599
		$sql .= ", multicurrency_tx";
4600
		$sql .= ", multicurrency_price";
4601
		$sql .= ", multicurrency_price_ttc";
4602
		$sql .= " FROM ".$this->db->prefix()."product_price";
4603
		$sql .= " WHERE fk_product = ".((int) $fromId);
4604
		$sql .= " ORDER BY date_price DESC";
4605
		if ($conf->global->PRODUIT_MULTIPRICES_LIMIT > 0) {
4606
			$sql .= " LIMIT ".$conf->global->PRODUIT_MULTIPRICES_LIMIT;
4607
		}
4608
4609
		dol_syslog(__METHOD__, LOG_DEBUG);
4610
		$resql = $this->db->query($sql);
4611
		if (!$resql) {
4612
			$this->db->rollback();
4613
			return -1;
4614
		}
4615
4616
		$this->db->commit();
4617
		return 1;
4618
	}
4619
4620
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4621
	/**
4622
	 * Clone links between products
4623
	 *
4624
	 * @param  int $fromId Product id
4625
	 * @param  int $toId   Product id
4626
	 * @return int                  <0 if KO, >0 if OK
4627
	 */
4628
	public function clone_associations($fromId, $toId)
4629
	{
4630
		// phpcs:enable
4631
		$this->db->begin();
4632
4633
		$sql = 'INSERT INTO '.$this->db->prefix().'product_association (fk_product_pere, fk_product_fils, qty)';
4634
		$sql .= " SELECT ".$toId.", fk_product_fils, qty FROM ".$this->db->prefix()."product_association";
4635
		$sql .= " WHERE fk_product_pere = ".((int) $fromId);
4636
4637
		dol_syslog(get_class($this).'::clone_association', LOG_DEBUG);
4638
		if (!$this->db->query($sql)) {
4639
			$this->db->rollback();
4640
			return -1;
4641
		}
4642
4643
		$this->db->commit();
4644
		return 1;
4645
	}
4646
4647
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4648
	/**
4649
	 *  Recopie les fournisseurs et prix fournisseurs d'un produit/service sur un autre
4650
	 *
4651
	 * @param  int $fromId Id produit source
4652
	 * @param  int $toId   Id produit cible
4653
	 * @return int                 < 0 si erreur, > 0 si ok
4654
	 */
4655
	public function clone_fournisseurs($fromId, $toId)
4656
	{
4657
		// phpcs:enable
4658
		$this->db->begin();
4659
4660
		$now = dol_now();
4661
4662
		// les fournisseurs
4663
		/*$sql = "INSERT ".$this->db->prefix()."product_fournisseur ("
4664
		 . " datec, fk_product, fk_soc, ref_fourn, fk_user_author )"
4665
		 . " SELECT '".$this->db->idate($now)."', ".$toId.", fk_soc, ref_fourn, fk_user_author"
4666
		 . " FROM ".$this->db->prefix()."product_fournisseur"
4667
		 . " WHERE fk_product = ".((int) $fromId);
4668
4669
		 if ( ! $this->db->query($sql ) )
4670
		 {
4671
		 $this->db->rollback();
4672
		 return -1;
4673
		 }*/
4674
4675
		// les prix de fournisseurs.
4676
		$sql = "INSERT ".$this->db->prefix()."product_fournisseur_price (";
4677
		$sql .= " datec, fk_product, fk_soc, price, quantity, fk_user)";
4678
		$sql .= " SELECT '".$this->db->idate($now)."', ".((int) $toId).", fk_soc, price, quantity, fk_user";
4679
		$sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
4680
		$sql .= " WHERE fk_product = ".((int) $fromId);
4681
4682
		dol_syslog(get_class($this).'::clone_fournisseurs', LOG_DEBUG);
4683
		$resql = $this->db->query($sql);
4684
		if (!$resql) {
4685
			$this->db->rollback();
4686
			return -1;
4687
		} else {
4688
			$this->db->commit();
4689
			return 1;
4690
		}
4691
	}
4692
4693
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4694
	/**
4695
	 *  Fonction recursive uniquement utilisee par get_arbo_each_prod, recompose l'arborescence des sousproduits
4696
	 *  Define value of this->res
4697
	 *
4698
	 * @param  array  $prod       			Products array
4699
	 * @param  string $compl_path 			Directory path of parents to add before
4700
	 * @param  int    $multiply   			Because each sublevel must be multiplicated by parent nb
4701
	 * @param  int    $level      			Init level
4702
	 * @param  int    $id_parent  			Id parent
4703
	 * @param  int    $ignore_stock_load 	Ignore stock load
4704
	 * @return void
4705
	 */
4706
	public function fetch_prod_arbo($prod, $compl_path = '', $multiply = 1, $level = 1, $id_parent = 0, $ignore_stock_load = 0)
4707
	{
4708
		// phpcs:enable
4709
		global $conf, $langs;
4710
4711
		$tmpproduct = null;
4712
		//var_dump($prod);
4713
		foreach ($prod as $id_product => $desc_pere) {    // $id_product is 0 (first call starting with root top) or an id of a sub_product
4714
			if (is_array($desc_pere)) {    // If desc_pere is an array, this means it's a child
4715
				$id = (!empty($desc_pere[0]) ? $desc_pere[0] : '');
4716
				$nb = (!empty($desc_pere[1]) ? $desc_pere[1] : '');
4717
				$type = (!empty($desc_pere[2]) ? $desc_pere[2] : '');
4718
				$label = (!empty($desc_pere[3]) ? $desc_pere[3] : '');
4719
				$incdec = (!empty($desc_pere[4]) ? $desc_pere[4] : 0);
4720
4721
				if ($multiply < 1) {
4722
					$multiply = 1;
4723
				}
4724
4725
				//print "XXX We add id=".$id." - label=".$label." - nb=".$nb." - multiply=".$multiply." fullpath=".$compl_path.$label."\n";
4726
				if (is_null($tmpproduct)) {
4727
					$tmpproduct = new Product($this->db); // So we initialize tmpproduct only once for all loop.
4728
				}
4729
				$tmpproduct->fetch($id); // Load product to get ->ref
4730
4731
				if (empty($ignore_stock_load) && ($tmpproduct->isProduct() || !empty($conf->global->STOCK_SUPPORTS_SERVICES))) {
4732
					$tmpproduct->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
4733
				}
4734
4735
				$this->res[] = array(
4736
					'id'=>$id, // Id product
4737
					'id_parent'=>$id_parent,
4738
					'ref'=>$tmpproduct->ref, // Ref product
4739
					'nb'=>$nb, // Nb of units that compose parent product
4740
					'nb_total'=>$nb * $multiply, // Nb of units for all nb of product
4741
					'stock'=>$tmpproduct->stock_reel, // Stock
4742
					'stock_alert'=>$tmpproduct->seuil_stock_alerte, // Stock alert
4743
					'label'=>$label,
4744
					'fullpath'=>$compl_path.$label, // Label
4745
					'type'=>$type, // Nb of units that compose parent product
4746
					'desiredstock'=>$tmpproduct->desiredstock,
4747
					'level'=>$level,
4748
					'incdec'=>$incdec,
4749
					'entity'=>$tmpproduct->entity
4750
				);
4751
4752
				// Recursive call if there is childs to child
4753
				if (isset($desc_pere['childs']) && is_array($desc_pere['childs'])) {
4754
					//print 'YYY We go down for '.$desc_pere[3]." -> \n";
4755
					$this->fetch_prod_arbo($desc_pere['childs'], $compl_path.$desc_pere[3]." -> ", $desc_pere[1] * $multiply, $level + 1, $id, $ignore_stock_load);
4756
				}
4757
			}
4758
		}
4759
	}
4760
4761
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4762
	/**
4763
	 *  Build the tree of subproducts into an array ->res and return it.
4764
	 *  this->sousprods must have been loaded by this->get_sousproduits_arbo()
4765
	 *
4766
	 * @param  int 		$multiply 			Because each sublevel must be multiplicated by parent nb
4767
	 * @param  int    	$ignore_stock_load 	Ignore stock load
4768
	 * @return array                    	$this->res
4769
	 */
4770
	public function get_arbo_each_prod($multiply = 1, $ignore_stock_load = 0)
4771
	{
4772
		// phpcs:enable
4773
		$this->res = array();
4774
		if (isset($this->sousprods) && is_array($this->sousprods)) {
4775
			foreach ($this->sousprods as $prod_name => $desc_product) {
4776
				if (is_array($desc_product)) {
4777
					$this->fetch_prod_arbo($desc_product, "", $multiply, 1, $this->id, $ignore_stock_load);
4778
				}
4779
			}
4780
		}
4781
		//var_dump($this->res);
4782
		return $this->res;
4783
	}
4784
4785
	/**
4786
	 * Count all parent and children products for current product (first level only)
4787
	 *
4788
	 * @param	int		$mode	0=Both parent and child, -1=Parents only, 1=Children only
4789
	 * @return 	int            	Nb of father + child
4790
	 * @see getFather(), get_sousproduits_arbo()
4791
	 */
4792
	public function hasFatherOrChild($mode = 0)
4793
	{
4794
		$nb = 0;
4795
4796
		$sql = "SELECT COUNT(pa.rowid) as nb";
4797
		$sql .= " FROM ".$this->db->prefix()."product_association as pa";
4798
		if ($mode == 0) {
4799
			$sql .= " WHERE pa.fk_product_fils = ".((int) $this->id)." OR pa.fk_product_pere = ".((int) $this->id);
4800
		} elseif ($mode == -1) {
4801
			$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)
4802
		} elseif ($mode == 1) {
4803
			$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)
4804
		}
4805
4806
		$resql = $this->db->query($sql);
4807
		if ($resql) {
4808
			$obj = $this->db->fetch_object($resql);
4809
			if ($obj) {
4810
				$nb = $obj->nb;
4811
			}
4812
		} else {
4813
			return -1;
4814
		}
4815
4816
		return $nb;
4817
	}
4818
4819
	/**
4820
	 * Return if a product has variants or not
4821
	 *
4822
	 * @return int        Number of variants
4823
	 */
4824
	public function hasVariants()
4825
	{
4826
		$nb = 0;
4827
		$sql = "SELECT count(rowid) as nb FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_parent = ".((int) $this->id);
4828
		$sql .= " AND entity IN (".getEntity('product').")";
4829
4830
		$resql = $this->db->query($sql);
4831
		if ($resql) {
4832
			$obj = $this->db->fetch_object($resql);
4833
			if ($obj) {
4834
				$nb = $obj->nb;
4835
			}
4836
		}
4837
4838
		return $nb;
4839
	}
4840
4841
4842
	/**
4843
	 * Return if loaded product is a variant
4844
	 *
4845
	 * @return int
4846
	 */
4847
	public function isVariant()
4848
	{
4849
		global $conf;
4850
		if (isModEnabled('variants')) {
4851
			$sql = "SELECT rowid FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_child = ".((int) $this->id)." AND entity IN (".getEntity('product').")";
4852
4853
			$query = $this->db->query($sql);
4854
4855
			if ($query) {
4856
				if (!$this->db->num_rows($query)) {
4857
					return false;
4858
				}
4859
				return true;
4860
			} else {
4861
				dol_print_error($this->db);
4862
				return -1;
4863
			}
4864
		} else {
4865
			return false;
4866
		}
4867
	}
4868
4869
	/**
4870
	 *  Return all parent products for current product (first level only)
4871
	 *
4872
	 * @return array|int         Array of product
4873
	 * @see hasFatherOrChild()
4874
	 */
4875
	public function getFather()
4876
	{
4877
		$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";
4878
		$sql .= ", p.tosell as status, p.tobuy as status_buy";
4879
		$sql .= " FROM ".$this->db->prefix()."product_association as pa,";
4880
		$sql .= " ".$this->db->prefix()."product as p";
4881
		$sql .= " WHERE p.rowid = pa.fk_product_pere";
4882
		$sql .= " AND pa.fk_product_fils = ".((int) $this->id);
4883
4884
		$res = $this->db->query($sql);
4885
		if ($res) {
4886
			$prods = array();
4887
			while ($record = $this->db->fetch_array($res)) {
4888
				// $record['id'] = $record['rowid'] = id of father
4889
				$prods[$record['id']]['id'] = $record['rowid'];
4890
				$prods[$record['id']]['ref'] = $record['ref'];
4891
				$prods[$record['id']]['label'] = $record['label'];
4892
				$prods[$record['id']]['qty'] = $record['qty'];
4893
				$prods[$record['id']]['incdec'] = $record['incdec'];
4894
				$prods[$record['id']]['fk_product_type'] = $record['fk_product_type'];
4895
				$prods[$record['id']]['entity'] = $record['entity'];
4896
				$prods[$record['id']]['status'] = $record['status'];
4897
				$prods[$record['id']]['status_buy'] = $record['status_buy'];
4898
			}
4899
			return $prods;
4900
		} else {
4901
			dol_print_error($this->db);
4902
			return -1;
4903
		}
4904
	}
4905
4906
4907
	/**
4908
	 *  Return childs of product $id
4909
	 *
4910
	 * @param  int $id             		Id of product to search childs of
4911
	 * @param  int $firstlevelonly 		Return only direct child
4912
	 * @param  int $level          		Level of recursing call (start to 1)
4913
	 * @param  array $parents   	    Array of all parents of $id
4914
	 * @return array|int                    Return array(prodid=>array(0=prodid, 1=>qty, 2=>product type, 3=>label, 4=>incdec, 5=>product ref)
4915
	 */
4916
	public function getChildsArbo($id, $firstlevelonly = 0, $level = 1, $parents = array())
4917
	{
4918
		global $alreadyfound;
4919
4920
		if (empty($id)) {
4921
			return array();
4922
		}
4923
4924
		$sql = "SELECT p.rowid, p.ref, p.label as label, p.fk_product_type,";
4925
		$sql .= " pa.qty as qty, pa.fk_product_fils as id, pa.incdec,";
4926
		$sql .= " pa.rowid as fk_association, pa.rang";
4927
		$sql .= " FROM ".$this->db->prefix()."product as p,";
4928
		$sql .= " ".$this->db->prefix()."product_association as pa";
4929
		$sql .= " WHERE p.rowid = pa.fk_product_fils";
4930
		$sql .= " AND pa.fk_product_pere = ".((int) $id);
4931
		$sql .= " AND pa.fk_product_fils <> ".((int) $id); // This should not happens, it is to avoid infinite loop if it happens
4932
		$sql.= " ORDER BY pa.rang";
4933
4934
		dol_syslog(get_class($this).'::getChildsArbo id='.$id.' level='.$level. ' parents='.(is_array($parents)?implode(',', $parents):$parents), LOG_DEBUG);
4935
4936
		if ($level == 1) {
4937
			$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 immediatly
4938
		}
4939
		// Protection against infinite loop
4940
		if ($level > 30) {
4941
			return array();
4942
		}
4943
4944
		$res = $this->db->query($sql);
4945
		if ($res) {
4946
			$prods = array();
4947
			while ($rec = $this->db->fetch_array($res)) {
4948
				if (!empty($alreadyfound[$rec['rowid']])) {
4949
					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);
4950
					if (in_array($rec['id'], $parents)) {
4951
						continue; // We discard this child if it is already found at a higher level in tree in the same branch.
4952
					}
4953
				}
4954
				$alreadyfound[$rec['rowid']] = 1;
4955
				$prods[$rec['rowid']] = array(
4956
					0=>$rec['rowid'],
4957
					1=>$rec['qty'],
4958
					2=>$rec['fk_product_type'],
4959
					3=>$this->db->escape($rec['label']),
4960
					4=>$rec['incdec'],
4961
					5=>$rec['ref'],
4962
					6=>$rec['fk_association'],
4963
					7=>$rec['rang']
4964
				);
4965
				//$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty'],2=>$rec['fk_product_type']);
4966
				//$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty']);
4967
				if (empty($firstlevelonly)) {
4968
					$listofchilds = $this->getChildsArbo($rec['rowid'], 0, $level + 1, array_push($parents, $rec['rowid']));
4969
					foreach ($listofchilds as $keyChild => $valueChild) {
4970
						$prods[$rec['rowid']]['childs'][$keyChild] = $valueChild;
4971
					}
4972
				}
4973
			}
4974
4975
			return $prods;
4976
		} else {
4977
			dol_print_error($this->db);
4978
			return -1;
4979
		}
4980
	}
4981
4982
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4983
	/**
4984
	 *     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)
4985
	 *     Set this->sousprods
4986
	 *
4987
	 * @return void
4988
	 */
4989
	public function get_sousproduits_arbo()
4990
	{
4991
		// phpcs:enable
4992
		$parent = array();
4993
4994
		foreach ($this->getChildsArbo($this->id) as $keyChild => $valueChild) {    // Warning. getChildsArbo can call getChildsArbo recursively. Starting point is $value[0]=id of product
4995
			$parent[$this->label][$keyChild] = $valueChild;
4996
		}
4997
		foreach ($parent as $key => $value) {        // key=label, value is array of childs
4998
			$this->sousprods[$key] = $value;
4999
		}
5000
	}
5001
5002
	/**
5003
	 * getTooltipContentArray
5004
	 * @param array $params params to construct tooltip data
5005
	 * @since v18
5006
	 * @return array
5007
	 */
5008
	public function getTooltipContentArray($params)
5009
	{
5010
		global $conf, $langs;
5011
5012
		$langs->load('products');
5013
5014
		$datas = [];
5015
5016
		if (!empty($conf->global->MAIN_OPTIMIZEFORTEXTBROWSER)) {
5017
			return ['optimize' => $langs->trans("ShowProduct")];
5018
		}
5019
5020
		if (!empty($this->entity)) {
5021
			$tmpphoto = $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 80);
5022
			if ($this->nbphoto > 0) {
5023
				$datas['photo'] = '<div class="photointooltip floatright">' . $tmpphoto . '</div>';
5024
				//$label .= '<div style="clear: both;"></div>';
5025
			}
5026
		}
5027
5028
		if ($this->type == Product::TYPE_PRODUCT) {
5029
			$datas['picto'] = img_picto('', 'product').' <u class="paddingrightonly">'.$langs->trans("Product").'</u>';
5030
		} elseif ($this->type == Product::TYPE_SERVICE) {
5031
			$datas['picto']= img_picto('', 'service').' <u class="paddingrightonly">'.$langs->trans("Service").'</u>';
5032
		}
5033
		if (isset($this->status) && isset($this->status_buy)) {
5034
			$datas['status']= ' '.$this->getLibStatut(5, 0) . ' '.$this->getLibStatut(5, 1);
5035
		}
5036
5037
		if (!empty($this->ref)) {
5038
			$datas['ref']= '<br><b>'.$langs->trans('ProductRef').':</b> '.$this->ref;
5039
		}
5040
		if (!empty($this->label)) {
5041
			$datas['label']= '<br><b>'.$langs->trans('ProductLabel').':</b> '.$this->label;
5042
		}
5043
		if ($this->type == Product::TYPE_PRODUCT || !empty($conf->global->STOCK_SUPPORTS_SERVICES)) {
5044
			if (isModEnabled('productbatch')) {
5045
				$langs->load("productbatch");
5046
				$datas['batchstatus']= "<br><b>".$langs->trans("ManageLotSerial").'</b>: '.$this->getLibStatut(0, 2);
5047
			}
5048
		}
5049
		if (isModEnabled('barcode')) {
5050
			$datas['barcode']= '<br><b>'.$langs->trans('BarCode').':</b> '.$this->barcode;
5051
		}
5052
5053
		if ($this->type == Product::TYPE_PRODUCT) {
5054
			if ($this->weight) {
5055
				$datas['weight']= "<br><b>".$langs->trans("Weight").'</b>: '.$this->weight.' '.measuringUnitString(0, "weight", $this->weight_units);
5056
			}
5057
			$labelsize = "";
5058
			if ($this->length) {
5059
				$labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Length").'</b>: '.$this->length.' '.measuringUnitString(0, 'size', $this->length_units);
5060
			}
5061
			if ($this->width) {
5062
				$labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Width").'</b>: '.$this->width.' '.measuringUnitString(0, 'size', $this->width_units);
5063
			}
5064
			if ($this->height) {
5065
				$labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Height").'</b>: '.$this->height.' '.measuringUnitString(0, 'size', $this->height_units);
5066
			}
5067
			if ($labelsize) {
5068
				$datas['size']= "<br>".$labelsize;
5069
			}
5070
5071
			$labelsurfacevolume = "";
5072
			if ($this->surface) {
5073
				$labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Surface").'</b>: '.$this->surface.' '.measuringUnitString(0, 'surface', $this->surface_units);
5074
			}
5075
			if ($this->volume) {
5076
				$labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Volume").'</b>: '.$this->volume.' '.measuringUnitString(0, 'volume', $this->volume_units);
5077
			}
5078
			if ($labelsurfacevolume) {
5079
				$datas['surface']= "<br>" . $labelsurfacevolume;
5080
			}
5081
		}
5082
		if (!empty($this->pmp) && $this->pmp) {
5083
			$datas['pmp'] = "<br><b>".$langs->trans("PMPValue").'</b>: '.price($this->pmp, 0, '', 1, -1, -1, $conf->currency);
5084
		}
5085
5086
		if (isModEnabled('accounting')) {
5087
			if ($this->status && isset($this->accountancy_code_sell)) {
5088
				include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
5089
				$selllabel = '<br>';
5090
				$selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellCode').':</b> '.length_accountg($this->accountancy_code_sell);
5091
				$selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellIntraCode').':</b> '.length_accountg($this->accountancy_code_sell_intra);
5092
				$selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellExportCode').':</b> '.length_accountg($this->accountancy_code_sell_export);
5093
				$datas['accountancysell'] = $selllabel;
5094
			}
5095
			if ($this->status_buy && isset($this->accountancy_code_buy)) {
5096
				include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
5097
				if (empty($this->status)) {
5098
					$buylabel = '<br>';
5099
				}
5100
				$buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyCode').':</b> '.length_accountg($this->accountancy_code_buy);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $buylabel does not seem to be defined for all execution paths leading up to this point.
Loading history...
5101
				$buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyIntraCode').':</b> '.length_accountg($this->accountancy_code_buy_intra);
5102
				$buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyExportCode').':</b> '.length_accountg($this->accountancy_code_buy_export);
5103
				$datas['accountancybuy'] = $buylabel;
5104
			}
5105
		}
5106
5107
		return $datas;
5108
	}
5109
5110
	/**
5111
	 *    Return clicable link of object (with eventually picto)
5112
	 *
5113
	 * @param	int		$withpicto				Add picto into link
5114
	 * @param	string	$option					Where point the link ('stock', 'composition', 'category', 'supplier', '')
5115
	 * @param	int		$maxlength				Maxlength of ref
5116
	 * @param 	int		$save_lastsearch_value	-1=Auto, 0=No save of lastsearch_values when clicking, 1=Save lastsearch_values whenclicking
5117
	 * @param	int		$notooltip				No tooltip
5118
	 * @param  	string  $morecss            	''=Add more css on link
5119
	 * @param	int		$add_label				0=Default, 1=Add label into string, >1=Add first chars into string
5120
	 * @param	string	$sep					' - '=Separator between ref and label if option 'add_label' is set
5121
	 * @return	string							String with URL
5122
	 */
5123
	public function getNomUrl($withpicto = 0, $option = '', $maxlength = 0, $save_lastsearch_value = -1, $notooltip = 0, $morecss = '', $add_label = 0, $sep = ' - ')
5124
	{
5125
		global $conf, $langs, $hookmanager;
5126
		include_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
5127
5128
		$result = '';
5129
5130
		$newref = $this->ref;
5131
		if ($maxlength) {
5132
			$newref = dol_trunc($newref, $maxlength, 'middle');
5133
		}
5134
		$params = [
5135
			'id' => $this->id,
5136
			'objecttype' => $this->element,
5137
			'option' => $option,
5138
		];
5139
		$classfortooltip = 'classfortooltip';
5140
		$dataparams = '';
5141
		if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
5142
			$classfortooltip = 'classforajaxtooltip';
5143
			$dataparams = ' data-params='.json_encode($params);
5144
			// $label = $langs->trans('Loading');
5145
		}
5146
5147
		$label = implode($this->getTooltipContentArray($params));
5148
5149
		$linkclose = '';
5150
		if (empty($notooltip)) {
5151
			if (!empty($conf->global->MAIN_OPTIMIZEFORTEXTBROWSER)) {
5152
				$label = $langs->trans("ShowProduct");
5153
				$linkclose .= ' alt="'.dol_escape_htmltag($label, 1).'"';
5154
			}
5155
			$linkclose .= ' title="'.dol_escape_htmltag($label, 1, 1).'"';
5156
			$linkclose .= $dataparams.' class="nowraponall '.$classfortooltip.($morecss ? ' '.$morecss : '').'"';
5157
		} else {
5158
			$linkclose = ' class="nowraponall'.($morecss ? ' '.$morecss : '').'"';
5159
		}
5160
5161
		if ($option == 'supplier' || $option == 'category') {
5162
			$url = DOL_URL_ROOT.'/product/fournisseurs.php?id='.$this->id;
5163
		} elseif ($option == 'stock') {
5164
			$url = DOL_URL_ROOT.'/product/stock/product.php?id='.$this->id;
5165
		} elseif ($option == 'composition') {
5166
			$url = DOL_URL_ROOT.'/product/composition/card.php?id='.$this->id;
5167
		} else {
5168
			$url = DOL_URL_ROOT.'/product/card.php?id='.$this->id;
5169
		}
5170
5171
		if ($option !== 'nolink') {
5172
			// Add param to save lastsearch_values or not
5173
			$add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
5174
			if ($save_lastsearch_value == -1 && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
5175
				$add_save_lastsearch_values = 1;
5176
			}
5177
			if ($add_save_lastsearch_values) {
5178
				$url .= '&save_lastsearch_values=1';
5179
			}
5180
		}
5181
5182
		$linkstart = '<a href="'.$url.'"';
5183
		$linkstart .= $linkclose.'>';
5184
		$linkend = '</a>';
5185
5186
		$result .= $linkstart;
5187
		if ($withpicto) {
5188
			if ($this->type == Product::TYPE_PRODUCT) {
5189
				$result .= (img_object(($notooltip ? '' : $label), 'product', ($notooltip ? 'class="paddingright"' : $dataparams.' class="paddingright '.$classfortooltip.'"'), 0, 0, $notooltip ? 0 : 1));
5190
			}
5191
			if ($this->type == Product::TYPE_SERVICE) {
5192
				$result .= (img_object(($notooltip ? '' : $label), 'service', ($notooltip ? 'class="paddingright"' : $dataparams.' class="paddingright '.$classfortooltip.'"'), 0, 0, $notooltip ? 0 : 1));
5193
			}
5194
		}
5195
		$result .= dol_escape_htmltag($newref);
5196
		$result .= $linkend;
5197
		if ($withpicto != 2) {
5198
			$result .= (($add_label && $this->label) ? $sep.dol_trunc($this->label, ($add_label > 1 ? $add_label : 0)) : '');
5199
		}
5200
5201
		global $action;
5202
		$hookmanager->initHooks(array('productdao'));
5203
		$parameters = array('id'=>$this->id, 'getnomurl' => &$result, 'label' => &$label);
5204
		$reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
5205
		if ($reshook > 0) {
5206
			$result = $hookmanager->resPrint;
5207
		} else {
5208
			$result .= $hookmanager->resPrint;
5209
		}
5210
5211
		return $result;
5212
	}
5213
5214
5215
	/**
5216
	 *  Create a document onto disk according to template module.
5217
	 *
5218
	 * @param  string    $modele      Force model to use ('' to not force)
5219
	 * @param  Translate $outputlangs Object langs to use for output
5220
	 * @param  int       $hidedetails Hide details of lines
5221
	 * @param  int       $hidedesc    Hide description
5222
	 * @param  int       $hideref     Hide ref
5223
	 * @return int                         0 if KO, 1 if OK
5224
	 */
5225
	public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0)
5226
	{
5227
		global $conf, $user, $langs;
5228
5229
		$langs->load("products");
5230
		$outputlangs->load("products");
5231
5232
		// Positionne le modele sur le nom du modele a utiliser
5233
		if (!dol_strlen($modele)) {
5234
			$modele = getDolGlobalString('PRODUCT_ADDON_PDF', 'strato');
5235
		}
5236
5237
		$modelpath = "core/modules/product/doc/";
5238
5239
		return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref);
5240
	}
5241
5242
	/**
5243
	 *    Return label of status of object
5244
	 *
5245
	 * @param  int $mode 0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=Short label + Picto
5246
	 * @param  int $type 0=Sell, 1=Buy, 2=Batch Number management
5247
	 * @return string          Label of status
5248
	 */
5249
	public function getLibStatut($mode = 0, $type = 0)
5250
	{
5251
		switch ($type) {
5252
			case 0:
5253
				return $this->LibStatut($this->status, $mode, $type);
5254
			case 1:
5255
				return $this->LibStatut($this->status_buy, $mode, $type);
5256
			case 2:
5257
				return $this->LibStatut($this->status_batch, $mode, $type);
5258
			default:
5259
				//Simulate previous behavior but should return an error string
5260
				return $this->LibStatut($this->status_buy, $mode, $type);
5261
		}
5262
	}
5263
5264
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5265
	/**
5266
	 *    Return label of a given status
5267
	 *
5268
	 * @param  int 		$status 	Statut
5269
	 * @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
5270
	 * @param  int 		$type   	0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
5271
	 * @return string              	Label of status
5272
	 */
5273
	public function LibStatut($status, $mode = 0, $type = 0)
5274
	{
5275
		// phpcs:enable
5276
		global $conf, $langs;
5277
5278
		$labelStatus = $labelStatusShort = '';
5279
5280
		$langs->load('products');
5281
		if (isModEnabled('productbatch')) {
5282
			$langs->load("productbatch");
5283
		}
5284
5285
		if ($type == 2) {
5286
			switch ($mode) {
5287
				case 0:
5288
					$label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatch') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial')));
5289
					return dolGetStatus($label);
5290
				case 1:
5291
					$label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort')));
5292
					return dolGetStatus($label);
5293
				case 2:
5294
					return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 1, 2);
5295
				case 3:
5296
					return dolGetStatus($langs->transnoentitiesnoconv('ProductStatusNotOnBatch'), '', '', empty($status) ? 'status5' : 'status4', 3, 'dot');
5297
				case 4:
5298
					return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 0, 2);
5299
				case 5:
5300
					return $this->LibStatut($status, 1, 2).' '.$this->LibStatut($status, 3, 2);
5301
				default:
5302
					return dolGetStatus($langs->transnoentitiesnoconv('Unknown'));
5303
			}
5304
		}
5305
5306
		$statuttrans = empty($status) ? 'status5' : 'status4';
5307
5308
		if ($status == 0) {
5309
			// $type   0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
5310
			if ($type == 0) {
5311
				$labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnSellShort');
5312
				$labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnSell');
5313
			} elseif ($type == 1) {
5314
				$labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBuyShort');
5315
				$labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBuy');
5316
			} elseif ($type == 2) {
5317
				$labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBatch');
5318
				$labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort');
5319
			}
5320
		} elseif ($status == 1) {
5321
			// $type   0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
5322
			if ($type == 0) {
5323
				$labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSellShort');
5324
				$labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSell');
5325
			} elseif ($type == 1) {
5326
				$labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnBuyShort');
5327
				$labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnBuy');
5328
			} elseif ($type == 2) {
5329
				$labelStatus = ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial'));
5330
				$labelStatusShort = ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort'));
5331
			}
5332
		} elseif ( $type == 2 && $status == 2 ) {
5333
			$labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSerial');
5334
			$labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSerialShort');
5335
		}
5336
5337
		if ($mode > 6) {
5338
			return dolGetStatus($langs->transnoentitiesnoconv('Unknown'), '', '', 'status0', 0);
5339
		} else {
5340
			return dolGetStatus($labelStatus, $labelStatusShort, '', $statuttrans, $mode);
5341
		}
5342
	}
5343
5344
5345
	/**
5346
	 *  Retour label of nature of product
5347
	 *
5348
	 * @return string        Label
5349
	 */
5350
	public function getLibFinished()
5351
	{
5352
		global $langs;
5353
		$langs->load('products');
5354
5355
		if (isset($this->finished) && $this->finished >= 0) {
5356
			$sql = "SELECT label, code FROM ".$this->db->prefix()."c_product_nature where code = ".((int) $this->finished)." AND active=1";
5357
			$resql = $this->db->query($sql);
5358
			if ($resql && $this->db->num_rows($resql) > 0) {
5359
				$res = $this->db->fetch_array($resql);
5360
				$label = $langs->trans($res['label']);
5361
				$this->db->free($resql);
5362
				return $label;
5363
			} else {
5364
				$this->error = $this->db->error().' sql='.$sql;
5365
				dol_syslog(__METHOD__.' Error '.$this->error, LOG_ERR);
5366
				return -1;
5367
			}
5368
		}
5369
5370
		return '';
5371
	}
5372
5373
5374
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5375
	/**
5376
	 *  Adjust stock in a warehouse for product
5377
	 *
5378
	 * @param  User   $user           user asking change
5379
	 * @param  int    $id_entrepot    id of warehouse
5380
	 * @param  double $nbpiece        nb of units (should be always positive, use $movement to decide if we add or remove)
5381
	 * @param  int    $movement       0 = add, 1 = remove
5382
	 * @param  string $label          Label of stock movement
5383
	 * @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.
5384
	 * @param  string $inventorycode  Inventory code
5385
	 * @param  string $origin_element Origin element type
5386
	 * @param  int    $origin_id      Origin id of element
5387
	 * @param  int	  $disablestockchangeforsubproduct	Disable stock change for sub-products of kit (usefull only if product is a subproduct)
5388
	 * @param  Extrafields $extrafields	  Array of extrafields
5389
	 * @return int                    <0 if KO, >0 if OK
5390
	 */
5391
	public function correct_stock($user, $id_entrepot, $nbpiece, $movement, $label = '', $price = 0, $inventorycode = '', $origin_element = '', $origin_id = null, $disablestockchangeforsubproduct = 0, $extrafields = null)
5392
	{
5393
		// phpcs:enable
5394
		if ($id_entrepot) {
5395
			$this->db->begin();
5396
5397
			include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
5398
5399
			if ($nbpiece < 0) {
5400
				if (!$movement) {
5401
					$movement = 1;
5402
				}
5403
				$nbpiece = abs($nbpiece);
5404
			}
5405
5406
			$op[0] = "+".trim($nbpiece);
0 ignored issues
show
Comprehensibility Best Practice introduced by
$op was never initialized. Although not strictly required by PHP, it is generally a good practice to add $op = array(); before regardless.
Loading history...
5407
			$op[1] = "-".trim($nbpiece);
5408
5409
			$movementstock = new MouvementStock($this->db);
5410
			$movementstock->setOrigin($origin_element, $origin_id); // Set ->origin_type and ->origin_id
5411
			$result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', '', '', '', false, 0, $disablestockchangeforsubproduct);
5412
5413
			if ($result >= 0) {
5414
				if ($extrafields) {
5415
					$array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
5416
					$movementstock->array_options = $array_options;
5417
					$movementstock->insertExtraFields();
5418
				}
5419
				$this->db->commit();
5420
				return 1;
5421
			} else {
5422
				$this->error = $movementstock->error;
5423
				$this->errors = $movementstock->errors;
5424
5425
				$this->db->rollback();
5426
				return -1;
5427
			}
5428
		}
5429
	}
5430
5431
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5432
	/**
5433
	 *  Adjust stock in a warehouse for product with batch number
5434
	 *
5435
	 * @param  User     $user           user asking change
5436
	 * @param  int      $id_entrepot    id of warehouse
5437
	 * @param  double   $nbpiece        nb of units (should be always positive, use $movement to decide if we add or remove)
5438
	 * @param  int      $movement       0 = add, 1 = remove
5439
	 * @param  string   $label          Label of stock movement
5440
	 * @param  double   $price          Price to use for stock eval
5441
	 * @param  integer  $dlc            eat-by date
5442
	 * @param  integer  $dluo           sell-by date
5443
	 * @param  string   $lot            Lot number
5444
	 * @param  string   $inventorycode  Inventory code
5445
	 * @param  string   $origin_element Origin element type
5446
	 * @param  int      $origin_id      Origin id of element
5447
	 * @param  int	    $disablestockchangeforsubproduct	Disable stock change for sub-products of kit (usefull only if product is a subproduct)
5448
	 * @param  Extrafields $extrafields	Array of extrafields
5449
	 * @return int                      <0 if KO, >0 if OK
5450
	 */
5451
	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)
5452
	{
5453
		// phpcs:enable
5454
		if ($id_entrepot) {
5455
			$this->db->begin();
5456
5457
			include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
5458
5459
			if ($nbpiece < 0) {
5460
				if (!$movement) {
5461
					$movement = 1;
5462
				}
5463
				$nbpiece = abs($nbpiece);
5464
			}
5465
5466
			$op[0] = "+".trim($nbpiece);
0 ignored issues
show
Comprehensibility Best Practice introduced by
$op was never initialized. Although not strictly required by PHP, it is generally a good practice to add $op = array(); before regardless.
Loading history...
5467
			$op[1] = "-".trim($nbpiece);
5468
5469
			$movementstock = new MouvementStock($this->db);
5470
			$movementstock->setOrigin($origin_element, $origin_id); // Set ->origin_type and ->fk_origin
5471
			$result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', $dlc, $dluo, $lot, false, 0, $disablestockchangeforsubproduct);
5472
5473
			if ($result >= 0) {
5474
				if ($extrafields) {
5475
					$array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
5476
					$movementstock->array_options = $array_options;
5477
					$movementstock->insertExtraFields();
5478
				}
5479
				$this->db->commit();
5480
				return 1;
5481
			} else {
5482
				$this->error = $movementstock->error;
5483
				$this->errors = $movementstock->errors;
5484
5485
				$this->db->rollback();
5486
				return -1;
5487
			}
5488
		}
5489
	}
5490
5491
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5492
	/**
5493
	 * Load information about stock of a product into ->stock_reel, ->stock_warehouse[] (including stock_warehouse[idwarehouse]->detail_batch for batch products)
5494
	 * This function need a lot of load. If you use it on list, use a cache to execute it once for each product id.
5495
	 * If ENTREPOT_EXTRA_STATUS is set, filtering on warehouse status is possible.
5496
	 *
5497
	 * @param  	string 	$option 					'' = Load all stock info, also from closed and internal warehouses, 'nobatch', 'novirtual'
5498
	 * 												You can also filter on 'warehouseclosed', 'warehouseopen', 'warehouseinternal'
5499
	 * @param	int		$includedraftpoforvirtual	Include draft status of PO for virtual stock calculation
5500
	 * @param	int		$dateofvirtualstock			Date of virtual stock
5501
	 * @return 	int                  				< 0 if KO, > 0 if OK
5502
	 * @see    	load_virtual_stock(), loadBatchInfo()
5503
	 */
5504
	public function load_stock($option = '', $includedraftpoforvirtual = null, $dateofvirtualstock = null)
5505
	{
5506
		// phpcs:enable
5507
		global $conf;
5508
5509
		$this->stock_reel = 0;
5510
		$this->stock_warehouse = array();
5511
		$this->stock_theorique = 0;
5512
5513
		// Set filter on warehouse status
5514
		$warehouseStatus = array();
5515
		if (preg_match('/warehouseclosed/', $option)) {
5516
			$warehouseStatus[Entrepot::STATUS_CLOSED] = Entrepot::STATUS_CLOSED;
5517
		}
5518
		if (preg_match('/warehouseopen/', $option)) {
5519
			$warehouseStatus[Entrepot::STATUS_OPEN_ALL] = Entrepot::STATUS_OPEN_ALL;
5520
		}
5521
		if (preg_match('/warehouseinternal/', $option)) {
5522
			if (!empty($conf->global->ENTREPOT_EXTRA_STATUS)) {
5523
				$warehouseStatus[Entrepot::STATUS_OPEN_INTERNAL] = Entrepot::STATUS_OPEN_INTERNAL;
5524
			} else {
5525
				$warehouseStatus[Entrepot::STATUS_OPEN_ALL] = Entrepot::STATUS_OPEN_ALL;
5526
			}
5527
		}
5528
5529
		$sql = "SELECT ps.rowid, ps.reel, ps.fk_entrepot";
5530
		$sql .= " FROM ".$this->db->prefix()."product_stock as ps";
5531
		$sql .= ", ".$this->db->prefix()."entrepot as w";
5532
		$sql .= " WHERE w.entity IN (".getEntity('stock').")";
5533
		$sql .= " AND w.rowid = ps.fk_entrepot";
5534
		$sql .= " AND ps.fk_product = ".((int) $this->id);
5535
		if (count($warehouseStatus)) {
5536
			$sql .= " AND w.statut IN (".$this->db->sanitize(implode(',', $warehouseStatus)).")";
5537
		}
5538
5539
		$sql .= " ORDER BY ps.reel ".(!empty($conf->global->DO_NOT_TRY_TO_DEFRAGMENT_STOCKS_WAREHOUSE)?'DESC':'ASC'); // Note : qty ASC is important for expedition card, to avoid stock fragmentation;
5540
5541
		dol_syslog(get_class($this)."::load_stock", LOG_DEBUG);
5542
		$result = $this->db->query($sql);
5543
		if ($result) {
5544
			$num = $this->db->num_rows($result);
5545
			$i = 0;
5546
			if ($num > 0) {
5547
				while ($i < $num) {
5548
					$row = $this->db->fetch_object($result);
5549
					$this->stock_warehouse[$row->fk_entrepot] = new stdClass();
5550
					$this->stock_warehouse[$row->fk_entrepot]->real = $row->reel;
5551
					$this->stock_warehouse[$row->fk_entrepot]->id = $row->rowid;
5552
					if ((!preg_match('/nobatch/', $option)) && $this->hasbatch()) {
5553
						$this->stock_warehouse[$row->fk_entrepot]->detail_batch = Productbatch::findAll($this->db, $row->rowid, 1, $this->id);
5554
					}
5555
					$this->stock_reel += $row->reel;
5556
					$i++;
5557
				}
5558
			}
5559
			$this->db->free($result);
5560
5561
			if (!preg_match('/novirtual/', $option)) {
5562
				$this->load_virtual_stock($includedraftpoforvirtual, $dateofvirtualstock); // This also load all arrays stats_xxx...
5563
			}
5564
5565
			return 1;
5566
		} else {
5567
			$this->error = $this->db->lasterror();
5568
			return -1;
5569
		}
5570
	}
5571
5572
5573
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5574
	/**
5575
	 *  Load value ->stock_theorique of a product. Property this->id must be defined.
5576
	 *  This function need a lot of load. If you use it on list, use a cache to execute it one for each product id.
5577
	 *
5578
	 * 	@param	int		$includedraftpoforvirtual	Include draft status and not yet approved Purchase Orders for virtual stock calculation
5579
	 *  @param	int		$dateofvirtualstock			Date of virtual stock
5580
	 *  @return int     							< 0 if KO, > 0 if OK
5581
	 *  @see	load_stock(), loadBatchInfo()
5582
	 */
5583
	public function load_virtual_stock($includedraftpoforvirtual = null, $dateofvirtualstock = null)
5584
	{
5585
		// phpcs:enable
5586
		global $conf, $hookmanager, $action;
5587
5588
		$stock_commande_client = 0;
5589
		$stock_commande_fournisseur = 0;
5590
		$stock_sending_client = 0;
5591
		$stock_reception_fournisseur = 0;
5592
		$stock_inproduction = 0;
5593
5594
		//dol_syslog("load_virtual_stock");
5595
5596
		if (isModEnabled('commande')) {
5597
			$result = $this->load_stats_commande(0, '1,2', 1);
5598
			if ($result < 0) {
5599
				dol_print_error($this->db, $this->error);
5600
			}
5601
			$stock_commande_client = $this->stats_commande['qty'];
5602
		}
5603
		if (isModEnabled("expedition")) {
5604
			require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
5605
			$filterShipmentStatus = '';
5606
			if (!empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT)) {
5607
				$filterShipmentStatus = Expedition::STATUS_VALIDATED.','.Expedition::STATUS_CLOSED;
5608
			} elseif (!empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE)) {
5609
				$filterShipmentStatus = Expedition::STATUS_CLOSED;
5610
			}
5611
			$result = $this->load_stats_sending(0, '1,2', 1, $filterShipmentStatus);
5612
			if ($result < 0) {
5613
				dol_print_error($this->db, $this->error);
5614
			}
5615
			$stock_sending_client = $this->stats_expedition['qty'];
5616
		}
5617
		if ((isModEnabled("fournisseur") && empty($conf->global->MAIN_USE_NEW_SUPPLIERMOD)) || isModEnabled("supplier_order")) {
5618
			$filterStatus = empty($conf->global->SUPPLIER_ORDER_STATUS_FOR_VIRTUAL_STOCK) ? '3,4' : $conf->global->SUPPLIER_ORDER_STATUS_FOR_VIRTUAL_STOCK;
5619
			if (isset($includedraftpoforvirtual)) {
5620
				$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
5621
			}
5622
			$result = $this->load_stats_commande_fournisseur(0, $filterStatus, 1, $dateofvirtualstock);
5623
			if ($result < 0) {
5624
				dol_print_error($this->db, $this->error);
5625
			}
5626
			$stock_commande_fournisseur = $this->stats_commande_fournisseur['qty'];
5627
		}
5628
		if (((isModEnabled("fournisseur") && empty($conf->global->MAIN_USE_NEW_SUPPLIERMOD)) || isModEnabled("supplier_order") || isModEnabled("supplier_invoice")) && empty($conf->reception->enabled)) {
5629
			// Case module reception is not used
5630
			$filterStatus = '4';
5631
			if (isset($includedraftpoforvirtual)) {
5632
				$filterStatus = '0,'.$filterStatus;
5633
			}
5634
			$result = $this->load_stats_reception(0, $filterStatus, 1, $dateofvirtualstock);
5635
			if ($result < 0) {
5636
				dol_print_error($this->db, $this->error);
5637
			}
5638
			$stock_reception_fournisseur = $this->stats_reception['qty'];
5639
		}
5640
		if (((isModEnabled("fournisseur") && empty($conf->global->MAIN_USE_NEW_SUPPLIERMOD)) || isModEnabled("supplier_order") || isModEnabled("supplier_invoice")) && isModEnabled("reception")) {
5641
			// Case module reception is used
5642
			$filterStatus = '4';
5643
			if (isset($includedraftpoforvirtual)) {
5644
				$filterStatus = '0,'.$filterStatus;
5645
			}
5646
			$result = $this->load_stats_reception(0, $filterStatus, 1, $dateofvirtualstock); // Use same tables than when module reception is not used.
5647
			if ($result < 0) {
5648
				dol_print_error($this->db, $this->error);
5649
			}
5650
			$stock_reception_fournisseur = $this->stats_reception['qty'];
5651
		}
5652
		if (isModEnabled('mrp')) {
5653
			$result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock);
5654
			if ($result < 0) {
5655
				dol_print_error($this->db, $this->error);
5656
			}
5657
			$stock_inproduction = $this->stats_mrptoproduce['qty'] - $this->stats_mrptoconsume['qty'];
5658
		}
5659
5660
		$this->stock_theorique = $this->stock_reel + $stock_inproduction;
5661
5662
		// Stock decrease mode
5663
		if (!empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT) || !empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE)) {
5664
			$this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
5665
		} elseif (!empty($conf->global->STOCK_CALCULATE_ON_VALIDATE_ORDER)) {
5666
			$this->stock_theorique += 0;
5667
		} elseif (!empty($conf->global->STOCK_CALCULATE_ON_BILL)) {
5668
			$this->stock_theorique -= $stock_commande_client;
5669
		}
5670
		// Stock Increase mode
5671
		if (!empty($conf->global->STOCK_CALCULATE_ON_RECEPTION) || !empty($conf->global->STOCK_CALCULATE_ON_RECEPTION_CLOSE)) {
5672
			$this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
5673
		} elseif (!empty($conf->global->STOCK_CALCULATE_ON_SUPPLIER_DISPATCH_ORDER)) {
5674
			$this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
5675
		} elseif (!empty($conf->global->STOCK_CALCULATE_ON_SUPPLIER_VALIDATE_ORDER)) {
5676
			$this->stock_theorique -= $stock_reception_fournisseur;
5677
		} elseif (!empty($conf->global->STOCK_CALCULATE_ON_SUPPLIER_BILL)) {
5678
			$this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
5679
		}
5680
5681
		if (!is_object($hookmanager)) {
5682
			include_once DOL_DOCUMENT_ROOT.'/core/class/hookmanager.class.php';
5683
			$hookmanager = new HookManager($this->db);
5684
		}
5685
		$hookmanager->initHooks(array('productdao'));
5686
		$parameters = array('id'=>$this->id, 'includedraftpoforvirtual' => $includedraftpoforvirtual);
5687
		// Note that $action and $object may have been modified by some hooks
5688
		$reshook = $hookmanager->executeHooks('loadvirtualstock', $parameters, $this, $action);
5689
		if ($reshook > 0) {
5690
			$this->stock_theorique = $hookmanager->resArray['stock_theorique'];
5691
		}
5692
5693
		return 1;
5694
	}
5695
5696
5697
	/**
5698
	 *  Load existing information about a serial
5699
	 *
5700
	 * @param  string $batch Lot/serial number
5701
	 * @return array                    Array with record into product_batch
5702
	 * @see    load_stock(), load_virtual_stock()
5703
	 */
5704
	public function loadBatchInfo($batch)
5705
	{
5706
		$result = array();
5707
5708
		$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";
5709
		$sql .= " WHERE pb.fk_product_stock = ps.rowid AND ps.fk_product = ".((int) $this->id)." AND pb.batch = '".$this->db->escape($batch)."'";
5710
		$sql .= " GROUP BY pb.batch, pb.eatby, pb.sellby";
5711
		dol_syslog(get_class($this)."::loadBatchInfo load first entry found for lot/serial = ".$batch, LOG_DEBUG);
5712
		$resql = $this->db->query($sql);
5713
		if ($resql) {
5714
			$num = $this->db->num_rows($resql);
5715
			$i = 0;
5716
			while ($i < $num) {
5717
				$obj = $this->db->fetch_object($resql);
5718
				$result[] = array('batch'=>$batch, 'eatby'=>$this->db->jdate($obj->eatby), 'sellby'=>$this->db->jdate($obj->sellby), 'qty'=>$obj->qty);
5719
				$i++;
5720
			}
5721
			return $result;
5722
		} else {
5723
			dol_print_error($this->db);
5724
			$this->db->rollback();
5725
			return array();
5726
		}
5727
	}
5728
5729
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5730
	/**
5731
	 *  Move an uploaded file described into $file array into target directory $sdir.
5732
	 *
5733
	 * @param  string $sdir Target directory
5734
	 * @param  string $file Array of file info of file to upload: array('name'=>..., 'tmp_name'=>...)
5735
	 * @return int                    <0 if KO, >0 if OK
5736
	 */
5737
	public function add_photo($sdir, $file)
5738
	{
5739
		// phpcs:enable
5740
		global $conf;
5741
5742
		include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
5743
5744
		$result = 0;
5745
5746
		$dir = $sdir;
5747
		if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
5748
			$dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos";
5749
		} else {
5750
			$dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product').dol_sanitizeFileName($this->ref);
5751
		}
5752
5753
		dol_mkdir($dir);
5754
5755
		$dir_osencoded = $dir;
5756
5757
		if (is_dir($dir_osencoded)) {
5758
			$originImage = $dir.'/'.$file['name'];
5759
5760
			// Cree fichier en taille origine
5761
			$result = dol_move_uploaded_file($file['tmp_name'], $originImage, 1);
5762
5763
			if (file_exists(dol_osencode($originImage))) {
5764
				// Create thumbs
5765
				$this->addThumbs($originImage);
5766
			}
5767
		}
5768
5769
		if (is_numeric($result) && $result > 0) {
5770
			return 1;
5771
		} else {
5772
			return -1;
5773
		}
5774
	}
5775
5776
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5777
	/**
5778
	 *  Return if at least one photo is available
5779
	 *
5780
	 * @param  string $sdir Directory to scan
5781
	 * @return boolean                 True if at least one photo is available, False if not
5782
	 */
5783
	public function is_photo_available($sdir)
5784
	{
5785
		// phpcs:enable
5786
		include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
5787
		include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
5788
5789
		global $conf;
5790
5791
		$dir = $sdir;
5792
		if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
5793
			$dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos/";
5794
		} else {
5795
			$dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product');
5796
		}
5797
5798
		$nbphoto = 0;
5799
5800
		$dir_osencoded = dol_osencode($dir);
5801
		if (file_exists($dir_osencoded)) {
5802
			$handle = opendir($dir_osencoded);
5803
			if (is_resource($handle)) {
5804
				while (($file = readdir($handle)) !== false) {
5805
					if (!utf8_check($file)) {
5806
						$file = utf8_encode($file); // To be sure data is stored in UTF8 in memory
5807
					}
5808
					if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
5809
						return true;
5810
					}
5811
				}
5812
			}
5813
		}
5814
		return false;
5815
	}
5816
5817
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5818
	/**
5819
	 * Return an array with all photos of product found on disk. There is no sorting criteria.
5820
	 *
5821
	 * @param  string $dir   	Directory to scan
5822
	 * @param  int    $nbmax 	Number maxium of photos (0=no maximum)
5823
	 * @return array            Array of photos
5824
	 */
5825
	public function liste_photos($dir, $nbmax = 0)
5826
	{
5827
		// phpcs:enable
5828
		include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
5829
		include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
5830
5831
		$nbphoto = 0;
5832
		$tabobj = array();
5833
5834
		$dir_osencoded = dol_osencode($dir);
5835
		$handle = @opendir($dir_osencoded);
5836
		if (is_resource($handle)) {
5837
			while (($file = readdir($handle)) !== false) {
5838
				if (!utf8_check($file)) {
5839
					$file = utf8_encode($file); // readdir returns ISO
5840
				}
5841
				if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
5842
					$nbphoto++;
5843
5844
					// We forge name of thumb.
5845
					$photo = $file;
5846
					$photo_vignette = '';
5847
					$regs = array();
5848
					if (preg_match('/('.$this->regeximgext.')$/i', $photo, $regs)) {
5849
						$photo_vignette = preg_replace('/'.$regs[0].'/i', '', $photo).'_small'.$regs[0];
5850
					}
5851
5852
					$dirthumb = $dir.'thumbs/';
5853
5854
					// Objet
5855
					$obj = array();
5856
					$obj['photo'] = $photo;
5857
					if ($photo_vignette && dol_is_file($dirthumb.$photo_vignette)) {
5858
						$obj['photo_vignette'] = 'thumbs/'.$photo_vignette;
5859
					} else {
5860
						$obj['photo_vignette'] = "";
5861
					}
5862
5863
					$tabobj[$nbphoto - 1] = $obj;
5864
5865
					// Do we have to continue with next photo ?
5866
					if ($nbmax && $nbphoto >= $nbmax) {
5867
						break;
5868
					}
5869
				}
5870
			}
5871
5872
			closedir($handle);
5873
		}
5874
5875
		return $tabobj;
5876
	}
5877
5878
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5879
	/**
5880
	 *  Delete a photo and its thumbs
5881
	 *
5882
	 * @param  string $file 	Path to image file
5883
	 * @return void
5884
	 */
5885
	public function delete_photo($file)
5886
	{
5887
		// phpcs:enable
5888
		include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
5889
		include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
5890
5891
		$dir = dirname($file).'/'; // Chemin du dossier contenant l'image d'origine
5892
		$dirthumb = $dir.'/thumbs/'; // Chemin du dossier contenant la vignette
5893
		$filename = preg_replace('/'.preg_quote($dir, '/').'/i', '', $file); // Nom du fichier
5894
5895
		// On efface l'image d'origine
5896
		dol_delete_file($file, 0, 0, 0, $this); // For triggers
5897
5898
		// Si elle existe, on efface la vignette
5899
		if (preg_match('/('.$this->regeximgext.')$/i', $filename, $regs)) {
5900
			$photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_small'.$regs[0];
5901
			if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
5902
				dol_delete_file($dirthumb.$photo_vignette);
5903
			}
5904
5905
			$photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_mini'.$regs[0];
5906
			if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
5907
				dol_delete_file($dirthumb.$photo_vignette);
5908
			}
5909
		}
5910
	}
5911
5912
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5913
	/**
5914
	 *  Load size of image file
5915
	 *
5916
	 * @param  string $file Path to file
5917
	 * @return void
5918
	 */
5919
	public function get_image_size($file)
5920
	{
5921
		// phpcs:enable
5922
		$file_osencoded = dol_osencode($file);
5923
		$infoImg = getimagesize($file_osencoded); // Get information on image
5924
		$this->imgWidth = $infoImg[0]; // Largeur de l'image
5925
		$this->imgHeight = $infoImg[1]; // Hauteur de l'image
5926
	}
5927
5928
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5929
	/**
5930
	 *  Load indicators this->nb for the dashboard
5931
	 *
5932
	 * @return int                 <0 if KO, >0 if OK
5933
	 */
5934
	public function load_state_board()
5935
	{
5936
		// phpcs:enable
5937
		global $hookmanager;
5938
5939
		$this->nb = array();
5940
5941
		$sql = "SELECT count(p.rowid) as nb, fk_product_type";
5942
		$sql .= " FROM ".$this->db->prefix()."product as p";
5943
		$sql .= ' WHERE p.entity IN ('.getEntity($this->element, 1).')';
5944
		// Add where from hooks
5945
		if (is_object($hookmanager)) {
5946
			$parameters = array();
5947
			$reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
5948
			$sql .= $hookmanager->resPrint;
5949
		}
5950
		$sql .= ' GROUP BY fk_product_type';
5951
5952
		$resql = $this->db->query($sql);
5953
		if ($resql) {
5954
			while ($obj = $this->db->fetch_object($resql)) {
5955
				if ($obj->fk_product_type == 1) {
5956
					$this->nb["services"] = $obj->nb;
5957
				} else {
5958
					$this->nb["products"] = $obj->nb;
5959
				}
5960
			}
5961
			$this->db->free($resql);
5962
			return 1;
5963
		} else {
5964
			dol_print_error($this->db);
5965
			$this->error = $this->db->error();
5966
			return -1;
5967
		}
5968
	}
5969
5970
	/**
5971
	 * Return if object is a product
5972
	 *
5973
	 * @return boolean     True if it's a product
5974
	 */
5975
	public function isProduct()
5976
	{
5977
		return ($this->type == Product::TYPE_PRODUCT ? true : false);
5978
	}
5979
5980
	/**
5981
	 * Return if object is a product
5982
	 *
5983
	 * @return boolean     True if it's a service
5984
	 */
5985
	public function isService()
5986
	{
5987
		return ($this->type == Product::TYPE_SERVICE ? true : false);
5988
	}
5989
5990
5991
	/**
5992
	 * Return if  object have a constraint on mandatory_period
5993
	 *
5994
	 * @return boolean     True if mandatory_period setted to 1
5995
	 */
5996
	public function isMandatoryPeriod()
5997
	{
5998
		return ($this->mandatory_period == 1 ? true : false);
5999
	}
6000
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6001
	/**
6002
	 *  Get a barcode from the module to generate barcode values.
6003
	 *  Return value is stored into this->barcode
6004
	 *
6005
	 * @param  Product $object Object product or service
6006
	 * @param  string  $type   Barcode type (ean, isbn, ...)
6007
	 * @return string
6008
	 */
6009
	public function get_barcode($object, $type = '')
6010
	{
6011
		// phpcs:enable
6012
		global $conf;
6013
6014
		$result = '';
6015
		if (!empty($conf->global->BARCODE_PRODUCT_ADDON_NUM)) {
6016
			$dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
6017
			foreach ($dirsociete as $dirroot) {
6018
				$res = dol_include_once($dirroot.$conf->global->BARCODE_PRODUCT_ADDON_NUM.'.php');
6019
				if ($res) {
6020
					break;
6021
				}
6022
			}
6023
			$var = $conf->global->BARCODE_PRODUCT_ADDON_NUM;
6024
			$mod = new $var;
6025
6026
			$result = $mod->getNextValue($object, $type);
6027
6028
			dol_syslog(get_class($this)."::get_barcode barcode=".$result." module=".$var);
6029
		}
6030
		return $result;
6031
	}
6032
6033
	/**
6034
	 *  Initialise an instance with random values.
6035
	 *  Used to build previews or test instances.
6036
	 *    id must be 0 if object instance is a specimen.
6037
	 *
6038
	 * @return void
6039
	 */
6040
	public function initAsSpecimen()
6041
	{
6042
		global $user, $langs, $conf, $mysoc;
6043
6044
		$now = dol_now();
6045
6046
		// Initialize parameters
6047
		$this->specimen = 1;
6048
		$this->id = 0;
6049
		$this->ref = 'PRODUCT_SPEC';
6050
		$this->label = 'PRODUCT SPECIMEN';
6051
		$this->description = 'This is description of this product specimen that was created the '.dol_print_date($now, 'dayhourlog').'.';
6052
		$this->specimen = 1;
6053
		$this->country_id = 1;
6054
		$this->status = 1;
6055
		$this->status_buy = 1;
6056
		$this->tobatch = 0;
6057
		$this->note_private = 'This is a comment (private)';
6058
		$this->note_public = 'This is a comment (public)';
6059
		$this->date_creation = $now;
6060
		$this->date_modification = $now;
6061
6062
		$this->weight = 4;
6063
		$this->weight_units = 3;
6064
6065
		$this->length = 5;
6066
		$this->length_units = 1;
6067
		$this->width = 6;
6068
		$this->width_units = 0;
6069
		$this->height = null;
6070
		$this->height_units = null;
6071
6072
		$this->surface = 30;
6073
		$this->surface_units = 0;
6074
		$this->volume = 300;
6075
		$this->volume_units = 0;
6076
6077
		$this->barcode = -1; // Create barcode automatically
6078
	}
6079
6080
	/**
6081
	 *    Returns the text label from units dictionary
6082
	 *
6083
	 * @param  string $type Label type (long or short)
6084
	 * @return string|int <0 if ko, label if ok
6085
	 */
6086
	public function getLabelOfUnit($type = 'long')
6087
	{
6088
		global $langs;
6089
6090
		if (!$this->fk_unit) {
6091
			return '';
6092
		}
6093
6094
		$langs->load('products');
6095
6096
		$label_type = 'label';
6097
		if ($type == 'short') {
6098
			$label_type = 'short_label';
6099
		}
6100
6101
		$sql = "SELECT ".$label_type.", code from ".$this->db->prefix()."c_units where rowid = ".((int) $this->fk_unit);
6102
6103
		$resql = $this->db->query($sql);
6104
		if ($resql && $this->db->num_rows($resql) > 0) {
6105
			$res = $this->db->fetch_array($resql);
6106
			$label = ($label_type == 'short_label' ? $res[$label_type] : 'unit'.$res['code']);
6107
			$this->db->free($resql);
6108
			return $label;
6109
		} else {
6110
			$this->error = $this->db->error();
6111
			dol_syslog(get_class($this)."::getLabelOfUnit Error ".$this->error, LOG_ERR);
6112
			return -1;
6113
		}
6114
	}
6115
6116
	/**
6117
	 * Return if object has a sell-by date or eat-by date
6118
	 *
6119
	 * @return boolean     True if it's has
6120
	 */
6121
	public function hasbatch()
6122
	{
6123
		return ($this->status_batch > 0 ? true : false);
6124
	}
6125
6126
6127
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6128
	/**
6129
	 * Return minimum product recommended price
6130
	 *
6131
	 * @return int            Minimum recommanded price that is higher price among all suppliers * PRODUCT_MINIMUM_RECOMMENDED_PRICE
6132
	 */
6133
	public function min_recommended_price()
6134
	{
6135
		// phpcs:enable
6136
		global $conf;
6137
6138
		$maxpricesupplier = 0;
6139
6140
		if (!empty($conf->global->PRODUCT_MINIMUM_RECOMMENDED_PRICE)) {
6141
			include_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
6142
			$product_fourn = new ProductFournisseur($this->db);
6143
			$product_fourn_list = $product_fourn->list_product_fournisseur_price($this->id, '', '');
6144
6145
			if (is_array($product_fourn_list) && count($product_fourn_list) > 0) {
6146
				foreach ($product_fourn_list as $productfourn) {
6147
					if ($productfourn->fourn_unitprice > $maxpricesupplier) {
6148
						$maxpricesupplier = $productfourn->fourn_unitprice;
6149
					}
6150
				}
6151
6152
				$maxpricesupplier *= $conf->global->PRODUCT_MINIMUM_RECOMMENDED_PRICE;
6153
			}
6154
		}
6155
6156
		return $maxpricesupplier;
6157
	}
6158
6159
6160
	/**
6161
	 * Sets object to supplied categories.
6162
	 *
6163
	 * Deletes object from existing categories not supplied.
6164
	 * Adds it to non existing supplied categories.
6165
	 * Existing categories are left untouch.
6166
	 *
6167
	 * @param  int[]|int 	$categories 	Category or categories IDs
6168
	 * @return int							<0 if KO, >0 if OK
6169
	 */
6170
	public function setCategories($categories)
6171
	{
6172
		require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php';
6173
		return parent::setCategoriesCommon($categories, Categorie::TYPE_PRODUCT);
6174
	}
6175
6176
	/**
6177
	 * Function used to replace a thirdparty id with another one.
6178
	 *
6179
	 * @param  DoliDB $dbs        	Database handler
6180
	 * @param  int    $origin_id 	Old thirdparty id
6181
	 * @param  int    $dest_id   	New thirdparty id
6182
	 * @return bool
6183
	 */
6184
	public static function replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
6185
	{
6186
		$tables = array(
6187
			'product_customer_price',
6188
			'product_customer_price_log'
6189
		);
6190
6191
		return CommonObject::commonReplaceThirdparty($dbs, $origin_id, $dest_id, $tables);
6192
	}
6193
6194
	/**
6195
	 * Generates prices for a product based on product multiprice generation rules
6196
	 *
6197
	 * @param  User   $user       User that updates the prices
6198
	 * @param  float  $baseprice  Base price
6199
	 * @param  string $price_type Base price type
6200
	 * @param  float  $price_vat  VAT % tax
6201
	 * @param  int    $npr        NPR
6202
	 * @param  string $psq        ¿?
6203
	 * @return int -1 KO, 1 OK
6204
	 */
6205
	public function generateMultiprices(User $user, $baseprice, $price_type, $price_vat, $npr, $psq)
6206
	{
6207
		global $conf, $db;
6208
6209
		$sql = "SELECT rowid, level, fk_level, var_percent, var_min_percent FROM ".$this->db->prefix()."product_pricerules";
6210
		$query = $this->db->query($sql);
6211
6212
		$rules = array();
6213
6214
		while ($result = $this->db->fetch_object($query)) {
6215
			$rules[$result->level] = $result;
6216
		}
6217
6218
		//Because prices can be based on other level's prices, we temporarily store them
6219
		$prices = array(
6220
			1 => $baseprice
6221
		);
6222
6223
		for ($i = 1; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++) {
6224
			$price = $baseprice;
6225
			$price_min = $baseprice;
6226
6227
			//We have to make sure it does exist and it is > 0
6228
			//First price level only allows changing min_price
6229
			if ($i > 1 && isset($rules[$i]->var_percent) && $rules[$i]->var_percent) {
6230
				$price = $prices[$rules[$i]->fk_level] * (1 + ($rules[$i]->var_percent / 100));
6231
			}
6232
6233
			$prices[$i] = $price;
6234
6235
			//We have to make sure it does exist and it is > 0
6236
			if (isset($rules[$i]->var_min_percent) && $rules[$i]->var_min_percent) {
6237
				$price_min = $price * (1 - ($rules[$i]->var_min_percent / 100));
6238
			}
6239
6240
			//Little check to make sure the price is modified before triggering generation
6241
			$check_amount = (($price == $this->multiprices[$i]) && ($price_min == $this->multiprices_min[$i]));
6242
			$check_type = ($baseprice == $this->multiprices_base_type[$i]);
6243
6244
			if ($check_amount && $check_type) {
6245
				continue;
6246
			}
6247
6248
			if ($this->updatePrice($price, $price_type, $user, $price_vat, $price_min, $i, $npr, $psq, true) < 0) {
6249
				return -1;
6250
			}
6251
		}
6252
6253
		return 1;
6254
	}
6255
6256
	/**
6257
	 * Returns the rights used for this class
6258
	 *
6259
	 * @return Object
6260
	 */
6261
	public function getRights()
6262
	{
6263
		global $user;
6264
6265
		if ($this->isProduct()) {
6266
			return $user->rights->produit;
6267
		} else {
6268
			return $user->rights->service;
6269
		}
6270
	}
6271
6272
	/**
6273
	 *  Load information for tab info
6274
	 *
6275
	 * @param  int $id Id of thirdparty to load
6276
	 * @return void
6277
	 */
6278
	public function info($id)
6279
	{
6280
		$sql = "SELECT p.rowid, p.ref, p.datec as date_creation, p.tms as date_modification,";
6281
		$sql .= " p.fk_user_author, p.fk_user_modif";
6282
		$sql .= " FROM ".$this->db->prefix().$this->table_element." as p";
6283
		$sql .= " WHERE p.rowid = ".((int) $id);
6284
6285
		$result = $this->db->query($sql);
6286
		if ($result) {
6287
			if ($this->db->num_rows($result)) {
6288
				$obj = $this->db->fetch_object($result);
6289
6290
				$this->id = $obj->rowid;
6291
6292
				if ($obj->fk_user_author) {
6293
					$cuser = new User($this->db);
6294
					$cuser->fetch($obj->fk_user_author);
6295
					$this->user_creation = $cuser;
6296
				}
6297
6298
				if ($obj->fk_user_modif) {
6299
					$muser = new User($this->db);
6300
					$muser->fetch($obj->fk_user_modif);
6301
					$this->user_modification = $muser;
6302
				}
6303
6304
				$this->ref = $obj->ref;
6305
				$this->date_creation     = $this->db->jdate($obj->date_creation);
6306
				$this->date_modification = $this->db->jdate($obj->date_modification);
6307
			}
6308
6309
			$this->db->free($result);
6310
		} else {
6311
			dol_print_error($this->db);
6312
		}
6313
	}
6314
6315
6316
	/**
6317
	 * Return the duration in Hours of a service base on duration fields
6318
	 * @return int -1 KO, >= 0 is the duration in hours
6319
	 */
6320
	public function getProductDurationHours()
6321
	{
6322
		global $langs;
6323
6324
		if (empty($this->duration_value)) {
6325
			$this->errors[]='ErrorDurationForServiceNotDefinedCantCalculateHourlyPrice';
6326
			return -1;
6327
		}
6328
6329
		if ($this->duration_unit == 'i') {
6330
			$prodDurationHours = 1. / 60;
6331
		}
6332
		if ($this->duration_unit == 'h') {
6333
			$prodDurationHours = 1.;
6334
		}
6335
		if ($this->duration_unit == 'd') {
6336
			$prodDurationHours = 24.;
6337
		}
6338
		if ($this->duration_unit == 'w') {
6339
			$prodDurationHours = 24. * 7;
6340
		}
6341
		if ($this->duration_unit == 'm') {
6342
			$prodDurationHours = 24. * 30;
6343
		}
6344
		if ($this->duration_unit == 'y') {
6345
			$prodDurationHours = 24. * 365;
6346
		}
6347
		$prodDurationHours *= $this->duration_value;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $prodDurationHours does not seem to be defined for all execution paths leading up to this point.
Loading history...
6348
6349
		return $prodDurationHours;
6350
	}
6351
6352
6353
		/**
6354
	 *	Return clicable link of object (with eventually picto)
6355
	 *
6356
	 *	@param      string	    $option                 Where point the link (0=> main card, 1,2 => shipment, 'nolink'=>No link)
6357
	 *  @return		string								HTML Code for Kanban thumb.
6358
	 */
6359
	public function getKanbanView($option = '')
6360
	{
6361
		global $langs,$conf;
6362
6363
		$return = '<div class="box-flex-item box-flex-grow-zero">';
6364
		$return .= '<div class="info-box info-box-sm">';
6365
		$return .= '<div class="info-box-img">';
6366
		$label = '';
6367
		if ($this->is_photo_available($conf->product->multidir_output[$this->entity])) {
6368
				$label .= $this->show_photos('product', $conf->product->multidir_output[$this->entity]);
6369
				$return .= $label;
6370
		} else {
6371
			if ($this->type == Product::TYPE_PRODUCT) {
6372
				$label .= img_picto('', 'product');
6373
			} elseif ($this->type == Product::TYPE_SERVICE) {
6374
				$label .= img_picto('', 'service');
6375
			}
6376
			$return .= $label;
6377
		}
6378
		$return .= '</div>';
6379
		$return .= '<div class="info-box-content">';
6380
		$return .= '<span class="info-box-ref">'.(method_exists($this, 'getNomUrl') ? $this->getNomUrl() : $this->ref).'</span>';
6381
		if (property_exists($this, 'label')) {
6382
			$return .= '<br><span class="info-box-label opacitymedium">'.$this->label.'</span>';
6383
		}
6384
		if (property_exists($this, 'price') && property_exists($this, 'price_ttc')) {
6385
			if ($this->price_base_type == 'TTC') {
6386
				$return .= '<br><span class="info-box-status amount">'.price($this->price_ttc).' '.$langs->trans("TTC").'</span>';
6387
			} else {
6388
				if ($this->status) {
6389
					$return .= '<br><span class="info-box-status amount">'.price($this->price).' '.$langs->trans("HT").'</span>';
6390
				}
6391
			}
6392
		}
6393
		if (property_exists($this, 'stock_reel')) {
6394
			$return .= '<br><span class="info-box-status opacitymedium">'.$langs->trans('PhysicalStock').' : <span class="bold">'.$this->stock_reel.'</span></span>';
6395
		}
6396
		if (method_exists($this, 'getLibStatut')) {
6397
			$return .='<br><span class="info-box-status margintoponly">'.$this->getLibStatut(5, 1).' '.$this->getLibStatut(5, 0).'</span>';
6398
		}
6399
		$return .= '</div>';
6400
		$return .= '</div>';
6401
		$return .= '</div>';
6402
		return $return;
6403
	}
6404
}
6405
6406
/**
6407
 * Class to manage products or services.
6408
 * Do not use 'Service' as class name since it is already used by APIs.
6409
 */
6410
class ProductService extends Product
6411
{
6412
	public $picto = 'service';
6413
}
6414