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

Product::getTooltipContentArray()   F

Complexity

Conditions 36
Paths > 20000

Size

Total Lines 100
Code Lines 63

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 36
eloc 63
nc 11763361
nop 1
dl 0
loc 100
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/* 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