Completed
Branch develop (45fc57)
by
unknown
27:54
created

Product::replaceThirdparty()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 3
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
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-2013	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-2017	Alexandre Spangaro		<[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-2017	Ferran Marcet			<[email protected]>
16
 * Copyright (C) 2017		Gustavo Novaro
17
 *
18
 * This program is free software; you can redistribute it and/or modify
19
 * it under the terms of the GNU General Public License as published by
20
 * the Free Software Foundation; either version 3 of the License, or
21
 * (at your option) any later version.
22
 *
23
 * This program is distributed in the hope that it will be useful,
24
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
25
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
26
 * GNU General Public License for more details.
27
 *
28
 * You should have received a copy of the GNU General Public License
29
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
30
 */
31
32
/**
33
 *	\file       htdocs/product/class/product.class.php
34
 *	\ingroup    produit
35
 *	\brief      File of class to manage predefined products or services
36
 */
37
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
38
require_once DOL_DOCUMENT_ROOT.'/product/class/productbatch.class.php';
39
require_once DOL_DOCUMENT_ROOT.'/product/stock/class/entrepot.class.php';
40
41
/**
42
 * Class to manage products or services
43
 */
44
class Product extends CommonObject
45
{
46
	public $element='product';
47
	public $table_element='product';
48
	public $fk_element='fk_product';
49
	protected $childtables=array('supplier_proposaldet', 'propaldet','commandedet','facturedet','contratdet','facture_fourn_det','commande_fournisseurdet');    // To test if we can delete object
50
	public $ismultientitymanaged = 1;	// 0=No test on entity, 1=Test with field entity, 2=Test with link by societe
51
52
	/**
53
	 * {@inheritdoc}
54
	 */
55
	protected $table_ref_field = 'ref';
56
57
	public $regeximgext='\.gif|\.jpg|\.jpeg|\.png|\.bmp|\.xpm|\.xbm'; // See also into images.lib.php
58
59
	/*
60
	 * @deprecated
61
	 * @see label
62
	 */
63
	public $libelle;
64
	/**
65
	 * Product label
66
	 * @var string
67
	 */
68
	public $label;
69
70
	/**
71
     	* Product descripion
72
     	* @var string
73
     	*/
74
	public $description;
75
76
	/**
77
	 * Check TYPE constants
78
	 * @var int
79
	 */
80
	public $type = self::TYPE_PRODUCT;
81
82
	/**
83
	 * Selling price
84
	 * @var float
85
	 */
86
	public $price;			// Price net
87
88
	/**
89
	 * Price with tax
90
	 * @var float
91
	 */
92
	public $price_ttc;
93
94
	/**
95
	 * Minimum price net
96
	 * @var float
97
	 */
98
	public $price_min;
99
100
	/**
101
	 * Minimum price with tax
102
	 * @var float
103
	 */
104
	public $price_min_ttc;
105
106
	/*
107
	 * Base price ('TTC' for price including tax or 'HT' for net price)
108
	 * @var float
109
	 */
110
	public $price_base_type;
111
112
	//! Arrays for multiprices
113
	public $multiprices=array();
114
	public $multiprices_ttc=array();
115
	public $multiprices_base_type=array();
116
	public $multiprices_min=array();
117
	public $multiprices_min_ttc=array();
118
	public $multiprices_tva_tx=array();
119
	public $multiprices_recuperableonly=array();
120
121
	//! Price by quantity arrays
122
	public $price_by_qty;
123
	public $prices_by_qty=array();
124
	public $prices_by_qty_id=array();
125
	public $prices_by_qty_list=array();
126
127
	//! Default VAT code for product (link to code into llx_c_tva but without foreign keys)
128
	public $default_vat_code;
129
130
	//! Default VAT rate of product
131
	public $tva_tx;
132
133
	//! French VAT NPR (0 or 1)
134
    public $tva_npr=0;
135
136
	//! Other local taxes
137
	public $localtax1_tx;
138
	public $localtax2_tx;
139
	public $localtax1_type;
140
	public $localtax2_type;
141
142
	/**
143
	 * Stock real
144
	 * @var int
145
	 */
146
	public $stock_reel = 0;
147
148
	/**
149
	 * Stock virtual
150
	 * @var int
151
	 */
152
	public $stock_theorique;
153
154
	/**
155
	 * Cost price
156
	 * @var float
157
	 */
158
	public $cost_price;
159
160
	//! Average price value for product entry into stock (PMP)
161
	public $pmp;
162
163
    	/**
164
	 * Stock alert
165
	 * @var int
166
	 */
167
	public $seuil_stock_alerte;
168
169
	/**
170
	 * Ask for replenishment when $desiredstock < $stock_reel
171
	 */
172
	public $desiredstock;
173
174
	/*
175
	 * Service expiration
176
	 */
177
	public $duration_value;
178
179
	/**
180
	 * Exoiration unit
181
	 */
182
	public $duration_unit;
183
184
	/**
185
	 * Status indicates whether the product is on sale '1' or not '0'
186
	 * @var int
187
	 */
188
	public $status;
189
190
	/**
191
	 * Status indicate whether the product is available for purchase '1' or not '0'
192
	 * @var int
193
	 */
194
	public $status_buy;
195
196
	/**
197
	 * Status indicates whether the product is a finished product '1' or a raw material '0'
198
	 * @var int
199
	 */
200
	public $finished;
201
202
	/**
203
	 * We must manage lot/batch number, sell-by date and so on : '1':yes '0':no
204
	 * @var int
205
	 */
206
	public $status_batch;
207
208
	/**
209
	 * Customs code
210
	 * @var
211
	 */
212
	public $customcode;
213
214
	/**
215
	 * Product URL
216
	 * @var string
217
	 */
218
	public $url;
219
220
	//! Unites de mesure
221
	public $weight;
222
	public $weight_units;
223
	public $length;
224
	public $length_units;
225
	public $surface;
226
	public $surface_units;
227
	public $volume;
228
	public $volume_units;
229
230
	public $accountancy_code_buy;
231
	public $accountancy_code_buy_intra;
232
	public $accountancy_code_buy_export;
233
	public $accountancy_code_sell;
234
235
	/**
236
	 * Main barcode
237
	 * barcode value
238
	 * @var
239
	 */
240
	public $barcode;
241
242
	/**
243
	 * Additional barcodes (Some products have different barcodes according to the country of origin of manufacture)
244
	 * @var array
245
	 */
246
	public $barcodes_extra=array();
247
248
	public $stats_propale=array();
249
	public $stats_commande=array();
250
	public $stats_contrat=array();
251
	public $stats_facture=array();
252
    public $stats_commande_fournisseur=array();
253
254
	public $multilangs=array();
255
256
	//! Taille de l'image
257
	public $imgWidth;
258
	public $imgHeight;
259
260
	public $date_creation;
261
	public $date_modification;
262
263
	//! Id du fournisseur
264
	public $product_fourn_id;
265
266
	//! Product ID already linked to a reference supplier
267
	public $product_id_already_linked;
268
269
	public $nbphoto;
270
271
	//! Contains detail of stock of product into each warehouse
272
	public $stock_warehouse=array();
273
274
	public $oldcopy;
275
276
    public $fk_price_expression;
277
278
	/**
279
	 * @deprecated
280
	 * @see fourn_pu
281
	 */
282
	public $buyprice;
283
	public $fourn_pu;
284
285
	public $fourn_price_base_type;
286
287
	/**
288
	 * @deprecated
289
	 * @see ref_supplier
290
	 */
291
	public $ref_fourn;
292
	public $ref_supplier;
293
294
	/**
295
	 * Unit code ('km', 'm', 'l', 'p', ...)
296
	 * @var string
297
	 */
298
	public $fk_unit;
299
300
	/**
301
	 * Price is generated using multiprice rules
302
	 * @var int
303
	 */
304
	public $price_autogen = 0;
305
306
307
	/**
308
	 * Regular product
309
	 */
310
	const TYPE_PRODUCT = 0;
311
	/**
312
	 * Service
313
	 */
314
	const TYPE_SERVICE = 1;
315
	/**
316
	 * Advanced feature: assembly kit
317
	 */
318
	const TYPE_ASSEMBLYKIT = 2;
319
	/**
320
	 * Advanced feature: stock kit
321
	 */
322
	const TYPE_STOCKKIT = 3;
323
324
325
	/**
326
	 *  Constructor
327
	 *
328
	 *  @param      DoliDB		$db      Database handler
329
	 */
330
	function __construct($db)
331
	{
332
		global $langs;
333
334
		$this->db = $db;
335
		$this->status = 0;
336
		$this->status_buy = 0;
337
		$this->nbphoto = 0;
338
		$this->stock_reel = 0;
339
		$this->seuil_stock_alerte = 0;
340
		$this->desiredstock = 0;
341
		$this->canvas = '';
342
		$this->status_batch=0;
343
	}
344
345
	/**
346
	 *    Check that ref and label are ok
347
	 *
348
	 *    @return     int         >1 if OK, <=0 if KO
349
	 */
350
	function check()
351
	{
352
		$this->ref = dol_sanitizeFileName(stripslashes($this->ref));
353
354
		$err = 0;
355
		if (dol_strlen(trim($this->ref)) == 0)
356
		$err++;
357
358
		if (dol_strlen(trim($this->label)) == 0)
359
		$err++;
360
361
		if ($err > 0)
362
		{
363
			return 0;
364
		}
365
		else
366
		{
367
			return 1;
368
		}
369
	}
370
371
	/**
372
	 *	Insert product into database
373
	 *
374
	 *	@param	User	$user     		User making insert
375
	 *  @param	int		$notrigger		Disable triggers
376
	 *	@return int			     		Id of product/service if OK, < 0 if KO
377
	 */
378
	function create($user,$notrigger=0)
379
	{
380
		global $conf, $langs;
381
382
        	$error=0;
383
384
		// Clean parameters
385
		$this->ref = dol_string_nospecial(trim($this->ref));
386
		$this->label = trim($this->label);
387
		$this->price_ttc=price2num($this->price_ttc);
1 ignored issue
show
Documentation Bug introduced by
It seems like price2num($this->price_ttc) can also be of type string. However, the property $price_ttc is declared as type double. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
391
		if (empty($this->tva_tx))    	$this->tva_tx = 0;
392
		if (empty($this->tva_npr))    	$this->tva_npr = 0;
393
		//Local taxes
394
		if (empty($this->localtax1_tx)) $this->localtax1_tx = 0;
395
		if (empty($this->localtax2_tx)) $this->localtax2_tx = 0;
396
		if (empty($this->localtax1_type)) $this->localtax1_type = '0';
397
		if (empty($this->localtax2_type)) $this->localtax2_type = '0';
398
399
		if (empty($this->price))     	$this->price = 0;
400
		if (empty($this->price_min)) 	$this->price_min = 0;
401
402
		// Price by quantity
403
		if (empty($this->price_by_qty)) 	$this->price_by_qty = 0;
404
405
		if (empty($this->status))    	$this->status = 0;
406
		if (empty($this->status_buy))   $this->status_buy = 0;
407
408
		$price_ht=0;
409
		$price_ttc=0;
410
		$price_min_ht=0;
411
		$price_min_ttc=0;
412
413
		//
414
		if ($this->price_base_type == 'TTC' && $this->price_ttc > 0)
415
		{
416
			$price_ttc = price2num($this->price_ttc,'MU');
417
			$price_ht = price2num($this->price_ttc / (1 + ($this->tva_tx / 100)),'MU');
418
		}
419
420
		//
421
		if ($this->price_base_type != 'TTC' && $this->price > 0)
422
		{
423
			$price_ht = price2num($this->price,'MU');
424
			$price_ttc = price2num($this->price * (1 + ($this->tva_tx / 100)),'MU');
425
		}
426
427
		//
428
		if (($this->price_min_ttc > 0) && ($this->price_base_type == 'TTC'))
429
		{
430
			$price_min_ttc = price2num($this->price_min_ttc,'MU');
431
			$price_min_ht = price2num($this->price_min_ttc / (1 + ($this->tva_tx / 100)),'MU');
432
		}
433
434
		//
435
		if (($this->price_min > 0) && ($this->price_base_type != 'TTC'))
436
		{
437
			$price_min_ht = price2num($this->price_min,'MU');
438
			$price_min_ttc = price2num($this->price_min * (1 + ($this->tva_tx / 100)),'MU');
439
		}
440
441
		$this->accountancy_code_buy = trim($this->accountancy_code_buy);
442
		$this->accountancy_code_sell= trim($this->accountancy_code_sell);
443
		$this->accountancy_code_sell_intra= trim($this->accountancy_code_sell_intra);
444
		$this->accountancy_code_sell_export= trim($this->accountancy_code_sell_export);
445
446
		// Barcode value
447
		$this->barcode=trim($this->barcode);
448
449
		// Check parameters
450
		if (empty($this->label))
451
		{
452
			$this->error='ErrorMandatoryParametersNotProvided';
453
			return -1;
454
		}
455
456
		if (empty($this->ref))
457
		{
458
			// Load object modCodeProduct
459
			$module=(! empty($conf->global->PRODUCT_CODEPRODUCT_ADDON)?$conf->global->PRODUCT_CODEPRODUCT_ADDON:'mod_codeproduct_leopard');
460
			if ($module != 'mod_codeproduct_leopard')	// Do not load module file for leopard
461
			{
462
				if (substr($module, 0, 16) == 'mod_codeproduct_' && substr($module, -3) == 'php')
463
				{
464
					$module = substr($module, 0, dol_strlen($module)-4);
465
				}
466
				dol_include_once('/core/modules/product/'.$module.'.php');
467
				$modCodeProduct = new $module;
468
				if (! empty($modCodeProduct->code_auto))
469
				{
470
					$this->ref = $modCodeProduct->getNextValue($this,$this->type);
471
				}
472
				unset($modCodeProduct);
473
			}
474
475
			if (empty($this->ref))
476
			{
477
				$this->error='ProductModuleNotSetupForAutoRef';
478
				return -2;
479
			}
480
		}
481
482
		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);
483
484
		$now=dol_now();
485
486
		$this->db->begin();
487
488
		// For automatic creation during create action (not used by Dolibarr GUI, can be used by scripts)
489
		if ($this->barcode == -1) $this->barcode = $this->get_barcode($this,$this->barcode_type_code);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $this->barcode is correct as $this->get_barcode($this...his->barcode_type_code) (which targets Product::get_barcode()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

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

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

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

Loading history...
490
491
		// Check more parameters
492
		// If error, this->errors[] is filled
493
		$result = $this->verify();
494
495
		if ($result >= 0)
496
		{
497
			$sql = "SELECT count(*) as nb";
498
			$sql.= " FROM ".MAIN_DB_PREFIX."product";
499
			$sql.= " WHERE entity IN (".getEntity('product').")";
500
			$sql.= " AND ref = '" .$this->db->escape($this->ref)."'";
501
502
			$result = $this->db->query($sql);
503
			if ($result)
504
			{
505
				$obj = $this->db->fetch_object($result);
506
				if ($obj->nb == 0)
507
				{
508
					// Produit non deja existant
509
					$sql = "INSERT INTO ".MAIN_DB_PREFIX."product (";
510
					$sql.= "datec";
511
					$sql.= ", entity";
512
					$sql.= ", ref";
513
					$sql.= ", ref_ext";
514
					$sql.= ", price_min";
515
					$sql.= ", price_min_ttc";
516
					$sql.= ", label";
517
					$sql.= ", fk_user_author";
518
					$sql.= ", fk_product_type";
519
					$sql.= ", price";
520
					$sql.= ", price_ttc";
521
					$sql.= ", price_base_type";
522
					$sql.= ", tobuy";
523
					$sql.= ", tosell";
524
					$sql.= ", accountancy_code_buy";
525
					$sql.= ", accountancy_code_sell";
526
					$sql.= ", accountancy_code_sell_intra";
527
					$sql.= ", accountancy_code_sell_export";
528
					$sql.= ", canvas";
529
					$sql.= ", finished";
530
					$sql.= ", tobatch";
531
					$sql.= ", fk_unit";
532
					$sql.= ") VALUES (";
533
					$sql.= "'".$this->db->idate($now)."'";
534
					$sql.= ", ".$conf->entity;
535
					$sql.= ", '".$this->db->escape($this->ref)."'";
536
					$sql.= ", ".(! empty($this->ref_ext)?"'".$this->db->escape($this->ref_ext)."'":"null");
537
					$sql.= ", ".price2num($price_min_ht);
538
					$sql.= ", ".price2num($price_min_ttc);
539
					$sql.= ", ".(! empty($this->label)?"'".$this->db->escape($this->label)."'":"null");
540
					$sql.= ", ".$user->id;
541
					$sql.= ", ".$this->type;
542
					$sql.= ", ".price2num($price_ht);
543
					$sql.= ", ".price2num($price_ttc);
544
					$sql.= ", '".$this->db->escape($this->price_base_type)."'";
545
					$sql.= ", ".$this->status;
546
					$sql.= ", ".$this->status_buy;
547
					$sql.= ", '".$this->db->escape($this->accountancy_code_buy)."'";
548
					$sql.= ", '".$this->db->escape($this->accountancy_code_sell)."'";
549
					$sql.= ", '".$this->db->escape($this->accountancy_code_sell_intra)."'";
550
					$sql.= ", '".$this->db->escape($this->accountancy_code_sell_export)."'";
551
					$sql.= ", '".$this->db->escape($this->canvas)."'";
552
					$sql.= ", ".((! isset($this->finished) || $this->finished < 0 || $this->finished == '') ? 'null' : (int) $this->finished);
553
					$sql.= ", ".((empty($this->status_batch) || $this->status_batch < 0)? '0':$this->status_batch);
554
					$sql.= ", ".(!$this->fk_unit ? 'NULL' : $this->fk_unit);
555
					$sql.= ")";
556
557
					dol_syslog(get_class($this)."::Create", LOG_DEBUG);
558
					$result = $this->db->query($sql);
559
					if ( $result )
560
					{
561
						$id = $this->db->last_insert_id(MAIN_DB_PREFIX."product");
562
563
						if ($id > 0)
564
						{
565
							$this->id				= $id;
566
							$this->price			= $price_ht;
0 ignored issues
show
Documentation Bug introduced by
It seems like $price_ht can also be of type string or integer. However, the property $price is declared as type double. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
570
571
							$result = $this->_log_price($user);
572
							if ($result > 0)
573
							{
574
								if ($this->update($id, $user, true, 'add') <= 0)
575
								{
576
									$error++;
577
								}
578
							}
579
							else
580
							{
581
								$error++;
582
								$this->error=$this->db->lasterror();
583
							}
584
						}
585
						else
586
						{
587
							$error++;
588
							$this->error='ErrorFailedToGetInsertedId';
589
						}
590
					}
591
					else
592
					{
593
						$error++;
594
						$this->error=$this->db->lasterror();
595
					}
596
				}
597
				else
598
				{
599
					// Product already exists with this ref
600
					$langs->load("products");
601
					$error++;
602
					$this->error = "ErrorProductAlreadyExists";
603
				}
604
			}
605
			else
606
			{
607
				$error++;
608
				$this->error=$this->db->lasterror();
609
			}
610
611
			if (! $error && ! $notrigger)
612
			{
613
				// Call trigger
614
				$result=$this->call_trigger('PRODUCT_CREATE',$user);
615
				if ($result < 0) { $error++; }
616
				// End call triggers
617
			}
618
619
			if (! $error)
620
			{
621
				$this->db->commit();
622
				return $this->id;
623
			}
624
			else
625
			{
626
				$this->db->rollback();
627
				return -$error;
628
			}
629
        }
630
        else
631
       {
632
            $this->db->rollback();
633
            dol_syslog(get_class($this)."::Create fails verify ".join(',',$this->errors), LOG_WARNING);
634
            return -3;
635
        }
636
637
	}
638
639
640
    /**
641
     *    Check properties of product are ok (like name, barcode, ...).
642
     *    All properties must be already loaded on object (this->barcode, this->barcode_type_code, ...).
643
     *
644
     *    @return     int		0 if OK, <0 if KO
645
     */
646
    function verify()
647
    {
648
        $this->errors=array();
649
650
        $result = 0;
651
        $this->ref = trim($this->ref);
652
653
        if (! $this->ref)
654
        {
655
            $this->errors[] = 'ErrorBadRef';
656
            $result = -2;
657
        }
658
659
        $rescode = $this->check_barcode($this->barcode,$this->barcode_type_code);
660
        if ($rescode <> 0)
661
        {
662
        	if ($rescode == -1)
663
        	{
664
        		$this->errors[] = 'ErrorBadBarCodeSyntax';
665
        	}
666
        	if ($rescode == -2)
667
        	{
668
        		$this->errors[] = 'ErrorBarCodeRequired';
669
        	}
670
        	if ($rescode == -3)
671
        	{
672
        		$this->errors[] = 'ErrorBarCodeAlreadyUsed';
673
        	}
674
        	$result = -3;
675
        }
676
677
        return $result;
678
    }
679
680
    /**
681
     *  Check barcode
682
     *
683
     *	@param	string	$valuetotest	Value to test
684
     *  @param	string	$typefortest	Type of barcode (ISBN, EAN, ...)
685
     *  @return int						0 if OK
686
     * 									-1 ErrorBadBarCodeSyntax
687
     * 									-2 ErrorBarCodeRequired
688
     * 									-3 ErrorBarCodeAlreadyUsed
689
     */
690
    function check_barcode($valuetotest,$typefortest)
691
    {
692
        global $conf;
693
        if (! empty($conf->barcode->enabled) && ! empty($conf->global->BARCODE_PRODUCT_ADDON_NUM))
694
        {
695
        	$module=strtolower($conf->global->BARCODE_PRODUCT_ADDON_NUM);
696
697
            $dirsociete=array_merge(array('/core/modules/barcode/'),$conf->modules_parts['barcode']);
698
            foreach ($dirsociete as $dirroot)
699
            {
700
                $res=dol_include_once($dirroot.$module.'.php');
701
                if ($res) break;
702
            }
703
704
            $mod = new $module();
705
706
            dol_syslog(get_class($this)."::check_barcode value=".$valuetotest." type=".$typefortest." module=".$module);
707
            $result = $mod->verif($this->db, $valuetotest, $this, 0, $typefortest);
708
            return $result;
709
        }
710
        else
711
		{
712
            return 0;
713
        }
714
    }
715
716
	/**
717
	 *	Update a record into database.
718
	 *  If batch flag is set to on, we create records into llx_product_batch
719
	 *
720
	 *	@param	int		$id         Id of product
721
	 *	@param  User	$user       Object user making update
722
	 *	@param	int		$notrigger	Disable triggers
723
	 *	@param	string	$action		Current action for hookmanager ('add' or 'update')
724
	 *	@return int         		1 if OK, -1 if ref already exists, -2 if other error
725
	 */
726
	function update($id, $user, $notrigger=false, $action='update')
727
	{
728
		global $langs, $conf, $hookmanager;
729
730
		$error=0;
731
732
		// Check parameters
733
		if (! $this->label) $this->label = 'MISSING LABEL';
734
735
		// Clean parameters
736
		$this->ref = dol_string_nospecial(trim($this->ref));
737
		$this->label = trim($this->label);
738
		$this->description = trim($this->description);
739
		$this->note = (isset($this->note) ? trim($this->note) : null);
740
		$this->weight = price2num($this->weight);
741
		$this->weight_units = trim($this->weight_units);
742
		$this->length = price2num($this->length);
743
		$this->length_units = trim($this->length_units);
744
		$this->width = price2num($this->width);
745
		$this->width_units = trim($this->width_units);
746
		$this->height = price2num($this->height);
747
		$this->height_units = trim($this->height_units);
748
		// set unit not defined
749
		if ($this->length_units) $this->width_units = $this->length_units;    // Not used yet
750
		if ($this->length_units) $this->height_units = $this->length_units;    // Not used yet
751
		// Automated compute surface and volume if not filled
752
		if (empty($this->surface) && !empty($this->length) && !empty($this->width) && $this->length_units == $this->width_units)
753
		{
754
			$this->surface = $this->length * $this->width;
755
			$this->surface_units = $this->length_units + $this->width_units;
756
		}
757
		if (empty($this->volume) && !empty($this->surface_units) && !empty($this->height) && $this->length_units == $this->height_units)
758
		{
759
			$this->volume =  $this->surface * $this->height;
760
			$this->volume_units = $this->surface_units + $this->height_units;
761
		}
762
763
		$this->surface = price2num($this->surface);
764
		$this->surface_units = trim($this->surface_units);
765
		$this->volume = price2num($this->volume);
766
		$this->volume_units = trim($this->volume_units);
767
		if (empty($this->tva_tx))    			$this->tva_tx = 0;
768
		if (empty($this->tva_npr))    			$this->tva_npr = 0;
769
		if (empty($this->localtax1_tx))			$this->localtax1_tx = 0;
770
		if (empty($this->localtax2_tx))			$this->localtax2_tx = 0;
771
		if (empty($this->localtax1_type))		$this->localtax1_type = '0';
772
		if (empty($this->localtax2_type))		$this->localtax2_type = '0';
773
		if (empty($this->status))				$this->status = 0;
774
		if (empty($this->status_buy))			$this->status_buy = 0;
775
776
        if (empty($this->country_id))           $this->country_id = 0;
777
778
        // Barcode value
779
        $this->barcode=trim($this->barcode);
780
781
		$this->accountancy_code_buy = trim($this->accountancy_code_buy);
782
		$this->accountancy_code_sell= trim($this->accountancy_code_sell);
783
		$this->accountancy_code_sell_intra= trim($this->accountancy_code_sell_intra);
784
		$this->accountancy_code_sell_export= trim($this->accountancy_code_sell_export);
785
786
787
        $this->db->begin();
788
789
        // Check name is required and codes are ok or unique.
790
        // If error, this->errors[] is filled
791
        if ($action != 'add')
792
        {
793
        	$result = $this->verify();	// We don't check when update called during a create because verify was already done
794
        }
795
796
        if ($result >= 0)
0 ignored issues
show
Bug introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
797
        {
798
            if (empty($this->oldcopy))
799
            {
800
                $org=new self($this->db);
801
                $org->fetch($this->id);
802
                $this->oldcopy=$org;
803
            }
804
805
            // Test if batch management is activated on existing product
806
            // If yes, we create missing entries into product_batch
807
            if ($this->hasbatch() && !$this->oldcopy->hasbatch())
808
            {
809
                //$valueforundefinedlot = 'Undefined';  // In previous version, 39 and lower
810
                $valueforundefinedlot = '000000';
811
812
                dol_syslog("Flag batch of product id=".$this->id." is set to ON, so we will create missing records into product_batch");
813
814
                $this->load_stock();
815
                foreach ($this->stock_warehouse as $idW => $ObjW)   // For each warehouse where we have stocks defined for this product (for each lines in product_stock)
816
                {
817
                    $qty_batch = 0;
818
                    foreach ($ObjW->detail_batch as $detail)    // Each lines of detail in product_batch of the current $ObjW = product_stock
819
                    {
820
                        if ($detail->batch == $valueforundefinedlot || $detail->batch == 'Undefined')
821
                        {
822
                            // We discard this line, we will create it later
823
                            $sqlclean="DELETE FROM ".MAIN_DB_PREFIX."product_batch WHERE batch in('Undefined', '".$valueforundefinedlot."') AND fk_product_stock = ".$ObjW->id;
824
                            $result = $this->db->query($sqlclean);
825
                            if (! $result)
826
                            {
827
                                dol_print_error($this->db);
828
                                exit;
829
                            }
830
                            continue;
831
                        }
832
833
                        $qty_batch += $detail->qty;
834
                    }
835
                    // Quantities in batch details are not same as stock quantity,
836
                    // so we add a default batch record to complete and get same qty in parent and child table
837
                    if ($ObjW->real <> $qty_batch)
838
                    {
839
                        $ObjBatch = new Productbatch($this->db);
840
                        $ObjBatch->batch = $valueforundefinedlot;
841
                        $ObjBatch->qty = ($ObjW->real - $qty_batch);
842
                        $ObjBatch->fk_product_stock = $ObjW->id;
843
844
                        if ($ObjBatch->create($user,1) < 0)
845
                        {
846
                            $error++;
847
                            $this->errors=$ObjBatch->errors;
848
                        }
849
                    }
850
                }
851
            }
852
853
	        // For automatic creation
854
	        if ($this->barcode == -1) $this->barcode = $this->get_barcode($this,$this->barcode_type_code);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $this->barcode is correct as $this->get_barcode($this...his->barcode_type_code) (which targets Product::get_barcode()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

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

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

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

Loading history...
855
856
			$sql = "UPDATE ".MAIN_DB_PREFIX."product";
857
			$sql.= " SET label = '" . $this->db->escape($this->label) ."'";
858
			$sql.= ", ref = '" . $this->db->escape($this->ref) ."'";
859
			$sql.= ", ref_ext = ".(! empty($this->ref_ext)?"'".$this->db->escape($this->ref_ext)."'":"null");
860
			$sql.= ", default_vat_code = ".($this->default_vat_code ? "'".$this->db->escape($this->default_vat_code)."'" : "null");
861
			$sql.= ", tva_tx = " . $this->tva_tx;
862
			$sql.= ", recuperableonly = " . $this->tva_npr;
863
			$sql.= ", localtax1_tx = " . $this->localtax1_tx;
864
			$sql.= ", localtax2_tx = " . $this->localtax2_tx;
865
			$sql.= ", localtax1_type = " . ($this->localtax1_type!=''?"'".$this->db->escape($this->localtax1_type)."'":"'0'");
866
			$sql.= ", localtax2_type = " . ($this->localtax2_type!=''?"'".$this->db->escape($this->localtax2_type)."'":"'0'");
867
868
			$sql.= ", barcode = ". (empty($this->barcode)?"null":"'".$this->db->escape($this->barcode)."'");
869
			$sql.= ", fk_barcode_type = ". (empty($this->barcode_type)?"null":$this->db->escape($this->barcode_type));
870
871
			$sql.= ", tosell = " . $this->status;
872
			$sql.= ", tobuy = " . $this->status_buy;
873
			$sql.= ", tobatch = " . ((empty($this->status_batch) || $this->status_batch < 0) ? '0' : $this->status_batch);
874
			$sql.= ", finished = " . ((! isset($this->finished) || $this->finished < 0) ? "null" : (int) $this->finished);
875
			$sql.= ", weight = " . ($this->weight!='' ? "'".$this->db->escape($this->weight)."'" : 'null');
876
			$sql.= ", weight_units = " . ($this->weight_units!='' ? "'".$this->db->escape($this->weight_units)."'": 'null');
877
			$sql.= ", length = " . ($this->length!='' ? "'".$this->db->escape($this->length)."'" : 'null');
878
			$sql.= ", length_units = " . ($this->length_units!='' ? "'".$this->db->escape($this->length_units)."'" : 'null');
879
			$sql.= ", width= " . ($this->width!='' ? "'".$this->db->escape($this->width)."'" : 'null');
880
			$sql.= ", width_units = " . ($this->width_units!='' ? "'".$this->db->escape($this->width_units)."'" : 'null');
881
			$sql.= ", height = " . ($this->height!='' ? "'".$this->db->escape($this->height)."'" : 'null');
882
			$sql.= ", height_units = " . ($this->height_units!='' ? "'".$this->db->escape($this->height_units)."'" : 'null');
883
			$sql.= ", surface = " . ($this->surface!='' ? "'".$this->db->escape($this->surface)."'" : 'null');
884
			$sql.= ", surface_units = " . ($this->surface_units!='' ? "'".$this->db->escape($this->surface_units)."'" : 'null');
885
			$sql.= ", volume = " . ($this->volume!='' ? "'".$this->db->escape($this->volume)."'" : 'null');
886
			$sql.= ", volume_units = " . ($this->volume_units!='' ? "'".$this->db->escape($this->volume_units)."'" : 'null');
887
			$sql.= ", seuil_stock_alerte = " . ((isset($this->seuil_stock_alerte) && $this->seuil_stock_alerte != '') ? "'".$this->db->escape($this->seuil_stock_alerte)."'" : "null");
888
			$sql.= ", description = '" . $this->db->escape($this->description) ."'";
889
			$sql.= ", url = " . ($this->url?"'".$this->db->escape($this->url)."'":'null');
890
			$sql.= ", customcode = '" .        $this->db->escape($this->customcode) ."'";
891
	        $sql.= ", fk_country = " . ($this->country_id > 0 ? $this->country_id : 'null');
892
	        $sql.= ", note = ".(isset($this->note) ? "'" .$this->db->escape($this->note)."'" : 'null');
893
			$sql.= ", duration = '" . $this->db->escape($this->duration_value . $this->duration_unit) ."'";
894
			$sql.= ", accountancy_code_buy = '" . $this->db->escape($this->accountancy_code_buy)."'";
895
			$sql.= ", accountancy_code_sell= '" . $this->db->escape($this->accountancy_code_sell)."'";
896
			$sql.= ", accountancy_code_sell_intra= '" . $this->db->escape($this->accountancy_code_sell_intra)."'";
897
			$sql.= ", accountancy_code_sell_export= '" . $this->db->escape($this->accountancy_code_sell_export)."'";
898
			$sql.= ", desiredstock = " . ((isset($this->desiredstock) && $this->desiredstock != '') ? $this->desiredstock : "null");
899
			$sql.= ", cost_price = " . ($this->cost_price != '' ? $this->db->escape($this->cost_price) : 'null');
900
	        $sql.= ", fk_unit= " . (!$this->fk_unit ? 'NULL' : $this->fk_unit);
901
	        $sql.= ", price_autogen = " . (!$this->price_autogen ? 0 : 1);
902
			$sql.= ", fk_price_expression = ".($this->fk_price_expression != 0 ? $this->fk_price_expression : 'NULL');
903
			$sql.= ", fk_user_modif = ".($user->id > 0 ? $user->id : 'NULL');
904
			// stock field is not here because it is a denormalized value from product_stock.
905
			$sql.= " WHERE rowid = " . $id;
906
907
			dol_syslog(get_class($this)."::update", LOG_DEBUG);
908
909
			$resql=$this->db->query($sql);
910
			if ($resql)
911
			{
912
				$this->id = $id;
913
914
				// Multilangs
915
				if (! empty($conf->global->MAIN_MULTILANGS))
916
				{
917
					if ( $this->setMultiLangs($user) < 0)
918
					{
919
						$this->error=$langs->trans("Error")." : ".$this->db->error()." - ".$sql;
920
						return -2;
921
					}
922
				}
923
924
				$action='update';
925
926
				// Actions on extra fields (by external module or standard code)
927
				$hookmanager->initHooks(array('productdao'));
928
				$parameters=array('id'=>$this->id);
929
				$reshook=$hookmanager->executeHooks('insertExtraFields',$parameters,$this,$action);    // Note that $action and $object may have been modified by some hooks
930
				if (empty($reshook))
931
				{
932
					if (empty($conf->global->MAIN_EXTRAFIELDS_DISABLED)) // For avoid conflicts if trigger used
933
					{
934
						$result=$this->insertExtraFields();
935
						if ($result < 0)
936
						{
937
							$error++;
938
						}
939
					}
940
				}
941
				else if ($reshook < 0) $error++;
942
943
				if (! $error && ! $notrigger)
944
				{
945
                    // Call trigger
946
                    $result=$this->call_trigger('PRODUCT_MODIFY',$user);
947
                    if ($result < 0) { $error++; }
948
                    // End call triggers
949
				}
950
951
				if (! $error && (is_object($this->oldcopy) && $this->oldcopy->ref !== $this->ref))
952
				{
953
					// We remove directory
954
					if ($conf->product->dir_output)
955
					{
956
						$olddir = $conf->product->dir_output . "/" . dol_sanitizeFileName($this->oldcopy->ref);
957
						$newdir = $conf->product->dir_output . "/" . dol_sanitizeFileName($this->ref);
958
						if (file_exists($olddir))
959
						{
960
							//include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
961
							//$res = dol_move($olddir, $newdir);
962
							// do not use dol_move with directory
963
							$res = @rename($olddir, $newdir);
964
							if (! $res)
965
							{
966
							    $langs->load("errors");
967
								$this->error=$langs->trans('ErrorFailToRenameDir',$olddir,$newdir);
968
								$error++;
969
							}
970
						}
971
					}
972
				}
973
974
				if (! $error)
975
				{
976
					if ($conf->variants->enabled) {
977
978
						require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
979
980
						$comb = new ProductCombination($this->db);
981
982
						foreach ($comb->fetchAllByFkProductParent($this->id) as $currcomb) {
983
							$currcomb->updateProperties($this);
984
						}
985
					}
986
987
					$this->db->commit();
988
					return 1;
989
				}
990
				else
991
				{
992
					$this->db->rollback();
993
					return -$error;
994
				}
995
			}
996
			else
997
			{
998
				if ($this->db->errno() == 'DB_ERROR_RECORD_ALREADY_EXISTS')
999
				{
1000
					if (empty($conf->barcode->enabled)) $this->error=$langs->trans("Error")." : ".$langs->trans("ErrorProductAlreadyExists",$this->ref);
1001
					else $this->error=$langs->trans("Error")." : ".$langs->trans("ErrorProductBarCodeAlreadyExists",$this->barcode);
1002
					$this->errors[]=$this->error;
1003
					$this->db->rollback();
1004
					return -1;
1005
				}
1006
				else
1007
				{
1008
					$this->error=$langs->trans("Error")." : ".$this->db->error()." - ".$sql;
1009
					$this->errors[]=$this->error;
1010
					$this->db->rollback();
1011
					return -2;
1012
				}
1013
			}
1014
        }
1015
        else
1016
       {
1017
            $this->db->rollback();
1018
            dol_syslog(get_class($this)."::Update fails verify ".join(',',$this->errors), LOG_WARNING);
1019
            return -3;
1020
        }
1021
	}
1022
1023
	/**
1024
	 *  Delete a product from database (if not used)
1025
	 *
1026
	 *	@param      User	$user       Product id (usage of this is deprecated, delete should be called without parameters on a fetched object)
1027
	 *  @param      int     $notrigger  Do not execute trigger
1028
	 * 	@return		int					< 0 if KO, 0 = Not possible, > 0 if OK
1029
	 */
1030
	function delete(User $user, $notrigger=0)
1031
	{
1032
		// Deprecation warning
1033
		if ($id > 0) {
0 ignored issues
show
Bug introduced by
The variable $id seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
1034
			dol_syslog(__METHOD__ . " with parameter is deprecated", LOG_WARNING);
1035
		}
1036
1037
		global $conf, $langs;
1038
		require_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
1039
1040
		$error=0;
1041
1042
		// Clean parameters
1043
		if (empty($id)) $id=$this->id;
0 ignored issues
show
Bug introduced by
The variable $id seems only to be defined at a later point. As such the call to empty() seems to always evaluate to true.

This check marks calls to isset(...) or empty(...) that are found before the variable itself is defined. These will always have the same result.

This is likely the result of code being shifted around. Consider removing these calls.

Loading history...
1044
		else $this->fetch($id);
1045
1046
		// Check parameters
1047
		if (empty($id))
1048
		{
1049
			$this->error = "Object must be fetched before calling delete";
1050
			return -1;
1051
		}
1052
		if (($this->type == Product::TYPE_PRODUCT && empty($user->rights->produit->supprimer)) || ($this->type == Product::TYPE_SERVICE && empty($user->rights->service->supprimer)))
1053
		{
1054
			$this->error = "ErrorForbidden";
1055
			return 0;
1056
		}
1057
1058
		$objectisused = $this->isObjectUsed($id);
1059
		if (empty($objectisused))
1060
		{
1061
			$this->db->begin();
1062
1063
			if (! $error && empty($notrigger))
1064
			{
1065
                // Call trigger
1066
                $result=$this->call_trigger('PRODUCT_DELETE',$user);
1067
                if ($result < 0) { $error++; }
1068
                // End call triggers
1069
			}
1070
1071
			// Delete from product_batch on product delete
1072
			if (! $error)
1073
			{
1074
				$sql = "DELETE FROM ".MAIN_DB_PREFIX.'product_batch';
1075
				$sql.= " WHERE fk_product_stock IN (";
1076
				$sql.= "SELECT rowid FROM ".MAIN_DB_PREFIX.'product_stock';
1077
				$sql.= " WHERE fk_product = ".$id.")";
1078
				dol_syslog(get_class($this).'::delete', LOG_DEBUG);
1079
				$result = $this->db->query($sql);
1080
				if (! $result)
1081
				{
1082
					$error++;
1083
					$this->errors[] = $this->db->lasterror();
1084
				}
1085
			}
1086
1087
   			// Delete all child tables
1088
			if (! $error)
1089
			{
1090
				$elements = array('product_fournisseur_price','product_price','product_lang','categorie_product','product_stock','product_customer_price','product_lot');  // product_batch is done before
1091
    			foreach($elements as $table)
1092
    			{
1093
    				if (! $error)
1094
    				{
1095
    					$sql = "DELETE FROM ".MAIN_DB_PREFIX.$table;
1096
    					$sql.= " WHERE fk_product = ".$id;
1097
    					dol_syslog(get_class($this).'::delete', LOG_DEBUG);
1098
    					$result = $this->db->query($sql);
1099
    					if (! $result)
1100
    					{
1101
    						$error++;
1102
    						$this->errors[] = $this->db->lasterror();
1103
    					}
1104
    				}
1105
    			}
1106
			}
1107
1108
			if (!$error) {
1109
1110
				require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
1111
				require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination2ValuePair.class.php';
1112
1113
				//If it is a parent product, then we remove the association with child products
1114
				$prodcomb = new ProductCombination($this->db);
1115
1116
				if ($prodcomb->deleteByFkProductParent($id) < 0) {
1117
					$error++;
1118
					$this->errors[] = 'Error deleting combinations';
1119
				}
1120
1121
				//We also check if it is a child product
1122
				if (!$error && ($prodcomb->fetchByFkProductChild($id) > 0) && ($prodcomb->delete() < 0)) {
0 ignored issues
show
Bug introduced by
The call to delete() misses a required argument $user.

This check looks for function calls that miss required arguments.

Loading history...
1123
					$error++;
1124
					$this->errors[] = 'Error deleting child combination';
1125
				}
1126
			}
1127
1128
			// Delete product
1129
			if (! $error)
1130
			{
1131
				$sqlz = "DELETE FROM ".MAIN_DB_PREFIX."product";
1132
				$sqlz.= " WHERE rowid = ".$id;
1133
				dol_syslog(get_class($this).'::delete', LOG_DEBUG);
1134
				$resultz = $this->db->query($sqlz);
1135
				if ( ! $resultz )
1136
				{
1137
					$error++;
1138
					$this->errors[] = $this->db->lasterror();
1139
				}
1140
			}
1141
1142
			if (! $error)
1143
			{
1144
				// We remove directory
1145
				$ref = dol_sanitizeFileName($this->ref);
1146
				if ($conf->product->dir_output)
1147
				{
1148
					$dir = $conf->product->dir_output . "/" . $ref;
1149
					if (file_exists($dir))
1150
					{
1151
						$res=@dol_delete_dir_recursive($dir);
1152
						if (! $res)
1153
						{
1154
							$this->errors[] = 'ErrorFailToDeleteDir';
1155
							$error++;
1156
						}
1157
					}
1158
				}
1159
			}
1160
1161
			// Remove extrafields
1162
			if ((! $error) && (empty($conf->global->MAIN_EXTRAFIELDS_DISABLED))) // For avoid conflicts if trigger used
1163
			{
1164
				$result=$this->deleteExtraFields();
1165
				if ($result < 0)
1166
				{
1167
					$error++;
1168
					dol_syslog(get_class($this)."::delete error -4 ".$this->error, LOG_ERR);
1169
				}
1170
			}
1171
1172
			if (! $error)
1173
			{
1174
				$this->db->commit();
1175
				return 1;
1176
			}
1177
			else
1178
			{
1179
				foreach($this->errors as $errmsg)
1180
				{
1181
					dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR);
1182
					$this->error.=($this->error?', '.$errmsg:$errmsg);
1183
				}
1184
				$this->db->rollback();
1185
				return -$error;
1186
			}
1187
		}
1188
		else
1189
		{
1190
			$this->error = "ErrorRecordIsUsedCantDelete";
1191
			return 0;
1192
		}
1193
	}
1194
1195
	/**
1196
	 *	Update or add a translation for a product
1197
	 *
1198
	 *	@param     User	   $user                   Object user making update
1199
	 *	@return	   int		<0 if KO, >0 if OK
1200
	 */
1201
	function setMultiLangs($user)
1202
	{
1203
		global $conf, $langs;
1204
1205
		$langs_available = $langs->get_available_languages(DOL_DOCUMENT_ROOT, 0, 2);
1206
		$current_lang = $langs->getDefaultLang();
1207
1208
		foreach ($langs_available as $key => $value)
1209
		{
1210
			if ($key == $current_lang)
1211
			{
1212
				$sql = "SELECT rowid";
1213
				$sql.= " FROM ".MAIN_DB_PREFIX."product_lang";
1214
				$sql.= " WHERE fk_product=".$this->id;
1215
				$sql.= " AND lang='".$key."'";
1216
1217
				$result = $this->db->query($sql);
1218
1219
				if ($this->db->num_rows($result)) // if there is already a description line for this language
1220
				{
1221
					$sql2 = "UPDATE ".MAIN_DB_PREFIX."product_lang";
1222
					$sql2.= " SET ";
1223
					$sql2.= " label='".$this->db->escape($this->label)."',";
1224
					$sql2.= " description='".$this->db->escape($this->description)."'";
1225
					if (! empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) $sql2.= ", note='".$this->db->escape($this->note)."'";
1226
					$sql2.= " WHERE fk_product=".$this->id." AND lang='".$this->db->escape($key)."'";
1227
				}
1228
				else
1229
				{
1230
					$sql2 = "INSERT INTO ".MAIN_DB_PREFIX."product_lang (fk_product, lang, label, description";
1231
					if (! empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) $sql2.=", note";
1232
					$sql2.= ")";
1233
					$sql2.= " VALUES(".$this->id.",'".$this->db->escape($key)."','". $this->db->escape($this->label)."',";
1234
					$sql2.= " '".$this->db->escape($this->description)."'";
1235
					if (! empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) $sql2.= ", '".$this->db->escape($this->note)."'";
1236
					$sql2.= ")";
1237
				}
1238
				dol_syslog(get_class($this).'::setMultiLangs key = current_lang = '.$key);
1239
				if (! $this->db->query($sql2))
1240
				{
1241
					$this->error=$this->db->lasterror();
1242
					return -1;
1243
				}
1244
			}
1245
			else if (isset($this->multilangs[$key]))
1246
			{
1247
				$sql = "SELECT rowid";
1248
				$sql.= " FROM ".MAIN_DB_PREFIX."product_lang";
1249
				$sql.= " WHERE fk_product=".$this->id;
1250
				$sql.= " AND lang='".$key."'";
1251
1252
				$result = $this->db->query($sql);
1253
1254
				if ($this->db->num_rows($result)) // if there is already a description line for this language
1255
				{
1256
					$sql2 = "UPDATE ".MAIN_DB_PREFIX."product_lang";
1257
					$sql2.= " SET ";
1258
					$sql2.= " label='".$this->db->escape($this->multilangs["$key"]["label"])."',";
1259
					$sql2.= " description='".$this->db->escape($this->multilangs["$key"]["description"])."'";
1260
					if (! empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) $sql2.= ", note='".$this->db->escape($this->multilangs["$key"]["note"])."'";
1261
					$sql2.= " WHERE fk_product=".$this->id." AND lang='".$this->db->escape($key)."'";
1262
				}
1263
				else
1264
				{
1265
					$sql2 = "INSERT INTO ".MAIN_DB_PREFIX."product_lang (fk_product, lang, label, description";
1266
					if (! empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) $sql2.=", note";
1267
					$sql2.= ")";
1268
					$sql2.= " VALUES(".$this->id.",'".$this->db->escape($key)."','". $this->db->escape($this->multilangs["$key"]["label"])."',";
1269
					$sql2.= " '".$this->db->escape($this->multilangs["$key"]["description"])."'";
1270
					if (! empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) $sql2.= ", '".$this->db->escape($this->note)."'";
1271
					$sql2.= ")";
1272
				}
1273
1274
				// We do not save if main fields are empty
1275
				if ($this->multilangs["$key"]["label"] || $this->multilangs["$key"]["description"])
1276
				{
1277
    				if (! $this->db->query($sql2))
1278
    				{
1279
    					$this->error=$this->db->lasterror();
1280
    					return -1;
1281
    				}
1282
				}
1283
			}
1284
			else
1285
			{
1286
				// language is not current language and we didn't provide a multilang description for this language
1287
			}
1288
		}
1289
1290
		// Call trigger
1291
		$result = $this->call_trigger('PRODUCT_SET_MULTILANGS',$user);
1292
		if ($result < 0) {
1293
			$this->error = $this->db->lasterror();
1294
			return -1;
1295
		}
1296
		// End call triggers
1297
1298
		return 1;
1299
	}
1300
1301
	/**
1302
	 *	Delete a language for this product
1303
	 *
1304
	 *  @param		string	$langtodelete		Language code to delete
1305
	 *	@param		User	$user       Object user making delete
1306
	 *
1307
	 *	@return		int							<0 if KO, >0 if OK
1308
	 */
1309
	function delMultiLangs($langtodelete, $user)
1310
	{
1311
		$sql = "DELETE FROM ".MAIN_DB_PREFIX."product_lang";
1312
		$sql.= " WHERE fk_product=".$this->id." AND lang='".$this->db->escape($langtodelete)."'";
1313
1314
		dol_syslog(get_class($this).'::delMultiLangs', LOG_DEBUG);
1315
		$result = $this->db->query($sql);
1316
		if ($result)
1317
		{
1318
			// Call trigger
1319
			$result = $this->call_trigger('PRODUCT_DEL_MULTILANGS',$user);
1320
			if ($result < 0) {
1321
				$this->error = $this->db->lasterror();
1322
				dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
1323
				return -1;
1324
			}
1325
			// End call triggers
1326
			return 1;
1327
		}
1328
		else
1329
		{
1330
			$this->error=$this->db->lasterror();
1331
			dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
1332
			return -1;
1333
		}
1334
	}
1335
1336
	/*
1337
	 * Sets an accountancy code for a product.
1338
	 * Also calls PRODUCT_MODIFY trigger when modified
1339
	 *
1340
	 * @param string $type It can be 'buy', 'sell', 'sell_intra' or 'sell_export'
1341
	 * @param string $value Accountancy code
1342
	 * @return int <0 KO >0 OK
1343
	 */
1344
	public function setAccountancyCode($type, $value)
1345
	{
1346
		global $user, $langs, $conf;
1347
1348
		$this->db->begin();
1349
1350
		if ($type == 'buy') {
1351
			$field = 'accountancy_code_buy';
1352
		} elseif ($type == 'sell') {
1353
			$field = 'accountancy_code_sell';
1354
		} elseif ($type == 'sell_intra') {
1355
			$field = 'accountancy_code_sell_intra';
1356
		} elseif ($type == 'sell_export') {
1357
			$field = 'accountancy_code_sell_export';
1358
		} else {
1359
			return -1;
1360
		}
1361
1362
		$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET ";
1363
		$sql.= "$field = '".$this->db->escape($value)."'";
1364
		$sql.= " WHERE rowid = ".$this->id;
1365
1366
		dol_syslog(get_class($this)."::".__FUNCTION__." sql=".$sql, LOG_DEBUG);
1367
		$resql = $this->db->query($sql);
1368
1369
		if ($resql)
1370
		{
1371
			// Call triggers
1372
			include_once DOL_DOCUMENT_ROOT . '/core/class/interfaces.class.php';
1373
			$interface=new Interfaces($this->db);
1374
			$result=$interface->run_triggers('PRODUCT_MODIFY',$this,$user,$langs,$conf);
1375
			if ($result < 0)
1376
			{
1377
				$this->errors=$interface->errors;
1378
				$this->db->rollback();
1379
				return -1;
1380
			}
1381
			// End call triggers
1382
1383
			$this->$field = $value;
1384
1385
			$this->db->commit();
1386
			return 1;
1387
		}
1388
		else
1389
		{
1390
			$this->error=$this->db->lasterror();
1391
			$this->db->rollback();
1392
			return -1;
1393
		}
1394
	}
1395
1396
	/**
1397
	 *	Load array this->multilangs
1398
	 *
1399
	 *	@return		int		<0 if KO, >0 if OK
1400
	 */
1401
	function getMultiLangs()
1402
	{
1403
		global $langs;
1404
1405
		$current_lang = $langs->getDefaultLang();
1406
1407
		$sql = "SELECT lang, label, description, note as other";
1408
		$sql.= " FROM ".MAIN_DB_PREFIX."product_lang";
1409
		$sql.= " WHERE fk_product=".$this->id;
1410
1411
		$result = $this->db->query($sql);
1412
		if ($result)
1413
		{
1414
			while ($obj = $this->db->fetch_object($result))
1415
			{
1416
				//print 'lang='.$obj->lang.' current='.$current_lang.'<br>';
1417
				if ($obj->lang == $current_lang)  // si on a les traduct. dans la langue courante on les charge en infos principales.
1418
				{
1419
					$this->label		= $obj->label;
1420
					$this->description	= $obj->description;
1421
					$this->other	    = $obj->other;
1422
				}
1423
				$this->multilangs["$obj->lang"]["label"]		= $obj->label;
1424
				$this->multilangs["$obj->lang"]["description"]	= $obj->description;
1425
				$this->multilangs["$obj->lang"]["other"]		= $obj->other;
1426
			}
1427
			return 1;
1428
		}
1429
		else
1430
		{
1431
			$this->error="Error: ".$this->db->lasterror()." - ".$sql;
1432
			return -1;
1433
		}
1434
	}
1435
1436
1437
1438
	/**
1439
	 *  Insert a track that we changed a customer price
1440
	 *
1441
	 *	@param  	User	$user       User making change
1442
	 *	@param		int		$level		price level to change
1443
	 *	@return		int					<0 if KO, >0 if OK
1444
	 */
1445
	function _log_price($user,$level=0)
1446
	{
1447
		global $conf;
1448
1449
		$now=dol_now();
1450
1451
		// Clean parameters
1452
		if (empty($this->price_by_qty)) $this->price_by_qty=0;
1453
1454
		// Add new price
1455
		$sql = "INSERT INTO ".MAIN_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,";
1456
		$sql.= " localtax1_tx, localtax2_tx, localtax1_type, localtax2_type, price_min,price_min_ttc,price_by_qty,entity,fk_price_expression) ";
1457
		$sql.= " VALUES(".($level?$level:1).", '".$this->db->idate($now)."',".$this->id.",".$user->id.",".$this->price.",".$this->price_ttc.",'".$this->db->escape($this->price_base_type)."',".$this->status.",".$this->tva_tx.", ".($this->default_vat_code?("'".$this->db->escape($this->default_vat_code)."'"):"null").",".$this->tva_npr.",";
1458
		$sql.= " ".$this->localtax1_tx.", ".$this->localtax2_tx.", '".$this->db->escape($this->localtax1_type)."', '".$this->db->escape($this->localtax2_type)."', ".$this->price_min.",".$this->price_min_ttc.",".$this->price_by_qty.",".$conf->entity.",".($this->fk_price_expression > 0?$this->fk_price_expression:'null');
1459
		$sql.= ")";
1460
1461
		dol_syslog(get_class($this)."::_log_price", LOG_DEBUG);
1462
		$resql=$this->db->query($sql);
1463
		if(! $resql)
1464
		{
1465
			$this->error=$this->db->lasterror();
1466
			dol_print_error($this->db);
1467
			return -1;
1468
		}
1469
		else
1470
		{
1471
			return 1;
1472
		}
1473
	}
1474
1475
1476
	/**
1477
	 *  Delete a price line
1478
	 *
1479
	 * 	@param		User	$user	Object user
1480
	 * 	@param		int		$rowid	Line id to delete
1481
	 * 	@return		int				<0 if KO, >0 if OK
1482
	 */
1483
	function log_price_delete($user,$rowid)
1484
	{
1485
		$sql = "DELETE FROM ".MAIN_DB_PREFIX."product_price";
1486
		$sql.= " WHERE rowid=".$rowid;
1487
1488
		dol_syslog(get_class($this)."::log_price_delete", LOG_DEBUG);
1489
		$resql=$this->db->query($sql);
1490
		if ($resql)
1491
		{
1492
			return 1;
1493
		}
1494
		else
1495
		{
1496
			$this->error=$this->db->lasterror();
1497
			return -1;
1498
		}
1499
1500
	}
1501
1502
1503
	/**
1504
	 *	Read price used by a provider.
1505
	 *	We enter as input couple prodfournprice/qty or triplet qty/product_id/fourn_ref.
1506
	 *  This also set some properties on product like ->buyprice, ->fourn_pu, ...
1507
	 *
1508
	 *  @param     	int		$prodfournprice     Id du tarif = rowid table product_fournisseur_price
1509
	 *  @param     	double	$qty                Quantity asked or -1 to get first entry found
1510
	 *	@param		int		$product_id			Filter on a particular product id
1511
	 * 	@param		string	$fourn_ref			Filter on a supplier price ref. 'none' to exclude ref in search.
1512
	 *  @param      int     $fk_soc             If of supplier
1513
	 *  @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...)
1514
	 */
1515
	function get_buyprice($prodfournprice, $qty, $product_id=0, $fourn_ref='', $fk_soc=0)
1516
	{
1517
		global $conf;
1518
		$result = 0;
1519
1520
		// We do a first seach with a select by searching with couple prodfournprice and qty only (later we will search on triplet qty/product_id/fourn_ref)
1521
		$sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent,";
1522
		$sql.= " pfp.fk_product, pfp.ref_fourn, pfp.fk_soc, pfp.tva_tx, pfp.fk_supplier_price_expression";
1523
		$sql.= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pfp";
1524
		$sql.= " WHERE pfp.rowid = ".$prodfournprice;
1525
		if ($qty > 0) $sql.= " AND pfp.quantity <= ".$qty;
1526
		$sql.= " ORDER BY pfp.quantity DESC";
1527
1528
		dol_syslog(get_class($this)."::get_buyprice first search by prodfournprice/qty", LOG_DEBUG);
1529
		$resql = $this->db->query($sql);
1530
		if ($resql)
1531
		{
1532
			$obj = $this->db->fetch_object($resql);
1533
			if ($obj && $obj->quantity > 0)		// If we found a supplier prices from the id of supplier price
1534
			{
1535
                if (!empty($conf->dynamicprices->enabled) && !empty($obj->fk_supplier_price_expression))
1536
                {
1537
					require_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
1538
                    $prod_supplier = new ProductFournisseur($this->db);
1539
                    $prod_supplier->product_fourn_price_id = $obj->rowid;
1540
                    $prod_supplier->id = $obj->fk_product;
1541
                    $prod_supplier->fourn_qty = $obj->quantity;
1542
                    $prod_supplier->fourn_tva_tx = $obj->tva_tx;
1543
                    $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
1544
                    $priceparser = new PriceParser($this->db);
1545
                    $price_result = $priceparser->parseProductSupplier($prod_supplier);
1546
                    if ($price_result >= 0) {
1547
                    	$obj->price = $price_result;
1548
                    }
1549
                }
1550
                $this->product_fourn_price_id = $obj->rowid;
1551
				$this->buyprice = $obj->price;                      // deprecated
1552
				$this->fourn_pu = $obj->price / $obj->quantity;     // Unit price of product of supplier
1553
				$this->fourn_price_base_type = 'HT';                // Price base type
1554
				$this->ref_fourn = $obj->ref_fourn;                 // deprecated
1555
				$this->ref_supplier = $obj->ref_fourn;              // Ref supplier
1556
				$this->remise_percent = $obj->remise_percent;       // remise percent if present and not typed
1557
				$this->vatrate_supplier = $obj->tva_tx;             // Vat ref supplier
1558
				$result=$obj->fk_product;
1559
				return $result;
1560
			}
1561
			else // If not found
1562
			{
1563
				// 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.
1564
				$sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.fk_soc,";
1565
				$sql.= " pfp.fk_product, pfp.ref_fourn as ref_supplier, pfp.tva_tx, pfp.fk_supplier_price_expression";
1566
				$sql.= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pfp";
1567
				$sql.= " WHERE pfp.fk_product = ".$product_id;
1568
				if ($fourn_ref != 'none') $sql.= " AND pfp.ref_fourn = '".$fourn_ref."'";
1569
				if ($fk_soc > 0) $sql.= " AND pfp.fk_soc = ".$fk_soc;
1570
				if ($qty > 0) $sql.= " AND pfp.quantity <= ".$qty;
1571
				$sql.= " ORDER BY pfp.quantity DESC";
1572
				$sql.= " LIMIT 1";
1573
1574
				dol_syslog(get_class($this)."::get_buyprice second search from qty/ref/product_id", LOG_DEBUG);
1575
				$resql = $this->db->query($sql);
1576
				if ($resql)
1577
				{
1578
					$obj = $this->db->fetch_object($resql);
1579
					if ($obj && $obj->quantity > 0)		// If found
1580
					{
1581
		                if (!empty($conf->dynamicprices->enabled) && !empty($obj->fk_supplier_price_expression))
1582
		                {
1583
							require_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
1584
		                    $prod_supplier = new ProductFournisseur($this->db);
1585
		                    $prod_supplier->product_fourn_price_id = $obj->rowid;
1586
		                    $prod_supplier->id = $obj->fk_product;
1587
		                    $prod_supplier->fourn_qty = $obj->quantity;
1588
		                    $prod_supplier->fourn_tva_tx = $obj->tva_tx;
1589
		                    $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
1590
		                    $priceparser = new PriceParser($this->db);
1591
		                    $price_result = $priceparser->parseProductSupplier($prod_supplier);
1592
		                    if ($result >= 0) {
1593
		                    	$obj->price = $price_result;
1594
		                    }
1595
		                }
1596
		                $this->product_fourn_price_id = $obj->rowid;
1597
						$this->buyprice = $obj->price;                      // deprecated
1598
						$this->fourn_qty = $obj->quantity;					// min quantity for price for a virtual supplier
1599
						$this->fourn_pu = $obj->price / $obj->quantity;     // Unit price of product for a virtual supplier
1600
						$this->fourn_price_base_type = 'HT';                // Price base type for a virtual supplier
1601
						$this->ref_fourn = $obj->ref_supplier;              // deprecated
1602
						$this->ref_supplier = $obj->ref_supplier;           // Ref supplier
1603
						$this->remise_percent = $obj->remise_percent;       // remise percent if present and not typed
1604
						$this->vatrate_supplier = $obj->tva_tx;             // Vat ref supplier
1605
						$result=$obj->fk_product;
1606
						return $result;
1607
					}
1608
					else
1609
					{
1610
						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é.
1611
					}
1612
				}
1613
				else
1614
				{
1615
					$this->error=$this->db->lasterror();
1616
					return -3;
1617
				}
1618
			}
1619
		}
1620
		else
1621
		{
1622
			$this->error=$this->db->lasterror();
1623
			return -2;
1624
		}
1625
	}
1626
1627
1628
	/**
1629
	 *	Modify customer price of a product/Service
1630
	 *
1631
	 *	@param  	double	$newprice		    New price
1632
	 *	@param  	string	$newpricebase	    HT or TTC
1633
	 *	@param  	User	$user        	    Object user that make change
1634
	 *	@param  	double	$newvat			    New VAT Rate (For example 8.5. Should not be a string)
1635
	 *  @param		double	$newminprice	    New price min
1636
	 *  @param		int		$level			    0=standard, >0 = level if multilevel prices
1637
	 *  @param     	int		$newnpr             0=Standard vat rate, 1=Special vat rate for French NPR VAT
1638
	 *  @param     	int		$newpsq             1 if it has price by quantity
1639
	 *  @param 		int 	$ignore_autogen     Used to avoid infinite loops
1640
     *	@param      array	$localtaxes_array	Array with localtaxes info array('0'=>type1,'1'=>rate1,'2'=>type2,'3'=>rate2) (loaded by getLocalTaxesFromRate(vatrate, 0, ...) function).
1641
     *  @param      string  $newdefaultvatcode  Default vat code
1642
	 * 	@return		int						    <0 if KO, >0 if OK
1643
	 */
1644
	function updatePrice($newprice, $newpricebase, $user, $newvat='',$newminprice='', $level=0, $newnpr=0, $newpsq=0, $ignore_autogen=0, $localtaxes_array=array(), $newdefaultvatcode='')
1645
	{
1646
		global $conf,$langs;
1647
1648
		$id=$this->id;
1649
1650
		dol_syslog(get_class($this)."::update_price id=".$id." newprice=".$newprice." newpricebase=".$newpricebase." newminprice=".$newminprice." level=".$level." npr=".$newnpr." newdefaultvatcode=".$newdefaultvatcode);
1651
1652
		// Clean parameters
1653
		if (empty($this->tva_tx))  $this->tva_tx=0;
1654
        if (empty($newnpr)) $newnpr=0;
1655
1656
		// Check parameters
1657
		if ($newvat == '') $newvat=$this->tva_tx;
1658
1659
		// If multiprices are enabled, then we check if the current product is subject to price autogeneration
1660
		// Price will be modified ONLY when the first one is the one that is being modified
1661
		if (!empty($conf->global->PRODUIT_MULTIPRICES) && !$ignore_autogen && $this->price_autogen && ($level == 1))
1662
		{
1663
			return $this->generateMultiprices($user, $newprice, $newpricebase, $newvat, $newnpr, $newpsq);
1664
		}
1665
1666
		if (! empty($newminprice) && ($newminprice > $newprice))
1667
		{
1668
			$this->error='ErrorPriceCantBeLowerThanMinPrice';
1669
			return -1;
1670
		}
1671
1672
		if ($newprice!='' || $newprice==0)
1673
		{
1674
			if ($newpricebase == 'TTC')
1675
			{
1676
				$price_ttc = price2num($newprice,'MU');
1677
				$price = price2num($newprice) / (1 + ($newvat / 100));
1678
				$price = price2num($price,'MU');
1679
1680
				if ($newminprice!='' || $newminprice==0)
1681
				{
1682
					$price_min_ttc = price2num($newminprice,'MU');
1683
					$price_min = price2num($newminprice) / (1 + ($newvat / 100));
1684
					$price_min = price2num($price_min,'MU');
1685
				}
1686
				else
1687
				{
1688
					$price_min=0;
1689
					$price_min_ttc=0;
1690
				}
1691
			}
1692
			else
1693
			{
1694
				$price = price2num($newprice,'MU');
1695
				$price_ttc = ( $newnpr != 1 ) ? price2num($newprice) * (1 + ($newvat / 100)) : $price;
1696
				$price_ttc = price2num($price_ttc,'MU');
1697
1698
				if ($newminprice!='' || $newminprice==0)
1699
				{
1700
					$price_min = price2num($newminprice,'MU');
1701
					$price_min_ttc = price2num($newminprice) * (1 + ($newvat / 100));
1702
					$price_min_ttc = price2num($price_min_ttc,'MU');
1703
					//print 'X'.$newminprice.'-'.$price_min;
1704
				}
1705
				else
1706
				{
1707
					$price_min=0;
1708
					$price_min_ttc=0;
1709
				}
1710
			}
1711
			//print 'x'.$id.'-'.$newprice.'-'.$newpricebase.'-'.$price.'-'.$price_ttc.'-'.$price_min.'-'.$price_min_ttc;
1712
1713
			if (count($localtaxes_array) > 0)
1714
			{
1715
			    $localtaxtype1=$localtaxes_array['0'];
1716
			    $localtax1=$localtaxes_array['1'];
1717
			    $localtaxtype2=$localtaxes_array['2'];
1718
			    $localtax2=$localtaxes_array['3'];
1719
			}
1720
			else     // old method. deprecated because ot can't retreive type
1721
			{
1722
	       		$localtaxtype1='0';
1723
			    $localtax1=get_localtax($newvat,1);
1724
	       		$localtaxtype2='0';
1725
			    $localtax2=get_localtax($newvat,2);
1726
			}
1727
			if (empty($localtax1)) $localtax1=0;	// If = '' then = 0
1728
			if (empty($localtax2)) $localtax2=0;	// If = '' then = 0
1729
1730
			$this->db->begin();
1731
1732
			// Ne pas mettre de quote sur les numeriques decimaux.
1733
			// Ceci provoque des stockages avec arrondis en base au lieu des valeurs exactes.
1734
			$sql = "UPDATE ".MAIN_DB_PREFIX."product SET";
1735
			$sql.= " price_base_type='".$newpricebase."',";
1736
			$sql.= " price=".$price.",";
1737
			$sql.= " price_ttc=".$price_ttc.",";
1738
			$sql.= " price_min=".$price_min.",";
1739
			$sql.= " price_min_ttc=".$price_min_ttc.",";
1740
			$sql.= " localtax1_tx=".($localtax1>=0?$localtax1:'NULL').",";
1741
			$sql.= " localtax2_tx=".($localtax2>=0?$localtax2:'NULL').",";
1742
			$sql.= " localtax1_type=".($localtaxtype1!=''?"'".$localtaxtype1."'":"'0'").",";
1743
			$sql.= " localtax2_type=".($localtaxtype2!=''?"'".$localtaxtype2."'":"'0'").",";
1744
            $sql.= " default_vat_code=".($newdefaultvatcode?"'".$this->db->escape($newdefaultvatcode)."'":"null").",";
1745
			$sql.= " tva_tx='".price2num($newvat)."',";
1746
            $sql.= " recuperableonly='".$newnpr."'";
1747
			$sql.= " WHERE rowid = ".$id;
1748
1749
			dol_syslog(get_class($this)."::update_price", LOG_DEBUG);
1750
			$resql=$this->db->query($sql);
1751
			if ($resql)
1752
			{
1753
				$this->multiprices[$level] = $price;
1754
				$this->multiprices_ttc[$level] = $price_ttc;
1755
				$this->multiprices_min[$level]= $price_min;
1756
				$this->multiprices_min_ttc[$level]= $price_min_ttc;
1757
				$this->multiprices_base_type[$level]= $newpricebase;
1758
				$this->multiprices_default_vat_code[$level]= $newdefaultvatcode;
1759
				$this->multiprices_tva_tx[$level]= $newvat;
1760
				$this->multiprices_recuperableonly[$level]= $newnpr;
1761
1762
				$this->price = $price;
1 ignored issue
show
Documentation Bug introduced by
It seems like $price can also be of type string. However, the property $price is declared as type double. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1766
				$this->price_base_type = $newpricebase;
0 ignored issues
show
Documentation Bug introduced by
The property $price_base_type was declared of type double, but $newpricebase is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
1767
				$this->default_vat_code = $newdefaultvatcode;
1768
				$this->tva_tx = $newvat;
1769
				$this->tva_npr = $newnpr;
1770
				//Local taxes
1771
				$this->localtax1_tx = $localtax1;
1772
				$this->localtax2_tx = $localtax2;
1773
				$this->localtax1_type = $localtaxtype1;
1774
				$this->localtax2_type = $localtaxtype2;
1775
1776
				// Price by quantity
1777
				$this->price_by_qty = $newpsq;
1778
1779
				$this->_log_price($user,$level);	// Save price for level into table product_price
1780
1781
				$this->level = $level;				// Store level of price edited for trigger
1782
1783
                // Call trigger
1784
                $result=$this->call_trigger('PRODUCT_PRICE_MODIFY',$user);
1785
                if ($result < 0)
1786
                {
1787
                	$this->db->rollback();
1788
                	return -1;
1789
                }
1790
                // End call triggers
1791
1792
                $this->db->commit();
1793
			}
1794
			else
1795
			{
1796
				$this->db->rollback();
1797
			    dol_print_error($this->db);
1798
			}
1799
		}
1800
1801
		return 1;
1802
	}
1803
1804
    /**
1805
     *  Sets the supplier price expression
1806
     *
1807
     *  @param  int     $expression_id	Expression
1808
     *  @return int                 	<0 if KO, >0 if OK
1809
	 * @deprecated Use Product::update instead
1810
     */
1811
    function setPriceExpression($expression_id)
1812
    {
1813
		global $user;
1814
1815
		$this->fk_price_expression = $expression_id;
1816
1817
		return $this->update($this->id, $user);
1818
    }
1819
1820
	/**
1821
	 *  Load a product in memory from database
1822
	 *
1823
	 *  @param	int		$id      			Id of product/service to load
1824
	 *  @param  string	$ref     			Ref of product/service to load
1825
	 *  @param	string	$ref_ext			Ref ext of product/service to load
1826
     *  @param	int		$ignore_expression  Ignores the math expression for calculating price and uses the db value instead
1827
	 *  @return int     					<0 if KO, 0 if not found, >0 if OK
1828
	 */
1829
	function fetch($id='', $ref='', $ref_ext='', $ignore_expression=0)
1830
	{
1831
	    include_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
1832
1833
		global $langs, $conf;
1834
1835
		dol_syslog(get_class($this)."::fetch id=".$id." ref=".$ref." ref_ext=".$ref_ext);
1836
1837
		// Check parameters
1838
		if (! $id && ! $ref && ! $ref_ext)
1839
		{
1840
			$this->error='ErrorWrongParameters';
1841
			dol_print_error(get_class($this)."::fetch ".$this->error);
1842
			return -1;
1843
		}
1844
1845
		$sql = "SELECT rowid, ref, ref_ext, label, description, url, note as note_private, customcode, fk_country, price, price_ttc,";
1846
		$sql.= " price_min, price_min_ttc, price_base_type, cost_price, default_vat_code, tva_tx, recuperableonly as tva_npr, localtax1_tx, localtax2_tx, localtax1_type, localtax2_type, tosell,";
1847
		$sql.= " tobuy, fk_product_type, duration, seuil_stock_alerte, canvas, weight, weight_units,";
1848
		$sql.= " length, length_units, width, width_units, height, height_units,";
1849
		$sql.= " surface, surface_units, volume, volume_units, barcode, fk_barcode_type, finished,";
1850
		$sql.= " accountancy_code_buy, accountancy_code_sell, accountancy_code_sell_intra, accountancy_code_sell_export, stock, pmp,";
1851
		$sql.= " datec, tms, import_key, entity, desiredstock, tobatch, fk_unit,";
1852
		$sql.= " fk_price_expression, price_autogen";
1853
		$sql.= " FROM ".MAIN_DB_PREFIX."product";
1854
		if ($id) $sql.= " WHERE rowid = ".$this->db->escape($id);
1855
		else
1856
		{
1857
			$sql.= " WHERE entity IN (".getEntity($this->element, 1).")";
1858
			if ($ref) $sql.= " AND ref = '".$this->db->escape($ref)."'";
1859
			else if ($ref_ext) $sql.= " AND ref_ext = '".$this->db->escape($ref_ext)."'";
1860
		}
1861
1862
		$resql = $this->db->query($sql);
1863
		if ( $resql )
1864
		{
1865
			if ($this->db->num_rows($resql) > 0)
1866
			{
1867
				$obj = $this->db->fetch_object($resql);
1868
1869
				$this->id							= $obj->rowid;
1870
				$this->ref							= $obj->ref;
1871
				$this->ref_ext						= $obj->ref_ext;
1872
				$this->label						= $obj->label;
1873
				$this->description					= $obj->description;
1874
				$this->url							= $obj->url;
1875
				$this->note_private					= $obj->note_private;
1876
				$this->note							= $obj->note_private;  // deprecated
1877
1878
				$this->type							= $obj->fk_product_type;
1879
				$this->status						= $obj->tosell;
1880
				$this->status_buy					= $obj->tobuy;
1881
				$this->status_batch					= $obj->tobatch;
1882
1883
				$this->customcode					= $obj->customcode;
1884
				$this->country_id					= $obj->fk_country;
1885
				$this->country_code					= getCountry($this->country_id,2,$this->db);
1886
				$this->price						= $obj->price;
1887
				$this->price_ttc					= $obj->price_ttc;
1888
				$this->price_min					= $obj->price_min;
1889
				$this->price_min_ttc				= $obj->price_min_ttc;
1890
				$this->price_base_type				= $obj->price_base_type;
1891
				$this->cost_price					= $obj->cost_price;
1892
				$this->default_vat_code				= $obj->default_vat_code;
1893
				$this->tva_tx						= $obj->tva_tx;
1894
				//! French VAT NPR
1895
				$this->tva_npr						= $obj->tva_npr;
1896
				$this->recuperableonly				= $obj->tva_npr;       // For backward compatibility
1897
				//! Local taxes
1898
				$this->localtax1_tx					= $obj->localtax1_tx;
1899
				$this->localtax2_tx					= $obj->localtax2_tx;
1900
				$this->localtax1_type				= $obj->localtax1_type;
1901
				$this->localtax2_type				= $obj->localtax2_type;
1902
1903
				$this->finished						= $obj->finished;
1904
				$this->duration						= $obj->duration;
1905
				$this->duration_value				= substr($obj->duration,0,dol_strlen($obj->duration)-1);
1906
				$this->duration_unit				= substr($obj->duration,-1);
1907
				$this->canvas						= $obj->canvas;
1908
				$this->weight						= $obj->weight;
1909
				$this->weight_units					= $obj->weight_units;
1910
				$this->length						= $obj->length;
1911
				$this->length_units					= $obj->length_units;
1912
				$this->width						= $obj->width;
1913
				$this->width_units					= $obj->width_units;
1914
				$this->height						= $obj->height;
1915
				$this->height_units					= $obj->height_units;
1916
1917
				$this->surface						= $obj->surface;
1918
				$this->surface_units				= $obj->surface_units;
1919
				$this->volume						= $obj->volume;
1920
				$this->volume_units					= $obj->volume_units;
1921
				$this->barcode						= $obj->barcode;
1922
				$this->barcode_type					= $obj->fk_barcode_type;
1923
1924
				$this->accountancy_code_buy			= $obj->accountancy_code_buy;
1925
				$this->accountancy_code_sell		= $obj->accountancy_code_sell;
1926
				$this->accountancy_code_sell_intra	= $obj->accountancy_code_sell_intra;
1927
				$this->accountancy_code_sell_export	= $obj->accountancy_code_sell_export;
1928
1929
				$this->seuil_stock_alerte			= $obj->seuil_stock_alerte;
1930
				$this->desiredstock					= $obj->desiredstock;
1931
				$this->stock_reel					= $obj->stock;
1932
				$this->pmp							= $obj->pmp;
1933
1934
				$this->date_creation				= $obj->datec;
1935
				$this->date_modification			= $obj->tms;
1936
				$this->import_key					= $obj->import_key;
1937
				$this->entity						= $obj->entity;
1938
1939
				$this->ref_ext						= $obj->ref_ext;
1940
				$this->fk_price_expression			= $obj->fk_price_expression;
1941
				$this->fk_unit						= $obj->fk_unit;
1942
				$this->price_autogen				= $obj->price_autogen;
1943
1944
				$this->db->free($resql);
1945
1946
1947
				// Retreive all extrafield for current object
1948
				// fetch optionals attributes and labels
1949
				require_once(DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php');
1950
				$extrafields=new ExtraFields($this->db);
1951
				$extralabels=$extrafields->fetch_name_optionals_label($this->table_element,true);
1952
				$this->fetch_optionals($this->id,$extralabels);
1953
1954
				// multilangs
1955
				if (! empty($conf->global->MAIN_MULTILANGS)) $this->getMultiLangs();
1956
1957
				// Load multiprices array
1958
				if (! empty($conf->global->PRODUIT_MULTIPRICES))
1959
				{
1960
					for ($i=1; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++)
1961
					{
1962
						$sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
1963
						$sql.= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
1964
						$sql.= " FROM ".MAIN_DB_PREFIX."product_price";
1965
						$sql.= " WHERE entity IN (".getEntity('productprice').")";
1966
						$sql.= " AND price_level=".$i;
1967
						$sql.= " AND fk_product = ".$this->id;
1968
						$sql.= " ORDER BY date_price DESC, rowid DESC";
1969
						$sql.= " LIMIT 1";
1970
						$resql = $this->db->query($sql);
1971
						if ($resql)
1972
						{
1973
							$result = $this->db->fetch_array($resql);
1974
1975
							$this->multiprices[$i]=$result["price"];
1976
							$this->multiprices_ttc[$i]=$result["price_ttc"];
1977
							$this->multiprices_min[$i]=$result["price_min"];
1978
							$this->multiprices_min_ttc[$i]=$result["price_min_ttc"];
1979
							$this->multiprices_base_type[$i]=$result["price_base_type"];
1980
							// Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
1981
							$this->multiprices_tva_tx[$i]=$result["tva_tx"];     // TODO Add ' ('.$result['default_vat_code'].')'
1982
							$this->multiprices_recuperableonly[$i]=$result["recuperableonly"];
1983
1984
							// Price by quantity
1985
							$this->prices_by_qty[$i]=$result["price_by_qty"];
1986
							$this->prices_by_qty_id[$i]=$result["rowid"];
1987
							// Récuperation de la liste des prix selon qty si flag positionné
1988
							if ($this->prices_by_qty[$i] == 1)
1989
							{
1990
								$sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise";
1991
								$sql.= " FROM ".MAIN_DB_PREFIX."product_price_by_qty";
1992
								$sql.= " WHERE fk_product_price = ".$this->prices_by_qty_id[$i];
1993
								$sql.= " ORDER BY quantity ASC";
1994
								$resultat=array();
1995
								$resql = $this->db->query($sql);
1996
								if ($resql)
1997
								{
1998
									$ii=0;
1999
									while ($result= $this->db->fetch_array($resql)) {
2000
										$resultat[$ii]=array();
2001
										$resultat[$ii]["rowid"]=$result["rowid"];
2002
										$resultat[$ii]["price"]= $result["price"];
2003
										$resultat[$ii]["unitprice"]= $result["unitprice"];
2004
										$resultat[$ii]["quantity"]= $result["quantity"];
2005
										$resultat[$ii]["remise_percent"]= $result["remise_percent"];
2006
										$resultat[$ii]["remise"]= $result["remise"];
2007
										$ii++;
2008
									}
2009
									$this->prices_by_qty_list[$i]=$resultat;
2010
								}
2011
								else
2012
								{
2013
									dol_print_error($this->db);
2014
									return -1;
2015
								}
2016
							}
2017
						}
2018
						else
2019
						{
2020
							dol_print_error($this->db);
2021
							return -1;
2022
						}
2023
					}
2024
				} else if (! empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY))
2025
				{
2026
					$sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
2027
					$sql.= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid";
2028
					$sql.= " FROM ".MAIN_DB_PREFIX."product_price";
2029
					$sql.= " WHERE fk_product = ".$this->id;
2030
					$sql.= " ORDER BY date_price DESC, rowid DESC";
2031
					$sql.= " LIMIT 1";
2032
					$resql = $this->db->query($sql);
2033
					if ($resql)
2034
					{
2035
						$result = $this->db->fetch_array($resql);
2036
2037
						// Price by quantity
2038
						$this->prices_by_qty[0]=$result["price_by_qty"];
2039
						$this->prices_by_qty_id[0]=$result["rowid"];
2040
						// Récuperation de la liste des prix selon qty si flag positionné
2041
						if ($this->prices_by_qty[0] == 1)
2042
						{
2043
							$sql = "SELECT rowid,price, unitprice, quantity, remise_percent, remise";
2044
							$sql.= " FROM ".MAIN_DB_PREFIX."product_price_by_qty";
2045
							$sql.= " WHERE fk_product_price = ".$this->prices_by_qty_id[0];
2046
							$sql.= " ORDER BY quantity ASC";
2047
							$resultat=array();
2048
							$resql = $this->db->query($sql);
2049
							if ($resql)
2050
							{
2051
								$ii=0;
2052
								while ($result= $this->db->fetch_array($resql)) {
2053
									$resultat[$ii]=array();
2054
									$resultat[$ii]["rowid"]=$result["rowid"];
2055
									$resultat[$ii]["price"]= $result["price"];
2056
									$resultat[$ii]["unitprice"]= $result["unitprice"];
2057
									$resultat[$ii]["quantity"]= $result["quantity"];
2058
									$resultat[$ii]["remise_percent"]= $result["remise_percent"];
2059
									$resultat[$ii]["remise"]= $result["remise"];
2060
									$ii++;
2061
								}
2062
								$this->prices_by_qty_list[0]=$resultat;
2063
							}
2064
							else
2065
							{
2066
								dol_print_error($this->db);
2067
								return -1;
2068
							}
2069
						}
2070
					}
2071
					else
2072
					{
2073
						dol_print_error($this->db);
2074
						return -1;
2075
					}
2076
				}
2077
2078
                if (!empty($conf->dynamicprices->enabled) && !empty($this->fk_price_expression) && empty($ignore_expression))
2079
                {
2080
					require_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2081
                	$priceparser = new PriceParser($this->db);
2082
                    $price_result = $priceparser->parseProduct($this);
2083
                    if ($price_result >= 0)
2084
                    {
2085
                        $this->price = $price_result;
2086
                        // Calculate the VAT
2087
						$this->price_ttc = price2num($this->price) * (1 + ($this->tva_tx / 100));
2088
						$this->price_ttc = price2num($this->price_ttc,'MU');
1 ignored issue
show
Documentation Bug introduced by
It seems like price2num($this->price_ttc, 'MU') can also be of type string. However, the property $price_ttc is declared as type double. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2089
                    }
2090
                }
2091
2092
				// We should not load stock during the fetch. If someone need stock of product, he must call load_stock after fetching product.
2093
				//$res=$this->load_stock();
2094
				// instead we just init the stock_warehouse array
2095
				$this->stock_warehouse = array();
2096
2097
				return 1;
2098
			}
2099
			else
2100
			{
2101
				return 0;
2102
			}
2103
		}
2104
		else
2105
		{
2106
			dol_print_error($this->db);
2107
			return -1;
2108
		}
2109
	}
2110
2111
2112
	/**
2113
	 *  Charge tableau des stats propale pour le produit/service
2114
	 *
2115
	 *  @param    int	$socid      Id societe
2116
	 *  @return   array       		Tableau des stats
2117
	 */
2118
	function load_stats_propale($socid=0)
2119
	{
2120
		global $conf;
2121
		global $user;
2122
2123
		$sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_customers, COUNT(DISTINCT p.rowid) as nb,";
2124
		$sql.= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
2125
		$sql.= " FROM ".MAIN_DB_PREFIX."propaldet as pd";
2126
		$sql.= ", ".MAIN_DB_PREFIX."propal as p";
2127
		$sql.= ", ".MAIN_DB_PREFIX."societe as s";
2128
		if (!$user->rights->societe->client->voir && !$socid) $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
2129
		$sql.= " WHERE p.rowid = pd.fk_propal";
2130
		$sql.= " AND p.fk_soc = s.rowid";
2131
		$sql.= " AND p.entity IN (".getEntity('propal').")";
2132
		$sql.= " AND pd.fk_product = ".$this->id;
2133
		if (!$user->rights->societe->client->voir && !$socid) $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = " .$user->id;
2134
		//$sql.= " AND pr.fk_statut != 0";
2135
		if ($socid > 0)	$sql.= " AND p.fk_soc = ".$socid;
2136
2137
		$result = $this->db->query($sql);
2138
		if ( $result )
2139
		{
2140
			$obj=$this->db->fetch_object($result);
2141
			$this->stats_propale['customers']=$obj->nb_customers;
2142
			$this->stats_propale['nb']=$obj->nb;
2143
			$this->stats_propale['rows']=$obj->nb_rows;
2144
			$this->stats_propale['qty']=$obj->qty?$obj->qty:0;
2145
			return 1;
2146
		}
2147
		else
2148
		{
2149
			$this->error=$this->db->error();
2150
			return -1;
2151
		}
2152
	}
2153
2154
2155
	/**
2156
	 *  Charge tableau des stats propale pour le produit/service
2157
	 *
2158
	 *  @param    int	$socid      Id thirdparty
2159
	 *  @return   array       		Tableau des stats
2160
	 */
2161
	function load_stats_proposal_supplier($socid=0)
2162
	{
2163
		global $conf;
2164
		global $user;
2165
2166
		$sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_suppliers, COUNT(DISTINCT p.rowid) as nb,";
2167
		$sql.= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
2168
		$sql.= " FROM ".MAIN_DB_PREFIX."supplier_proposaldet as pd";
2169
		$sql.= ", ".MAIN_DB_PREFIX."supplier_proposal as p";
2170
		$sql.= ", ".MAIN_DB_PREFIX."societe as s";
2171
		if (!$user->rights->societe->client->voir && !$socid) $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
2172
		$sql.= " WHERE p.rowid = pd.fk_supplier_proposal";
2173
		$sql.= " AND p.fk_soc = s.rowid";
2174
		$sql.= " AND p.entity IN (".getEntity('supplier_proposal').")";
2175
		$sql.= " AND pd.fk_product = ".$this->id;
2176
		if (!$user->rights->societe->client->voir && !$socid) $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = " .$user->id;
2177
		//$sql.= " AND pr.fk_statut != 0";
2178
		if ($socid > 0)	$sql.= " AND p.fk_soc = ".$socid;
2179
2180
		$result = $this->db->query($sql);
2181
		if ( $result )
2182
		{
2183
			$obj=$this->db->fetch_object($result);
2184
			$this->stats_proposal_supplier['suppliers']=$obj->nb_suppliers;
2185
			$this->stats_proposal_supplier['nb']=$obj->nb;
2186
			$this->stats_proposal_supplier['rows']=$obj->nb_rows;
2187
			$this->stats_proposal_supplier['qty']=$obj->qty?$obj->qty:0;
2188
			return 1;
2189
		}
2190
		else
2191
		{
2192
			$this->error=$this->db->error();
2193
			return -1;
2194
		}
2195
	}
2196
2197
2198
	/**
2199
	 *  Charge tableau des stats commande client pour le produit/service
2200
	 *
2201
	 *  @param    int    $socid           Id societe pour filtrer sur une societe
2202
	 *  @param    string $filtrestatut    Id statut pour filtrer sur un statut
2203
	 *  @param    int    $forVirtualStock Ignore rights filter for virtual stock calculation.
2204
	 *  @return   array                  Array of stats (nb=nb of order, qty=qty ordered)
2205
	 */
2206
	function load_stats_commande($socid=0,$filtrestatut='', $forVirtualStock = 0)
2207
	{
2208
		global $conf,$user;
2209
2210
		$sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
2211
		$sql.= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
2212
		$sql.= " FROM ".MAIN_DB_PREFIX."commandedet as cd";
2213
		$sql.= ", ".MAIN_DB_PREFIX."commande as c";
2214
		$sql.= ", ".MAIN_DB_PREFIX."societe as s";
2215
		if (!$user->rights->societe->client->voir && !$socid && !$forVirtualStock) $sql.= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
2216
		$sql.= " WHERE c.rowid = cd.fk_commande";
2217
		$sql.= " AND c.fk_soc = s.rowid";
2218
		$sql.= " AND c.entity IN (".getEntity('commande').")";
2219
		$sql.= " AND cd.fk_product = ".$this->id;
2220
		if (!$user->rights->societe->client->voir && !$socid && !$forVirtualStock) $sql.= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = " .$user->id;
2221
		if ($socid > 0)	$sql.= " AND c.fk_soc = ".$socid;
2222
		if ($filtrestatut <> '') $sql.= " AND c.fk_statut in (".$filtrestatut.")";
2223
2224
		$result = $this->db->query($sql);
2225
		if ( $result )
2226
		{
2227
			$obj=$this->db->fetch_object($result);
2228
			$this->stats_commande['customers']=$obj->nb_customers;
2229
			$this->stats_commande['nb']=$obj->nb;
2230
			$this->stats_commande['rows']=$obj->nb_rows;
2231
			$this->stats_commande['qty']=$obj->qty?$obj->qty:0;
2232
2233
			// if it's a virtual product, maybe it is in order by extension
2234
			if (! empty($conf->global->ORDER_ADD_ORDERS_WITH_PARENT_PROD_IF_INCDEC))
2235
			{
2236
				$TFather = $this->getFather();
2237
				if (is_array($TFather) && !empty($TFather)) {
2238
					foreach($TFather as &$fatherData) {
2239
						$pFather = new Product($this->db);
2240
						$pFather->id = $fatherData['id'];
2241
						$qtyCoef = $fatherData['qty'];
2242
2243
						if ($fatherData['incdec']) {
2244
							$pFather->load_stats_commande($socid, $filtrestatut);
2245
2246
							$this->stats_commande['customers']+=$pFather->stats_commande['customers'];
2247
							$this->stats_commande['nb']+=$pFather->stats_commande['nb'];
2248
							$this->stats_commande['rows']+=$pFather->stats_commande['rows'];
2249
							$this->stats_commande['qty']+=$pFather->stats_commande['qty'] * $qtyCoef;
2250
2251
						}
2252
					}
2253
				}
2254
			}
2255
2256
			return 1;
2257
		}
2258
		else
2259
		{
2260
			$this->error=$this->db->error();
2261
			return -1;
2262
		}
2263
	}
2264
2265
	/**
2266
	 *  Charge tableau des stats commande fournisseur pour le produit/service
2267
	 *
2268
	 *  @param    int      $socid           Id societe pour filtrer sur une societe
2269
	 *  @param    string   $filtrestatut    Id des statuts pour filtrer sur des statuts
2270
	 *  @param    int      $forVirtualStock Ignore rights filter for virtual stock calculation.
2271
	 *  @return   array                     Tableau des stats
2272
	 */
2273
	function load_stats_commande_fournisseur($socid=0,$filtrestatut='', $forVirtualStock = 0)
2274
	{
2275
		global $conf,$user;
2276
2277
		$sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_suppliers, COUNT(DISTINCT c.rowid) as nb,";
2278
		$sql.= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
2279
		$sql.= " FROM ".MAIN_DB_PREFIX."commande_fournisseurdet as cd";
2280
		$sql.= ", ".MAIN_DB_PREFIX."commande_fournisseur as c";
2281
		$sql.= ", ".MAIN_DB_PREFIX."societe as s";
2282
		if (!$user->rights->societe->client->voir && !$socid && !$forVirtualStock) $sql.= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
2283
		$sql.= " WHERE c.rowid = cd.fk_commande";
2284
		$sql.= " AND c.fk_soc = s.rowid";
2285
		$sql.= " AND c.entity IN (".getEntity('supplier_order').")";
2286
		$sql.= " AND cd.fk_product = ".$this->id;
2287
		if (!$user->rights->societe->client->voir && !$socid && !$forVirtualStock) $sql.= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = " .$user->id;
2288
		if ($socid > 0) $sql.= " AND c.fk_soc = ".$socid;
2289
		if ($filtrestatut != '') $sql.= " AND c.fk_statut in (".$filtrestatut.")"; // Peut valoir 0
2290
2291
		$result = $this->db->query($sql);
2292
		if ( $result )
2293
		{
2294
			$obj=$this->db->fetch_object($result);
2295
			$this->stats_commande_fournisseur['suppliers']=$obj->nb_suppliers;
2296
			$this->stats_commande_fournisseur['nb']=$obj->nb;
2297
			$this->stats_commande_fournisseur['rows']=$obj->nb_rows;
2298
			$this->stats_commande_fournisseur['qty']=$obj->qty?$obj->qty:0;
2299
			return 1;
2300
		}
2301
		else
2302
		{
2303
			$this->error=$this->db->error().' sql='.$sql;
2304
			return -1;
2305
		}
2306
	}
2307
2308
	/**
2309
	 *  Charge tableau des stats expedition client pour le produit/service
2310
	 *
2311
	 *  @param    int    $socid           Id societe pour filtrer sur une societe
2312
	 *  @param    string $filtrestatut    Id statut pour filtrer sur un statut
2313
	 *  @param    int    $forVirtualStock Ignore rights filter for virtual stock calculation.
2314
	 *  @return   array                   Tableau des stats
2315
	 */
2316
	function load_stats_sending($socid=0,$filtrestatut='', $forVirtualStock = 0)
2317
	{
2318
		global $conf,$user;
2319
2320
		$sql = "SELECT COUNT(DISTINCT e.fk_soc) as nb_customers, COUNT(DISTINCT e.rowid) as nb,";
2321
		$sql.= " COUNT(ed.rowid) as nb_rows, SUM(ed.qty) as qty";
2322
		$sql.= " FROM ".MAIN_DB_PREFIX."expeditiondet as ed";
2323
		$sql.= ", ".MAIN_DB_PREFIX."commandedet as cd";
2324
		$sql.= ", ".MAIN_DB_PREFIX."commande as c";
2325
		$sql.= ", ".MAIN_DB_PREFIX."expedition as e";
2326
		$sql.= ", ".MAIN_DB_PREFIX."societe as s";
2327
		if (!$user->rights->societe->client->voir && !$socid && !$forVirtualStock) $sql.= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
2328
		$sql.= " WHERE e.rowid = ed.fk_expedition";
2329
		$sql.= " AND c.rowid = cd.fk_commande";
2330
		$sql.= " AND e.fk_soc = s.rowid";
2331
		$sql.= " AND e.entity IN (".getEntity('expedition').")";
2332
		$sql.= " AND ed.fk_origin_line = cd.rowid";
2333
		$sql.= " AND cd.fk_product = ".$this->id;
2334
		if (!$user->rights->societe->client->voir && !$socid && !$forVirtualStock) $sql.= " AND e.fk_soc = sc.fk_soc AND sc.fk_user = " .$user->id;
2335
		if ($socid > 0)	$sql.= " AND e.fk_soc = ".$socid;
2336
		if ($filtrestatut <> '') $sql.= " AND c.fk_statut in (".$filtrestatut.")";
2337
2338
		$result = $this->db->query($sql);
2339
		if ( $result )
2340
		{
2341
			$obj=$this->db->fetch_object($result);
2342
			$this->stats_expedition['customers']=$obj->nb_customers;
2343
			$this->stats_expedition['nb']=$obj->nb;
2344
			$this->stats_expedition['rows']=$obj->nb_rows;
2345
			$this->stats_expedition['qty']=$obj->qty?$obj->qty:0;
2346
			return 1;
2347
		}
2348
		else
2349
		{
2350
			$this->error=$this->db->error();
2351
			return -1;
2352
		}
2353
	}
2354
2355
	/**
2356
	 *  Charge tableau des stats réception fournisseur pour le produit/service
2357
	 *
2358
	 *  @param    int    $socid           Id societe pour filtrer sur une societe
2359
	 *  @param    string $filtrestatut    Id statut pour filtrer sur un statut
2360
	 *  @param    int    $forVirtualStock Ignore rights filter for virtual stock calculation.
2361
	 *  @return   array                   Tableau des stats
2362
	 */
2363
	function load_stats_reception($socid=0,$filtrestatut='', $forVirtualStock = 0)
2364
	{
2365
		global $conf,$user;
2366
2367
		$sql = "SELECT COUNT(DISTINCT cf.fk_soc) as nb_customers, COUNT(DISTINCT cf.rowid) as nb,";
2368
		$sql.= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
2369
		$sql.= " FROM ".MAIN_DB_PREFIX."commande_fournisseur_dispatch as fd";
2370
		$sql.= ", ".MAIN_DB_PREFIX."commande_fournisseur as cf";
2371
		$sql.= ", ".MAIN_DB_PREFIX."societe as s";
2372
		if (!$user->rights->societe->client->voir && !$socid && !$forVirtualStock) $sql.= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
2373
		$sql.= " WHERE cf.rowid = fd.fk_commande";
2374
		$sql.= " AND cf.fk_soc = s.rowid";
2375
		$sql.= " AND cf.entity IN (".getEntity('supplier_order').")";
2376
		$sql.= " AND fd.fk_product = ".$this->id;
2377
		if (!$user->rights->societe->client->voir && !$socid && !$forVirtualStock) $sql.= " AND cf.fk_soc = sc.fk_soc AND sc.fk_user = " .$user->id;
2378
		if ($socid > 0)	$sql.= " AND cf.fk_soc = ".$socid;
2379
		if ($filtrestatut <> '') $sql.= " AND cf.fk_statut in (".$filtrestatut.")";
2380
2381
		$result = $this->db->query($sql);
2382
		if ( $result )
2383
		{
2384
			$obj=$this->db->fetch_object($result);
2385
			$this->stats_reception['suppliers']=$obj->nb_customers;
2386
			$this->stats_reception['nb']=$obj->nb;
2387
			$this->stats_reception['rows']=$obj->nb_rows;
2388
			$this->stats_reception['qty']=$obj->qty?$obj->qty:0;
2389
			return 1;
2390
		}
2391
		else
2392
		{
2393
			$this->error=$this->db->error();
2394
			return -1;
2395
		}
2396
	}
2397
2398
	/**
2399
	 *  Charge tableau des stats contrat pour le produit/service
2400
	 *
2401
	 *  @param    int	$socid      Id societe
2402
	 *  @return   array       		Tableau des stats
2403
	 */
2404
	function load_stats_contrat($socid=0)
2405
	{
2406
		global $conf;
2407
		global $user;
2408
2409
		$sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
2410
		$sql.= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
2411
		$sql.= " FROM ".MAIN_DB_PREFIX."contratdet as cd";
2412
		$sql.= ", ".MAIN_DB_PREFIX."contrat as c";
2413
		$sql.= ", ".MAIN_DB_PREFIX."societe as s";
2414
		if (!$user->rights->societe->client->voir && !$socid) $sql.= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
2415
		$sql.= " WHERE c.rowid = cd.fk_contrat";
2416
		$sql.= " AND c.fk_soc = s.rowid";
2417
		$sql.= " AND c.entity IN (".getEntity('contract').")";
2418
		$sql.= " AND cd.fk_product = ".$this->id;
2419
		if (!$user->rights->societe->client->voir && !$socid) $sql.= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = " .$user->id;
2420
		//$sql.= " AND c.statut != 0";
2421
		if ($socid > 0)	$sql.= " AND c.fk_soc = ".$socid;
2422
2423
		$result = $this->db->query($sql);
2424
		if ( $result )
2425
		{
2426
			$obj=$this->db->fetch_object($result);
2427
			$this->stats_contrat['customers']=$obj->nb_customers;
2428
			$this->stats_contrat['nb']=$obj->nb;
2429
			$this->stats_contrat['rows']=$obj->nb_rows;
2430
			$this->stats_contrat['qty']=$obj->qty?$obj->qty:0;
2431
			return 1;
2432
		}
2433
		else
2434
		{
2435
			$this->error=$this->db->error().' sql='.$sql;
2436
			return -1;
2437
		}
2438
	}
2439
2440
	/**
2441
	 *  Charge tableau des stats facture pour le produit/service
2442
	 *
2443
	 *  @param    int		$socid      Id societe
2444
	 *  @return   array       			Tableau des stats
2445
	 */
2446
	function load_stats_facture($socid=0)
2447
	{
2448
		global $conf;
2449
		global $user;
2450
2451
		$sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
2452
		$sql.= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
2453
		$sql.= " FROM ".MAIN_DB_PREFIX."facturedet as fd";
2454
		$sql.= ", ".MAIN_DB_PREFIX."facture as f";
2455
		$sql.= ", ".MAIN_DB_PREFIX."societe as s";
2456
		if (!$user->rights->societe->client->voir && !$socid) $sql.= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
2457
		$sql.= " WHERE f.rowid = fd.fk_facture";
2458
		$sql.= " AND f.fk_soc = s.rowid";
2459
		$sql.= " AND f.entity IN (".getEntity('facture').")";
2460
		$sql.= " AND fd.fk_product = ".$this->id;
2461
		if (!$user->rights->societe->client->voir && !$socid) $sql.= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = " .$user->id;
2462
		//$sql.= " AND f.fk_statut != 0";
2463
		if ($socid > 0)	$sql .= " AND f.fk_soc = ".$socid;
2464
2465
		$result = $this->db->query($sql);
2466
		if ( $result )
2467
		{
2468
			$obj=$this->db->fetch_object($result);
2469
			$this->stats_facture['customers']=$obj->nb_customers;
2470
			$this->stats_facture['nb']=$obj->nb;
2471
			$this->stats_facture['rows']=$obj->nb_rows;
2472
			$this->stats_facture['qty']=$obj->qty?$obj->qty:0;
2473
			return 1;
2474
		}
2475
		else
2476
		{
2477
			$this->error=$this->db->error();
2478
			return -1;
2479
		}
2480
	}
2481
2482
	/**
2483
	 *  Charge tableau des stats facture pour le produit/service
2484
	 *
2485
	 *  @param    int		$socid      Id societe
2486
	 *  @return   array       			Tableau des stats
2487
	 */
2488
	function load_stats_facture_fournisseur($socid=0)
2489
	{
2490
		global $conf;
2491
		global $user;
2492
2493
		$sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,";
2494
		$sql.= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
2495
		$sql.= " FROM ".MAIN_DB_PREFIX."facture_fourn_det as fd";
2496
		$sql.= ", ".MAIN_DB_PREFIX."facture_fourn as f";
2497
		$sql.= ", ".MAIN_DB_PREFIX."societe as s";
2498
		if (!$user->rights->societe->client->voir && !$socid) $sql.= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
2499
		$sql.= " WHERE f.rowid = fd.fk_facture_fourn";
2500
		$sql.= " AND f.fk_soc = s.rowid";
2501
		$sql.= " AND f.entity IN (".getEntity('facture_fourn').")";
2502
		$sql.= " AND fd.fk_product = ".$this->id;
2503
		if (!$user->rights->societe->client->voir && !$socid) $sql.= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = " .$user->id;
2504
		//$sql.= " AND f.fk_statut != 0";
2505
		if ($socid > 0)	$sql .= " AND f.fk_soc = ".$socid;
2506
2507
		$result = $this->db->query($sql);
2508
		if ( $result )
2509
		{
2510
			$obj=$this->db->fetch_object($result);
2511
			$this->stats_facture_fournisseur['suppliers']=$obj->nb_suppliers;
2512
			$this->stats_facture_fournisseur['nb']=$obj->nb;
2513
			$this->stats_facture_fournisseur['rows']=$obj->nb_rows;
2514
			$this->stats_facture_fournisseur['qty']=$obj->qty?$obj->qty:0;
2515
			return 1;
2516
		}
2517
		else
2518
		{
2519
			$this->error=$this->db->error();
2520
			return -1;
2521
		}
2522
	}
2523
2524
	/**
2525
	 *  Return an array formated for showing graphs
2526
	 *
2527
	 *  @param		string	$sql        Request to execute
2528
	 *  @param		string	$mode		'byunit'=number of unit, 'bynumber'=nb of entities
2529
	 *  @param      int     $year       Year (0=current year)
2530
	 *  @return   	array       		<0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
2531
	 */
2532
	function _get_stats($sql, $mode, $year=0)
2533
	{
2534
		$resql = $this->db->query($sql);
2535
		if ($resql)
2536
		{
2537
			$num = $this->db->num_rows($resql);
2538
			$i = 0;
2539
			while ($i < $num)
2540
			{
2541
				$arr = $this->db->fetch_array($resql);
2542
				if ($mode == 'byunit')   $tab[$arr[1]] = $arr[0];	// 1st field
0 ignored issues
show
Coding Style Comprehensibility introduced by
$tab was never initialized. Although not strictly required by PHP, it is generally a good practice to add $tab = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
2543
				if ($mode == 'bynumber') $tab[$arr[1]] = $arr[2];	// 3rd field
0 ignored issues
show
Bug introduced by
The variable $tab does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2544
				$i++;
2545
			}
2546
		}
2547
		else
2548
		{
2549
			$this->error=$this->db->error().' sql='.$sql;
2550
			return -1;
2551
		}
2552
2553
		if (empty($year))
2554
		{
2555
		    $year = strftime('%Y',time());
2556
		    $month = strftime('%m',time());
2557
		}
2558
		else
2559
		{
2560
		    $month=12;    // We imagine we are at end of year, so we get last 12 month before, so all correct year.
2561
		}
2562
		$result = array();
2563
2564
		for ($j = 0 ; $j < 12 ; $j++)
2565
		{
2566
			$idx=ucfirst(dol_trunc(dol_print_date(dol_mktime(12,0,0,$month,1,$year),"%b"),3,'right','UTF-8',1));
2567
			$monthnum=sprintf("%02s",$month);
2568
2569
			$result[$j] = array($idx,isset($tab[$year.$month])?$tab[$year.$month]:0);
2570
			//            $result[$j] = array($monthnum,isset($tab[$year.$month])?$tab[$year.$month]:0);
2571
2572
			$month = "0".($month - 1);
2573
			if (dol_strlen($month) == 3)
2574
			{
2575
				$month = substr($month,1);
2576
			}
2577
			if ($month == 0)
2578
			{
2579
				$month = 12;
2580
				$year = $year - 1;
2581
			}
2582
		}
2583
2584
		return array_reverse($result);
2585
	}
2586
2587
2588
	/**
2589
	 *  Return nb of units or customers invoices in which product is included
2590
	 *
2591
	 *  @param  	int		$socid                   Limit count on a particular third party id
2592
	 *  @param		string	$mode		             'byunit'=number of unit, 'bynumber'=nb of entities
2593
	 *  @param      int     $filteronproducttype     0=To filter on product only, 1=To filter on services only
2594
	 *  @param      int     $year                    Year (0=last 12 month)
2595
	 *  @param      string  $morefilter              More sql filters
2596
	 * 	@return   	array       		             <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
2597
	 */
2598
	function get_nb_vente($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
2599
	{
2600
		global $conf;
2601
		global $user;
2602
2603
		$sql = "SELECT sum(d.qty), date_format(f.datef, '%Y%m')";
2604
		if ($mode == 'bynumber') $sql.= ", count(DISTINCT f.rowid)";
2605
		$sql.= " FROM ".MAIN_DB_PREFIX."facturedet as d, ".MAIN_DB_PREFIX."facture as f, ".MAIN_DB_PREFIX."societe as s";
2606
		if ($filteronproducttype >= 0) $sql.=", ".MAIN_DB_PREFIX."product as p";
2607
		if (!$user->rights->societe->client->voir && !$socid) $sql.= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
2608
		$sql.= " WHERE f.rowid = d.fk_facture";
2609
		if ($this->id > 0) $sql.= " AND d.fk_product =".$this->id;
2610
		else $sql.=" AND d.fk_product > 0";
2611
		if ($filteronproducttype >= 0) $sql.= " AND p.rowid = d.fk_product AND p.fk_product_type =".$filteronproducttype;
2612
		$sql.= " AND f.fk_soc = s.rowid";
2613
		$sql.= " AND f.entity IN (".getEntity('facture').")";
2614
		if (!$user->rights->societe->client->voir && !$socid) $sql.= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = " .$user->id;
2615
		if ($socid > 0)	$sql.= " AND f.fk_soc = $socid";
2616
		$sql.=$morefilter;
2617
		$sql.= " GROUP BY date_format(f.datef,'%Y%m')";
2618
		$sql.= " ORDER BY date_format(f.datef,'%Y%m') DESC";
2619
2620
		return $this->_get_stats($sql,$mode, $year);
2621
	}
2622
2623
2624
	/**
2625
	 *  Return nb of units or supplier invoices in which product is included
2626
	 *
2627
	 *  @param  	int		$socid                   Limit count on a particular third party id
2628
	 * 	@param		string	$mode		             'byunit'=number of unit, 'bynumber'=nb of entities
2629
	 *  @param      int     $filteronproducttype     0=To filter on product only, 1=To filter on services only
2630
	 *  @param      int     $year                    Year (0=last 12 month)
2631
	 *  @param      string  $morefilter              More sql filters
2632
	 * 	@return   	array       		             <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
2633
	 */
2634
	function get_nb_achat($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
2635
	{
2636
		global $conf;
2637
		global $user;
2638
2639
		$sql = "SELECT sum(d.qty), date_format(f.datef, '%Y%m')";
2640
		if ($mode == 'bynumber') $sql.= ", count(DISTINCT f.rowid)";
2641
		$sql.= " FROM ".MAIN_DB_PREFIX."facture_fourn_det as d, ".MAIN_DB_PREFIX."facture_fourn as f, ".MAIN_DB_PREFIX."societe as s";
2642
        if ($filteronproducttype >= 0) $sql.=", ".MAIN_DB_PREFIX."product as p";
2643
        if (!$user->rights->societe->client->voir && !$socid) $sql.= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
2644
		$sql.= " WHERE f.rowid = d.fk_facture_fourn";
2645
		if ($this->id > 0) $sql.= " AND d.fk_product =".$this->id;
2646
		else $sql.=" AND d.fk_product > 0";
2647
		if ($filteronproducttype >= 0) $sql.= " AND p.rowid = d.fk_product AND p.fk_product_type =".$filteronproducttype;
2648
		$sql.= " AND f.fk_soc = s.rowid";
2649
		$sql.= " AND f.entity IN (".getEntity('facture_fourn').")";
2650
		if (!$user->rights->societe->client->voir && !$socid) $sql.= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = " .$user->id;
2651
		if ($socid > 0)	$sql.= " AND f.fk_soc = $socid";
2652
		$sql.=$morefilter;
2653
		$sql.= " GROUP BY date_format(f.datef,'%Y%m')";
2654
		$sql.= " ORDER BY date_format(f.datef,'%Y%m') DESC";
2655
2656
		return $this->_get_stats($sql,$mode, $year);
2657
	}
2658
2659
	/**
2660
	 *  Return nb of units or proposals in which product is included
2661
	 *
2662
	 *  @param  	int		$socid                   Limit count on a particular third party id
2663
	 * 	@param		string	$mode		             'byunit'=number of unit, 'bynumber'=nb of entities
2664
	 *  @param      int     $filteronproducttype     0=To filter on product only, 1=To filter on services only
2665
	 *  @param      int     $year                    Year (0=last 12 month)
2666
	 *  @param      string  $morefilter              More sql filters
2667
	 * 	@return   	array       		             <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
2668
	 */
2669
	function get_nb_propal($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
2670
	{
2671
		global $conf;
2672
		global $user;
2673
2674
		$sql = "SELECT sum(d.qty), date_format(p.datep, '%Y%m')";
2675
		if ($mode == 'bynumber') $sql.= ", count(DISTINCT p.rowid)";
2676
		$sql.= " FROM ".MAIN_DB_PREFIX."propaldet as d, ".MAIN_DB_PREFIX."propal as p, ".MAIN_DB_PREFIX."societe as s";
2677
        if ($filteronproducttype >= 0) $sql.=", ".MAIN_DB_PREFIX."product as prod";
2678
		if (!$user->rights->societe->client->voir && !$socid) $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
2679
		$sql.= " WHERE p.rowid = d.fk_propal";
2680
		if ($this->id > 0) $sql.= " AND d.fk_product =".$this->id;
2681
		else $sql.=" AND d.fk_product > 0";
2682
		if ($filteronproducttype >= 0) $sql.= " AND prod.rowid = d.fk_product AND prod.fk_product_type =".$filteronproducttype;
2683
		$sql.= " AND p.fk_soc = s.rowid";
2684
		$sql.= " AND p.entity IN (".getEntity('propal').")";
2685
		if (!$user->rights->societe->client->voir && !$socid) $sql.= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = " .$user->id;
2686
		if ($socid > 0)	$sql.= " AND p.fk_soc = ".$socid;
2687
		$sql.=$morefilter;
2688
		$sql.= " GROUP BY date_format(p.datep,'%Y%m')";
2689
		$sql.= " ORDER BY date_format(p.datep,'%Y%m') DESC";
2690
2691
		return $this->_get_stats($sql,$mode, $year);
2692
	}
2693
2694
	/**
2695
	 *  Return nb of units or proposals in which product is included
2696
	 *
2697
	 *  @param  	int		$socid                   Limit count on a particular third party id
2698
	 * 	@param		string	$mode		             'byunit'=number of unit, 'bynumber'=nb of entities
2699
	 *  @param      int     $filteronproducttype     0=To filter on product only, 1=To filter on services only
2700
	 *  @param      int     $year                    Year (0=last 12 month)
2701
	 *  @param      string  $morefilter              More sql filters
2702
	 * 	@return   	array       		             <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
2703
	 */
2704
	function get_nb_propalsupplier($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
2705
	{
2706
		global $conf;
2707
		global $user;
2708
2709
		$sql = "SELECT sum(d.qty), date_format(p.date_valid, '%Y%m')";
2710
		if ($mode == 'bynumber') $sql.= ", count(DISTINCT p.rowid)";
2711
		$sql.= " FROM ".MAIN_DB_PREFIX."supplier_proposaldet as d, ".MAIN_DB_PREFIX."supplier_proposal as p, ".MAIN_DB_PREFIX."societe as s";
2712
        if ($filteronproducttype >= 0) $sql.=", ".MAIN_DB_PREFIX."product as prod";
2713
		if (!$user->rights->societe->client->voir && !$socid) $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
2714
		$sql.= " WHERE p.rowid = d.fk_supplier_proposal";
2715
		if ($this->id > 0) $sql.= " AND d.fk_product =".$this->id;
2716
		else $sql.=" AND d.fk_product > 0";
2717
		if ($filteronproducttype >= 0) $sql.= " AND prod.rowid = d.fk_product AND prod.fk_product_type =".$filteronproducttype;
2718
		$sql.= " AND p.fk_soc = s.rowid";
2719
		$sql.= " AND p.entity IN (".getEntity('propal').")";
2720
		if (!$user->rights->societe->client->voir && !$socid) $sql.= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = " .$user->id;
2721
		if ($socid > 0)	$sql.= " AND p.fk_soc = ".$socid;
2722
		$sql.=$morefilter;
2723
		$sql.= " GROUP BY date_format(p.date_valid,'%Y%m')";
2724
		$sql.= " ORDER BY date_format(p.date_valid,'%Y%m') DESC";
2725
2726
		return $this->_get_stats($sql,$mode, $year);
2727
	}
2728
2729
	/**
2730
	 *  Return nb of units or orders in which product is included
2731
	 *
2732
	 *  @param  	int		$socid                   Limit count on a particular third party id
2733
	 *  @param		string	$mode		             'byunit'=number of unit, 'bynumber'=nb of entities
2734
	 *  @param      int     $filteronproducttype     0=To filter on product only, 1=To filter on services only
2735
	 *  @param      int     $year                    Year (0=last 12 month)
2736
	 *  @param      string  $morefilter              More sql filters
2737
	 * 	@return   	array       		             <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
2738
	 */
2739
	function get_nb_order($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
2740
	{
2741
		global $conf, $user;
2742
2743
		$sql = "SELECT sum(d.qty), date_format(c.date_commande, '%Y%m')";
2744
		if ($mode == 'bynumber') $sql.= ", count(DISTINCT c.rowid)";
2745
		$sql.= " FROM ".MAIN_DB_PREFIX."commandedet as d, ".MAIN_DB_PREFIX."commande as c, ".MAIN_DB_PREFIX."societe as s";
2746
        if ($filteronproducttype >= 0) $sql.=", ".MAIN_DB_PREFIX."product as p";
2747
        if (!$user->rights->societe->client->voir && !$socid) $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
2748
		$sql.= " WHERE c.rowid = d.fk_commande";
2749
		if ($this->id > 0) $sql.= " AND d.fk_product =".$this->id;
2750
		else $sql.=" AND d.fk_product > 0";
2751
		if ($filteronproducttype >= 0) $sql.= " AND p.rowid = d.fk_product AND p.fk_product_type =".$filteronproducttype;
2752
		$sql.= " AND c.fk_soc = s.rowid";
2753
		$sql.= " AND c.entity IN (".getEntity('commande').")";
2754
		if (!$user->rights->societe->client->voir && !$socid) $sql.= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = " .$user->id;
2755
		if ($socid > 0)	$sql.= " AND c.fk_soc = ".$socid;
2756
		$sql.=$morefilter;
2757
		$sql.= " GROUP BY date_format(c.date_commande,'%Y%m')";
2758
		$sql.= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
2759
2760
		return $this->_get_stats($sql,$mode, $year);
2761
	}
2762
2763
	/**
2764
	 *  Return nb of units or orders in which product is included
2765
	 *
2766
	 *  @param  	int		$socid                   Limit count on a particular third party id
2767
	 *  @param		string	$mode		             'byunit'=number of unit, 'bynumber'=nb of entities
2768
	 *  @param      int     $filteronproducttype     0=To filter on product only, 1=To filter on services only
2769
	 *  @param      int     $year                    Year (0=last 12 month)
2770
	 *  @param      string  $morefilter              More sql filters
2771
	 * 	@return   	array       		             <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11
2772
	 */
2773
	function get_nb_ordersupplier($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
2774
	{
2775
		global $conf, $user;
2776
2777
		$sql = "SELECT sum(d.qty), date_format(c.date_commande, '%Y%m')";
2778
		if ($mode == 'bynumber') $sql.= ", count(DISTINCT c.rowid)";
2779
		$sql.= " FROM ".MAIN_DB_PREFIX."commande_fournisseurdet as d, ".MAIN_DB_PREFIX."commande_fournisseur as c, ".MAIN_DB_PREFIX."societe as s";
2780
        if ($filteronproducttype >= 0) $sql.=", ".MAIN_DB_PREFIX."product as p";
2781
		if (!$user->rights->societe->client->voir && !$socid) $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
2782
		$sql.= " WHERE c.rowid = d.fk_commande";
2783
		if ($this->id > 0) $sql.= " AND d.fk_product =".$this->id;
2784
		else $sql.=" AND d.fk_product > 0";
2785
		if ($filteronproducttype >= 0) $sql.= " AND p.rowid = d.fk_product AND p.fk_product_type =".$filteronproducttype;
2786
		$sql.= " AND c.fk_soc = s.rowid";
2787
		$sql.= " AND c.entity IN (".getEntity('supplier_order').")";
2788
		if (!$user->rights->societe->client->voir && !$socid) $sql.= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = " .$user->id;
2789
		if ($socid > 0)	$sql.= " AND c.fk_soc = ".$socid;
2790
		$sql.=$morefilter;
2791
		$sql.= " GROUP BY date_format(c.date_commande,'%Y%m')";
2792
		$sql.= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
2793
2794
		return $this->_get_stats($sql,$mode, $year);
2795
	}
2796
2797
	/**
2798
	 *  Link a product/service to a parent product/service
2799
	 *
2800
	 *  @param      int	$id_pere    Id of parent product/service
2801
	 *  @param      int	$id_fils    Id of child product/service
2802
	 *  @param		int	$qty		Quantity
2803
	 *  @param		int	$incdec		1=Increase/decrease stock of child when parent stock increase/decrease
2804
	 *  @return     int        		< 0 if KO, > 0 if OK
2805
	 */
2806
	function add_sousproduit($id_pere, $id_fils, $qty, $incdec=1)
2807
	{
2808
		// Clean parameters
2809
		if (! is_numeric($id_pere)) $id_pere=0;
2810
		if (! is_numeric($id_fils)) $id_fils=0;
2811
		if (! is_numeric($incdec)) $incdec=0;
2812
2813
		$result=$this->del_sousproduit($id_pere, $id_fils);
2814
		if ($result < 0) return $result;
2815
2816
		// Check not already father of id_pere (to avoid father -> child -> father links)
2817
		$sql = 'SELECT fk_product_pere from '.MAIN_DB_PREFIX.'product_association';
2818
		$sql .= ' WHERE fk_product_pere  = '.$id_fils.' AND fk_product_fils = '.$id_pere;
2819
		if (! $this->db->query($sql))
2820
		{
2821
			dol_print_error($this->db);
2822
			return -1;
2823
		}
2824
		else
2825
		{
2826
			$result = $this->db->query($sql);
2827
			if ($result)
2828
			{
2829
				$num = $this->db->num_rows($result);
2830
				if($num > 0)
2831
				{
2832
					$this->error="isFatherOfThis";
2833
					return -1;
2834
				}
2835
				else
2836
				{
2837
					$sql = 'INSERT INTO '.MAIN_DB_PREFIX.'product_association(fk_product_pere,fk_product_fils,qty,incdec)';
2838
					$sql .= ' VALUES ('.$id_pere.', '.$id_fils.', '.$qty.', '.$incdec.')';
2839
					if (! $this->db->query($sql))
2840
					{
2841
						dol_print_error($this->db);
2842
						return -1;
2843
					}
2844
					else
2845
					{
2846
						return 1;
2847
					}
2848
				}
2849
			}
2850
		}
2851
	}
2852
2853
	/**
2854
	 *  Modify composed product
2855
	 *
2856
	 *  @param      int	$id_pere    Id of parent product/service
2857
	 *  @param      int	$id_fils    Id of child product/service
2858
	 *  @param		int	$qty		Quantity
2859
	 *  @param		int	$incdec		1=Increase/decrease stock of child when parent stock increase/decrease
2860
	 * 	@return     int        		< 0 if KO, > 0 if OK
2861
	 */
2862
	function update_sousproduit($id_pere, $id_fils, $qty, $incdec=1)
2863
	{
2864
		// Clean parameters
2865
		if (! is_numeric($id_pere)) $id_pere=0;
2866
		if (! is_numeric($id_fils)) $id_fils=0;
2867
		if (! is_numeric($incdec)) $incdec=1;
2868
		if (! is_numeric($qty)) $qty=1;
2869
2870
		$sql = 'UPDATE '.MAIN_DB_PREFIX.'product_association SET ';
2871
		$sql.= 'qty='.$qty;
2872
		$sql.= ',incdec='.$incdec;
2873
		$sql .= ' WHERE fk_product_pere='.$id_pere.' AND fk_product_fils='.$id_fils;
2874
2875
		if (!$this->db->query($sql))
2876
		{
2877
			dol_print_error($this->db);
2878
			return -1;
2879
		}
2880
		else
2881
		{
2882
			return 1;
2883
		}
2884
2885
	}
2886
2887
	/**
2888
	 *  Retire le lien entre un sousproduit et un produit/service
2889
	 *
2890
	 *  @param      int	$fk_parent		Id du produit auquel ne sera plus lie le produit lie
2891
	 *  @param      int	$fk_child		Id du produit a ne plus lie
2892
	 *  @return     int			    	< 0 if KO, > 0 if OK
2893
	 */
2894
	function del_sousproduit($fk_parent, $fk_child)
2895
	{
2896
		if (! is_numeric($fk_parent)) $fk_parent=0;
2897
		if (! is_numeric($fk_child)) $fk_child=0;
2898
2899
		$sql = "DELETE FROM ".MAIN_DB_PREFIX."product_association";
2900
		$sql.= " WHERE fk_product_pere  = ".$fk_parent;
2901
		$sql.= " AND fk_product_fils = ".$fk_child;
2902
2903
		dol_syslog(get_class($this).'::del_sousproduit', LOG_DEBUG);
2904
		if (! $this->db->query($sql))
2905
		{
2906
			dol_print_error($this->db);
2907
			return -1;
2908
		}
2909
2910
		return 1;
2911
	}
2912
2913
	/**
2914
	 *  Verifie si c'est un sous-produit
2915
	 *
2916
	 *  @param      int	$fk_parent		Id du produit auquel le produit est lie
2917
	 *  @param      int	$fk_child		Id du produit lie
2918
	 *  @return     int			    	< 0 si erreur, > 0 si ok
2919
	 */
2920
	function is_sousproduit($fk_parent, $fk_child)
2921
	{
2922
		$sql = "SELECT fk_product_pere, qty, incdec";
2923
		$sql.= " FROM ".MAIN_DB_PREFIX."product_association";
2924
		$sql.= " WHERE fk_product_pere  = '".$fk_parent."'";
2925
		$sql.= " AND fk_product_fils = '".$fk_child."'";
2926
2927
		$result = $this->db->query($sql);
2928
		if ($result)
2929
		{
2930
			$num = $this->db->num_rows($result);
2931
2932
			if($num > 0)
2933
			{
2934
				$obj = $this->db->fetch_object($result);
2935
				$this->is_sousproduit_qty = $obj->qty;
2936
				$this->is_sousproduit_incdec = $obj->incdec;
2937
2938
				return true;
2939
			}
2940
			else
2941
			{
2942
				return false;
2943
			}
2944
		}
2945
		else
2946
		{
2947
			dol_print_error($this->db);
2948
			return -1;
2949
		}
2950
	}
2951
2952
2953
	/**
2954
	 *  Add a supplier price for the product.
2955
	 *  Note: Duplicate ref is accepted for different quantity only, or for different companies.
2956
	 *
2957
	 *  @param      User	$user       User that make link
2958
	 *  @param      int		$id_fourn   Supplier id
2959
	 *  @param      string	$ref_fourn  Supplier ref
2960
	 *  @param		float	$quantity	Quantity minimum for price
2961
	 *  @return     int         		< 0 if KO, 0 if link already exists for this product, > 0 if OK
2962
	 */
2963
	function add_fournisseur($user, $id_fourn, $ref_fourn, $quantity)
2964
	{
2965
		global $conf;
2966
2967
		$now=dol_now();
2968
2969
    	dol_syslog(get_class($this)."::add_fournisseur id_fourn = ".$id_fourn." ref_fourn=".$ref_fourn." quantity=".$quantity, LOG_DEBUG);
2970
2971
		if ($ref_fourn)
2972
		{
2973
    		$sql = "SELECT rowid, fk_product";
2974
    		$sql.= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
2975
    		$sql.= " WHERE fk_soc = ".$id_fourn;
2976
    		$sql.= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
2977
    		$sql.= " AND fk_product != ".$this->id;
2978
    		$sql.= " AND entity IN (".getEntity('productprice').")";
2979
2980
    		$resql=$this->db->query($sql);
2981
    		if ($resql)
2982
    		{
2983
    			$obj = $this->db->fetch_object($resql);
2984
                if ($obj)
2985
                {
2986
        			// If the supplier ref already exists but for another product (duplicate ref is accepted for different quantity only or different companies)
2987
                    $this->product_id_already_linked = $obj->fk_product;
2988
    				return -3;
2989
    			}
2990
                $this->db->free($resql);
2991
    		}
2992
		}
2993
2994
		$sql = "SELECT rowid";
2995
		$sql.= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
2996
		$sql.= " WHERE fk_soc = ".$id_fourn;
2997
		if ($ref_fourn) $sql.= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
2998
		else $sql.= " AND (ref_fourn = '' OR ref_fourn IS NULL)";
2999
		$sql.= " AND quantity = '".$quantity."'";
3000
		$sql.= " AND fk_product = ".$this->id;
3001
		$sql.= " AND entity IN (".getEntity('productprice').")";
3002
3003
		$resql=$this->db->query($sql);
3004
		if ($resql)
3005
		{
3006
    		$obj = $this->db->fetch_object($resql);
3007
3008
		    // The reference supplier does not exist, we create it for this product.
3009
			if (! $obj)
3010
			{
3011
				$sql = "INSERT INTO ".MAIN_DB_PREFIX."product_fournisseur_price(";
3012
				$sql.= "datec";
3013
				$sql.= ", entity";
3014
				$sql.= ", fk_product";
3015
				$sql.= ", fk_soc";
3016
				$sql.= ", ref_fourn";
3017
				$sql.= ", quantity";
3018
				$sql.= ", fk_user";
3019
				$sql.= ", tva_tx";
3020
				$sql.= ") VALUES (";
3021
				$sql.= "'".$this->db->idate($now)."'";
3022
				$sql.= ", ".$conf->entity;
3023
				$sql.= ", ".$this->id;
3024
				$sql.= ", ".$id_fourn;
3025
				$sql.= ", '".$this->db->escape($ref_fourn)."'";
3026
				$sql.= ", ".$quantity;
3027
				$sql.= ", ".$user->id;
3028
				$sql.= ", 0";
3029
				$sql.= ")";
3030
3031
				if ($this->db->query($sql))
3032
				{
3033
					$this->product_fourn_price_id = $this->db->last_insert_id(MAIN_DB_PREFIX."product_fournisseur_price");
3034
					return 1;
3035
				}
3036
				else
3037
				{
3038
					$this->error=$this->db->lasterror();
3039
					return -1;
3040
				}
3041
			}
3042
			// If the supplier price already exists for this product and quantity
3043
			else
3044
			{
3045
				$this->product_fourn_price_id = $obj->rowid;
3046
				return 0;
3047
			}
3048
		}
3049
		else
3050
		{
3051
			$this->error=$this->db->lasterror();
3052
			return -2;
3053
		}
3054
	}
3055
3056
3057
	/**
3058
	 *  Renvoie la liste des fournisseurs du produit/service
3059
	 *
3060
	 *  @return 	array		Tableau des id de fournisseur
3061
	 */
3062
	function list_suppliers()
3063
	{
3064
		global $conf;
3065
3066
		$list = array();
3067
3068
		$sql = "SELECT DISTINCT p.fk_soc";
3069
		$sql.= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price as p";
3070
		$sql.= " WHERE p.fk_product = ".$this->id;
3071
		$sql.= " AND p.entity = ".$conf->entity;
3072
3073
		$result = $this->db->query($sql);
3074
		if ($result)
3075
		{
3076
			$num = $this->db->num_rows($result);
3077
			$i=0;
3078
			while ($i < $num)
3079
			{
3080
				$obj = $this->db->fetch_object($result);
3081
				$list[$i] = $obj->fk_soc;
3082
				$i++;
3083
			}
3084
		}
3085
3086
		return $list;
3087
	}
3088
3089
	/**
3090
	 *  Recopie les prix d'un produit/service sur un autre
3091
	 *
3092
	 *  @param	int		$fromId     Id product source
3093
	 *  @param  int		$toId       Id product target
3094
	 *  @return nt         			< 0 if KO, > 0 if OK
3095
	 */
3096
	function clone_price($fromId, $toId)
3097
	{
3098
		$this->db->begin();
3099
3100
		// les prix
3101
		$sql = "INSERT ".MAIN_DB_PREFIX."product_price (";
3102
		$sql.= " fk_product, date_price, price, tva_tx, localtax1_tx, localtax2_tx, fk_user_author, tosell)";
3103
		$sql.= " SELECT ".$toId . ", date_price, price, tva_tx, localtax1_tx, localtax2_tx, fk_user_author, tosell";
3104
		$sql.= " FROM ".MAIN_DB_PREFIX."product_price ";
3105
		$sql.= " WHERE fk_product = ". $fromId;
3106
3107
		dol_syslog(get_class($this).'::clone_price', LOG_DEBUG);
3108
		if (! $this->db->query($sql))
3109
		{
3110
			$this->db->rollback();
3111
			return -1;
3112
		}
3113
		$this->db->commit();
3114
		return 1;
3115
	}
3116
3117
	/**
3118
	 * Clone links between products
3119
	 *
3120
	 * @param  int		$fromId		Product id
3121
	 * @param  int		$toId		Product id
3122
	 * @return int                  <0 if KO, >0 if OK
3123
	 */
3124
	function clone_associations($fromId, $toId)
3125
	{
3126
		$this->db->begin();
3127
3128
		$sql = 'INSERT INTO '.MAIN_DB_PREFIX.'product_association (fk_product_pere, fk_product_fils, qty)';
3129
		$sql.= " SELECT ".$toId.", fk_product_fils, qty FROM ".MAIN_DB_PREFIX."product_association";
3130
		$sql.= " WHERE fk_product_pere = ".$fromId;
3131
3132
		dol_syslog(get_class($this).'::clone_association', LOG_DEBUG);
3133
		if (! $this->db->query($sql))
3134
		{
3135
			$this->db->rollback();
3136
			return -1;
3137
		}
3138
3139
		$this->db->commit();
3140
		return 1;
3141
	}
3142
3143
	/**
3144
	 *  Recopie les fournisseurs et prix fournisseurs d'un produit/service sur un autre
3145
	 *
3146
	 *  @param    int	$fromId      Id produit source
3147
	 *  @param    int	$toId        Id produit cible
3148
	 *  @return   int    		     < 0 si erreur, > 0 si ok
3149
	 */
3150
	function clone_fournisseurs($fromId, $toId)
3151
	{
3152
		$this->db->begin();
3153
3154
		$now=dol_now();
3155
3156
		// les fournisseurs
3157
		/*$sql = "INSERT ".MAIN_DB_PREFIX."product_fournisseur ("
3158
		. " datec, fk_product, fk_soc, ref_fourn, fk_user_author )"
3159
		. " SELECT '".$this->db->idate($now)."', ".$toId.", fk_soc, ref_fourn, fk_user_author"
3160
		. " FROM ".MAIN_DB_PREFIX."product_fournisseur"
3161
		. " WHERE fk_product = ".$fromId;
3162
3163
		if ( ! $this->db->query($sql ) )
3164
		{
3165
			$this->db->rollback();
3166
			return -1;
3167
		}*/
3168
3169
		// les prix de fournisseurs.
3170
		$sql = "INSERT ".MAIN_DB_PREFIX."product_fournisseur_price (";
3171
		$sql.= " datec, fk_product, fk_soc, price, quantity, fk_user)";
3172
		$sql.= " SELECT '".$this->db->idate($now)."', ".$toId. ", fk_soc, price, quantity, fk_user";
3173
		$sql.= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
3174
		$sql.= " WHERE fk_product = ".$fromId;
3175
3176
		dol_syslog(get_class($this).'::clone_fournisseurs', LOG_DEBUG);
3177
		$resql=$this->db->query($sql);
3178
		if (! $resql)
3179
		{
3180
			$this->db->rollback();
3181
			return -1;
3182
		}
3183
		else
3184
		{
3185
		    $this->db->commit();
3186
		    return 1;
3187
		}
3188
	}
3189
3190
	/**
3191
	 *  Fonction recursive uniquement utilisee par get_arbo_each_prod, recompose l'arborescence des sousproduits
3192
	 * 	Define value of this->res
3193
	 *
3194
	 *	@param		array		$prod			Products array
3195
	 *	@param		string		$compl_path		Directory path of parents to add before
3196
	 *	@param		int			$multiply		Because each sublevel must be multiplicated by parent nb
3197
	 *	@param		int			$level			Init level
3198
	 *  @param		int			$id_parent		Id parent
3199
	 *  @return 	void
3200
	 */
3201
	function fetch_prod_arbo($prod, $compl_path="", $multiply=1, $level=1, $id_parent=0)
3202
	{
3203
		global $conf,$langs;
3204
3205
		$product = new Product($this->db);
3206
		//var_dump($prod);
3207
		foreach($prod as $id_product => $desc_pere)	// $id_product is 0 (first call starting with root top) or an id of a sub_product
3208
		{
3209
			if (is_array($desc_pere))	// If desc_pere is an array, this means it's a child
3210
			{
3211
				$id=(! empty($desc_pere[0]) ? $desc_pere[0] :'');
3212
				$nb=(! empty($desc_pere[1]) ? $desc_pere[1] :'');
3213
				$type=(! empty($desc_pere[2]) ? $desc_pere[2] :'');
3214
				$label=(! empty($desc_pere[3]) ? $desc_pere[3] :'');
3215
				$incdec=!empty($desc_pere[4]) ? $desc_pere[4] : 0;
3216
3217
				if ($multiply < 1) $multiply=1;
3218
3219
				//print "XXX We add id=".$id." - label=".$label." - nb=".$nb." - multiply=".$multiply." fullpath=".$compl_path.$label."\n";
3220
				$this->fetch($id);		// Load product
3221
				$this->load_stock('nobatch,novirtual');	// Load stock to get true this->stock_reel
3222
				$this->res[]= array(
3223
					'id'=>$id,					// Id product
3224
					'id_parent'=>$id_parent,
3225
					'ref'=>$this->ref,			// Ref product
3226
					'nb'=>$nb,					// Nb of units that compose parent product
3227
					'nb_total'=>$nb*$multiply,	// Nb of units for all nb of product
3228
					'stock'=>$this->stock_reel,	// Stock
3229
					'stock_alert'=>$this->seuil_stock_alerte,	// Stock alert
3230
					'label'=>$label,
3231
					'fullpath'=>$compl_path.$label,			// Label
3232
					'type'=>$type,				// Nb of units that compose parent product
3233
					'desiredstock'=>$this->desiredstock,
3234
					'level'=>$level,
3235
					'incdec'=>$incdec,
3236
					'entity'=>$this->entity
3237
				);
3238
3239
				// Recursive call if there is childs to child
3240
				if (is_array($desc_pere['childs']))
3241
				{
3242
					//print 'YYY We go down for '.$desc_pere[3]." -> \n";
3243
					$this->fetch_prod_arbo($desc_pere['childs'], $compl_path.$desc_pere[3]." -> ", $desc_pere[1]*$multiply, $level+1, $id);
3244
				}
3245
			}
3246
		}
3247
	}
3248
3249
	/**
3250
	 *  fonction recursive uniquement utilisee par get_each_prod, ajoute chaque sousproduits dans le tableau res
3251
	 *
3252
	 *	@param	array	$prod	Products array
3253
	 *  @return void
3254
	 */
3255
	function fetch_prods($prod)
3256
	{
3257
		$this->res;
3258
		foreach($prod as $nom_pere => $desc_pere)
3259
		{
3260
			// on est dans une sous-categorie
3261
			if(is_array($desc_pere))
3262
			$this->res[]= array($desc_pere[1],$desc_pere[0]);
3263
			if(count($desc_pere) >1)
3264
			{
3265
				$this->fetch_prods($desc_pere);
3266
			}
3267
		}
3268
	}
3269
3270
	/**
3271
	 *  reconstruit l'arborescence des produits sous la forme d'un tableau
3272
	 *
3273
	 *	@param		int		$multiply		Because each sublevel must be multiplicated by parent nb
3274
	 *  @return 	array 					$this->res
3275
	 */
3276
	function get_arbo_each_prod($multiply=1)
3277
	{
3278
		$this->res = array();
3279
		if (isset($this->sousprods) && is_array($this->sousprods))
3280
		{
3281
			foreach($this->sousprods as $prod_name => $desc_product)
3282
			{
3283
				if (is_array($desc_product)) $this->fetch_prod_arbo($desc_product,"",$multiply,1,$this->id);
3284
			}
3285
		}
3286
		//var_dump($this->res);
3287
		return $this->res;
3288
	}
3289
3290
	/**
3291
	 *  Renvoie tous les sousproduits dans le tableau res, chaque ligne de res contient : id -> qty
3292
	 *
3293
	 *  @return array $this->res
3294
	 */
3295
	function get_each_prod()
3296
	{
3297
		$this->res = array();
3298
		if (is_array($this->sousprods))
3299
		{
3300
			foreach($this->sousprods as $nom_pere => $desc_pere)
3301
			{
3302
				if (count($desc_pere) >1) $this->fetch_prods($desc_pere);
3303
			}
3304
			sort($this->res);
3305
		}
3306
		return $this->res;
3307
	}
3308
3309
3310
	/**
3311
	 *  Return all parent products for current product (first level only)
3312
	 *
3313
	 *  @return 	array 		Array of product
3314
	 */
3315
	function getFather()
3316
	{
3317
		$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";
3318
		$sql.= " FROM ".MAIN_DB_PREFIX."product_association as pa,";
3319
		$sql.= " ".MAIN_DB_PREFIX."product as p";
3320
		$sql.= " WHERE p.rowid = pa.fk_product_pere";
3321
		$sql.= " AND pa.fk_product_fils = ".$this->id;
3322
3323
		$res = $this->db->query($sql);
3324
		if ($res)
3325
		{
3326
			$prods = array ();
3327
			while ($record = $this->db->fetch_array($res))
3328
			{
3329
				// $record['id'] = $record['rowid'] = id of father
3330
				$prods[$record['id']]['id'] = $record['rowid'];
3331
				$prods[$record['id']]['ref'] = $record['ref'];
3332
				$prods[$record['id']]['label'] = $record['label'];
3333
				$prods[$record['id']]['qty'] = $record['qty'];
3334
				$prods[$record['id']]['incdec'] = $record['incdec'];
3335
				$prods[$record['id']]['fk_product_type'] =  $record['fk_product_type'];
3336
				$prods[$record['id']]['entity'] =  $record['entity'];
3337
			}
3338
			return $prods;
3339
		}
3340
		else
3341
		{
3342
			dol_print_error($this->db);
3343
			return -1;
3344
		}
3345
	}
3346
3347
3348
	/**
3349
	 *  Return childs of product $id
3350
	 *
3351
	 * 	@param		int		$id					Id of product to search childs of
3352
	 *  @param		int		$firstlevelonly		Return only direct child
3353
	 *  @param		int		$level				Level of recursing call (start to 1)
3354
	 *  @return     array       				Prod
3355
	 */
3356
	function getChildsArbo($id, $firstlevelonly=0, $level=1)
3357
	{
3358
		global $alreadyfound;
3359
3360
		$sql = "SELECT p.rowid, p.label as label, pa.qty as qty, pa.fk_product_fils as id, p.fk_product_type, pa.incdec";
3361
		$sql.= " FROM ".MAIN_DB_PREFIX."product as p";
3362
		$sql.= ", ".MAIN_DB_PREFIX."product_association as pa";
3363
		$sql.= " WHERE p.rowid = pa.fk_product_fils";
3364
		$sql.= " AND pa.fk_product_pere = ".$id;
3365
		$sql.= " AND pa.fk_product_fils != ".$id;	// This should not happens, it is to avoid infinite loop if it happens
3366
3367
		dol_syslog(get_class($this).'::getChildsArbo id='.$id.' level='.$level, LOG_DEBUG);
3368
3369
		if ($level == 1) $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
3370
		// Protection against infinite loop
3371
		if ($level > 30) return array();
3372
3373
		$res  = $this->db->query($sql);
3374
		if ($res)
3375
		{
3376
			$prods = array();
3377
			while ($rec = $this->db->fetch_array($res))
3378
			{
3379
				if (! empty($alreadyfound[$rec['rowid']]))
3380
				{
3381
					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);
3382
					continue;
3383
				}
3384
				$alreadyfound[$rec['rowid']]=1;
3385
				$prods[$rec['rowid']]= array(
3386
					0=>$rec['rowid'],
3387
					1=>$rec['qty'],
3388
					2=>$rec['fk_product_type'],
3389
					3=>$this->db->escape($rec['label']),
3390
					4=>$rec['incdec']
3391
				);
3392
				//$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty'],2=>$rec['fk_product_type']);
3393
				//$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty']);
3394
				if (empty($firstlevelonly))
3395
				{
3396
					$listofchilds=$this->getChildsArbo($rec['rowid'], 0, $level + 1);
3397
					foreach($listofchilds as $keyChild => $valueChild)
3398
					{
3399
						$prods[$rec['rowid']]['childs'][$keyChild] = $valueChild;
3400
					}
3401
				}
3402
			}
3403
3404
			return $prods;
3405
		}
3406
		else
3407
		{
3408
			dol_print_error($this->db);
3409
			return -1;
3410
		}
3411
	}
3412
3413
	/**
3414
	 * 	Return tree of all subproducts for product. Tree contains id, name and quantity.
3415
	 * 	Set this->sousprods
3416
	 *
3417
	 *  @return    	void
3418
	 */
3419
	function get_sousproduits_arbo()
3420
	{
3421
	    $parent=array();
3422
3423
		foreach($this->getChildsArbo($this->id) as $keyChild => $valueChild)	// Warning. getChildsArbo can call getChildsArbo recursively. Starting point is $value[0]=id of product
3424
		{
3425
			$parent[$this->label][$keyChild] = $valueChild;
3426
		}
3427
		foreach($parent as $key => $value)		// key=label, value is array of childs
3428
		{
3429
			$this->sousprods[$key] = $value;
3430
		}
3431
	}
3432
3433
	/**
3434
	 *	Return clicable link of object (with eventually picto)
3435
	 *
3436
	 *	@param		int		$withpicto					Add picto into link
3437
	 *	@param		string	$option						Where point the link ('stock', 'composition', 'category', 'supplier', '')
3438
	 *	@param		int		$maxlength					Maxlength of ref
3439
     *  @param      int     $save_lastsearch_value		-1=Auto, 0=No save of lastsearch_values when clicking, 1=Save lastsearch_values whenclicking
3440
	 *	@return		string								String with URL
3441
	 */
3442
	function getNomUrl($withpicto=0, $option='', $maxlength=0, $save_lastsearch_value=-1)
3443
	{
3444
		global $conf, $langs, $hookmanager;
3445
		include_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
3446
3447
		$result='';
3448
        $newref=$this->ref;
3449
        if ($maxlength) $newref=dol_trunc($newref,$maxlength,'middle');
3450
3451
        if ($this->type == Product::TYPE_PRODUCT) $label = '<u>' . $langs->trans("ShowProduct") . '</u>';
3452
        if ($this->type == Product::TYPE_SERVICE) $label = '<u>' . $langs->trans("ShowService") . '</u>';
3453
        if (! empty($this->ref))
3454
            $label .= '<br><b>' . $langs->trans('ProductRef') . ':</b> ' . $this->ref;
0 ignored issues
show
Bug introduced by
The variable $label does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
3455
        if (! empty($this->label))
3456
            $label .= '<br><b>' . $langs->trans('ProductLabel') . ':</b> ' . $this->label;
3457
3458
        if ($this->type == Product::TYPE_PRODUCT)
3459
        {
3460
            if ($this->weight)  $label.="<br><b>".$langs->trans("Weight").'</b>: '.$this->weight.' '.measuring_units_string($this->weight_units,"weight");
3461
    		if ($this->length)  $label.="<br><b>".$langs->trans("Length").'</b>: '.$this->length.' '.measuring_units_string($this->length_units,'length');
3462
    		if ($this->surface) $label.="<br><b>".$langs->trans("Surface").'</b>: '.$this->surface.' '.measuring_units_string($this->surface_units,'surface');
3463
    		if ($this->volume)  $label.="<br><b>".$langs->trans("Volume").'</b>: '.$this->volume.' '.measuring_units_string($this->volume_units,'volume');
3464
            if (! empty($conf->productbatch->enabled))
3465
            {
3466
                $label.="<br><b>".$langs->trans("ManageLotSerial").'</b>: '.$this->getLibStatut(0,2);
3467
            }
3468
        }
3469
        if ($this->type == Product::TYPE_SERVICE)
3470
        {
3471
            //
3472
        }
3473
        if (! empty($this->entity))
3474
        {
3475
            $tmpphoto = $this->show_photos($conf->product->multidir_output[$this->entity],1,1,0,0,0,80);
3476
            if ($this->nbphoto > 0) $label .= '<br>' . $tmpphoto;
3477
        }
3478
3479
		$linkclose='';
3480
		if (empty($notooltip))
0 ignored issues
show
Bug introduced by
The variable $notooltip seems to never exist, and therefore empty should always return true. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
3481
		{
3482
		    if (! empty($conf->global->MAIN_OPTIMIZEFORTEXTBROWSER))
3483
		    {
3484
		        $label=$langs->trans("ShowOrder");
3485
		        $linkclose.=' alt="'.dol_escape_htmltag($label, 1).'"';
3486
		    }
3487
3488
		    $linkclose.= ' title="'.dol_escape_htmltag($label, 1, 1).'"';
3489
		    $linkclose.= ' class="classfortooltip"';
3490
3491
		    if (! is_object($hookmanager))
3492
	        {
3493
	            include_once DOL_DOCUMENT_ROOT.'/core/class/hookmanager.class.php';
3494
	            $hookmanager=new HookManager($this->db);
3495
	        }
3496
	        $hookmanager->initHooks(array('productdao'));
3497
	        $parameters=array('id'=>$this->id);
3498
	        $reshook=$hookmanager->executeHooks('getnomurltooltip',$parameters,$this,$action);    // Note that $action and $object may have been modified by some hooks
0 ignored issues
show
Bug introduced by
The variable $action does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
3499
	        if ($reshook > 0) $linkclose = $hookmanager->resPrint;
3500
		}
3501
3502
        if ($option == 'supplier' || $option == 'category') {
3503
            $url = DOL_URL_ROOT.'/product/fournisseurs.php?id='.$this->id;
3504
        } else if ($option == 'stock') {
3505
            $url = DOL_URL_ROOT.'/product/stock/product.php?id='.$this->id;
3506
        } else if ($option == 'composition') {
3507
            $url = DOL_URL_ROOT.'/product/composition/card.php?id='.$this->id;
3508
        } else {
3509
            $url = DOL_URL_ROOT.'/product/card.php?id='.$this->id;
3510
        }
3511
3512
        if ($option !== 'nolink')
3513
        {
3514
        	// Add param to save lastsearch_values or not
3515
        	$add_save_lastsearch_values=($save_lastsearch_value == 1 ? 1 : 0);
3516
        	if ($save_lastsearch_value == -1 && preg_match('/list\.php/',$_SERVER["PHP_SELF"])) $add_save_lastsearch_values=1;
3517
        	if ($add_save_lastsearch_values) $url.='&save_lastsearch_values=1';
3518
        }
3519
3520
        $linkstart = '<a href="'.$url.'"';
3521
        $linkstart.=$linkclose.'>';
3522
        $linkend='</a>';
3523
3524
        $result.=$linkstart;
3525
		if ($withpicto) {
3526
			if ($this->type == Product::TYPE_PRODUCT) $result.=(img_object(($notooltip?'':$label), 'product', ($notooltip?'class="paddingright"':'class="paddingright classfortooltip"'), 0, 0, $notooltip?0:1));
0 ignored issues
show
Bug introduced by
The variable $notooltip does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
3527
			if ($this->type == Product::TYPE_SERVICE) $result.=(img_object(($notooltip?'':$label), 'service',  ($notooltip?'class="paddinright"':'class="paddingright classfortooltip"'), 0, 0, $notooltip?0:1));
3528
		}
3529
		$result.= $newref;
3530
		$result.= $linkend;
3531
		return $result;
3532
	}
3533
3534
3535
	/**
3536
	 *  Create a document onto disk according to template module.
3537
	 *
3538
	 * 	@param	    string		$modele			Force model to use ('' to not force)
3539
	 * 	@param		Translate	$outputlangs	Object langs to use for output
3540
	 *  @param      int			$hidedetails    Hide details of lines
3541
	 *  @param      int			$hidedesc       Hide description
3542
	 *  @param      int			$hideref        Hide ref
3543
	 * 	@return     int         				0 if KO, 1 if OK
3544
	 */
3545
	public function generateDocument($modele, $outputlangs, $hidedetails=0, $hidedesc=0, $hideref=0)
3546
	{
3547
		global $conf,$user,$langs;
3548
3549
		$langs->load("products");
3550
3551
		// Positionne le modele sur le nom du modele a utiliser
3552
		if (! dol_strlen($modele))
3553
		{
3554
			if (! empty($conf->global->PRODUCT_ADDON_PDF))
3555
			{
3556
				$modele = $conf->global->PRODUCT_ADDON_PDF;
3557
			}
3558
			else
3559
			{
3560
				$modele = 'strato';
3561
			}
3562
		}
3563
3564
		$modelpath = "core/modules/product/doc/";
3565
3566
		return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref);
3567
	}
3568
3569
	/**
3570
	 *	Return label of status of object
3571
	 *
3572
	 *	@param      int	$mode       0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=Short label + Picto
3573
	 *	@param      int	$type       0=Sell, 1=Buy, 2=Batch Number management
3574
	 *	@return     string      	Label of status
3575
	 */
3576
	function getLibStatut($mode=0, $type=0)
3577
	{
3578
		switch ($type)
3579
		{
3580
		case 0:
3581
			return $this->LibStatut($this->status,$mode,$type);
3582
		case 1:
3583
			return $this->LibStatut($this->status_buy,$mode,$type);
3584
		case 2:
3585
			return $this->LibStatut($this->status_batch,$mode,$type);
3586
		default:
3587
			//Simulate previous behavior but should return an error string
3588
			return $this->LibStatut($this->status_buy,$mode,$type);
3589
		}
3590
	}
3591
3592
	/**
3593
	 *	Return label of a given status
3594
	 *
3595
	 *	@param      int		$status     Statut
3596
	 *	@param      int		$mode       0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=Short label + Picto
3597
	 *	@param      int		$type       0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
3598
	 *	@return     string      		Label of status
3599
	 */
3600
	function LibStatut($status,$mode=0,$type=0)
3601
	{
3602
		global $conf, $langs;
3603
3604
		$langs->load('products');
3605
		if (! empty($conf->productbatch->enabled)) $langs->load("productbatch");
3606
3607
		if ($type == 2)
3608
		{
3609
			switch ($mode)
3610
			{
3611
				case 0:
3612
					return ($status == 0 ? $langs->trans('ProductStatusNotOnBatch') : $langs->trans('ProductStatusOnBatch'));
3613
				case 1:
3614
					return ($status == 0 ? $langs->trans('ProductStatusNotOnBatchShort') : $langs->trans('ProductStatusOnBatchShort'));
3615
				case 2:
3616
					return $this->LibStatut($status,3,2).' '.$this->LibStatut($status,1,2);
3617
				case 3:
3618
					if ($status == 0)
3619
					{
3620
						return img_picto($langs->trans('ProductStatusNotOnBatch'),'statut5');
3621
					}
3622
					return img_picto($langs->trans('ProductStatusOnBatch'),'statut4');
3623
				case 4:
3624
					return $this->LibStatut($status,3,2).' '.$this->LibStatut($status,0,2);
3625
				case 5:
3626
					return $this->LibStatut($status,1,2).' '.$this->LibStatut($status,3,2);
3627
				default:
3628
					return $langs->trans('Unknown');
3629
			}
3630
		}
3631
		if ($mode == 0)
3632
		{
3633
			if ($status == 0) return ($type==0 ? $langs->trans('ProductStatusNotOnSellShort'):$langs->trans('ProductStatusNotOnBuyShort'));
3634
			if ($status == 1) return ($type==0 ? $langs->trans('ProductStatusOnSellShort'):$langs->trans('ProductStatusOnBuyShort'));
3635
		}
3636
		if ($mode == 1)
3637
		{
3638
			if ($status == 0) return ($type==0 ? $langs->trans('ProductStatusNotOnSell'):$langs->trans('ProductStatusNotOnBuy'));
3639
			if ($status == 1) return ($type==0 ? $langs->trans('ProductStatusOnSell'):$langs->trans('ProductStatusOnBuy'));
3640
		}
3641
		if ($mode == 2)
3642
		{
3643
			if ($status == 0) return img_picto($langs->trans('ProductStatusNotOnSell'),'statut5', 'class="pictostatus"').' '.($type==0 ? $langs->trans('ProductStatusNotOnSellShort'):$langs->trans('ProductStatusNotOnBuyShort'));
3644
			if ($status == 1) return img_picto($langs->trans('ProductStatusOnSell'),'statut4', 'class="pictostatus"').' '.($type==0 ? $langs->trans('ProductStatusOnSellShort'):$langs->trans('ProductStatusOnBuyShort'));
3645
		}
3646
		if ($mode == 3)
3647
		{
3648
			if ($status == 0) return img_picto(($type==0 ? $langs->trans('ProductStatusNotOnSell') : $langs->trans('ProductStatusNotOnBuy')),'statut5', 'class="pictostatus"');
3649
			if ($status == 1) return img_picto(($type==0 ? $langs->trans('ProductStatusOnSell') : $langs->trans('ProductStatusOnBuy')),'statut4', 'class="pictostatus"');
3650
		}
3651
		if ($mode == 4)
3652
		{
3653
			if ($status == 0) return img_picto($langs->trans('ProductStatusNotOnSell'),'statut5', 'class="pictostatus"').' '.($type==0 ? $langs->trans('ProductStatusNotOnSell'):$langs->trans('ProductStatusNotOnBuy'));
3654
			if ($status == 1) return img_picto($langs->trans('ProductStatusOnSell'),'statut4', 'class="pictostatus"').' '.($type==0 ? $langs->trans('ProductStatusOnSell'):$langs->trans('ProductStatusOnBuy'));
3655
		}
3656
		if ($mode == 5)
3657
		{
3658
			if ($status == 0) return ($type==0 ? $langs->trans('ProductStatusNotOnSellShort'):$langs->trans('ProductStatusNotOnBuyShort')).' '.img_picto(($type==0 ? $langs->trans('ProductStatusNotOnSell'):$langs->trans('ProductStatusNotOnBuy')), 'statut5', 'class="pictostatus"');
3659
			if ($status == 1) return ($type==0 ? $langs->trans('ProductStatusOnSellShort'):$langs->trans('ProductStatusOnBuyShort')).' '.img_picto(($type==0 ? $langs->trans('ProductStatusOnSell'):$langs->trans('ProductStatusOnBuy')),'statut4', 'class="pictostatus"');
3660
		}
3661
		if ($mode == 6)
3662
		{
3663
			if ($status == 0) return ($type==0 ? $langs->trans('ProductStatusNotOnSellShort'):$langs->trans('ProductStatusNotOnBuyShort')).' '.img_picto(($type==0 ? $langs->trans('ProductStatusNotOnSell'):$langs->trans('ProductStatusNotOnBuy')), 'statut5', 'class="pictostatus"');
3664
			if ($status == 1) return ($type==0 ? $langs->trans('ProductStatusOnSellShort'):$langs->trans('ProductStatusOnBuyShort')).' '.img_picto(($type==0 ? $langs->trans('ProductStatusOnSell'):$langs->trans('ProductStatusOnBuy')),'statut4', 'class="pictostatus"');
3665
		}
3666
		return $langs->trans('Unknown');
3667
	}
3668
3669
3670
	/**
3671
	 *  Retourne le libelle du finished du produit
3672
	 *
3673
	 *  @return     string		Libelle
3674
	 */
3675
	function getLibFinished()
3676
	{
3677
		global $langs;
3678
		$langs->load('products');
3679
3680
		if ($this->finished == '0') return $langs->trans("RowMaterial");
3681
		if ($this->finished == '1') return $langs->trans("Finished");
3682
		return '';
3683
	}
3684
3685
3686
	/**
3687
	 *  Adjust stock in a warehouse for product
3688
	 *
3689
	 *  @param  	User	$user           user asking change
3690
	 *  @param  	int		$id_entrepot    id of warehouse
3691
	 *  @param  	double	$nbpiece        nb of units
3692
	 *  @param  	int		$movement       0 = add, 1 = remove
3693
	 * 	@param		string	$label			Label of stock movement
3694
	 * 	@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.
3695
	 *  @param		string	$inventorycode	Inventory code
3696
	 *  @param  	string	$origin_element Origin element type
3697
	 *  @param  	int		$origin_id      Origin id of element
3698
	 * 	@return     int     				<0 if KO, >0 if OK
3699
	 */
3700
	function correct_stock($user, $id_entrepot, $nbpiece, $movement, $label='', $price=0, $inventorycode='', $origin_element='', $origin_id=null)
3701
	{
3702
		if ($id_entrepot)
3703
		{
3704
			$this->db->begin();
3705
3706
			require_once DOL_DOCUMENT_ROOT .'/product/stock/class/mouvementstock.class.php';
3707
3708
			$op[0] = "+".trim($nbpiece);
0 ignored issues
show
Coding Style Comprehensibility introduced by
$op was never initialized. Although not strictly required by PHP, it is generally a good practice to add $op = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
3709
			$op[1] = "-".trim($nbpiece);
3710
3711
			$movementstock=new MouvementStock($this->db);
3712
			$movementstock->setOrigin($origin_element, $origin_id);
3713
			$result=$movementstock->_create($user,$this->id,$id_entrepot,$op[$movement],$movement,$price,$label,$inventorycode);
3714
3715
			if ($result >= 0)
3716
			{
3717
				$this->db->commit();
3718
				return 1;
3719
			}
3720
			else
3721
			{
3722
			    $this->error=$movementstock->error;
3723
			    $this->errors=$movementstock->errors;
3724
3725
				$this->db->rollback();
3726
				return -1;
3727
			}
3728
		}
3729
	}
3730
3731
	/**
3732
	 *  Adjust stock in a warehouse for product with batch number
3733
	 *
3734
	 *  @param  	User	$user           user asking change
3735
	 *  @param  	int		$id_entrepot    id of warehouse
3736
	 *  @param  	double	$nbpiece        nb of units
3737
	 *  @param  	int		$movement       0 = add, 1 = remove
3738
	 * 	@param		string	$label			Label of stock movement
3739
	 * 	@param		double	$price			Price to use for stock eval
3740
	 * 	@param		date	$dlc			eat-by date
3741
	 * 	@param		date	$dluo			sell-by date
3742
	 * 	@param		string	$lot			Lot number
3743
	 *  @param		string	$inventorycode	Inventory code
3744
	 *  @param  	string	$origin_element Origin element type
3745
	 *  @param  	int		$origin_id      Origin id of element
3746
	 * 	@return     int     				<0 if KO, >0 if OK
3747
	 */
3748
	function correct_stock_batch($user, $id_entrepot, $nbpiece, $movement, $label='', $price=0, $dlc='', $dluo='',$lot='', $inventorycode='', $origin_element='', $origin_id=null)
3749
	{
3750
		if ($id_entrepot)
3751
		{
3752
			$this->db->begin();
3753
3754
			require_once DOL_DOCUMENT_ROOT .'/product/stock/class/mouvementstock.class.php';
3755
3756
			$op[0] = "+".trim($nbpiece);
0 ignored issues
show
Coding Style Comprehensibility introduced by
$op was never initialized. Although not strictly required by PHP, it is generally a good practice to add $op = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
3757
			$op[1] = "-".trim($nbpiece);
3758
3759
			$movementstock=new MouvementStock($this->db);
3760
			$movementstock->setOrigin($origin_element, $origin_id);
3761
			$result=$movementstock->_create($user,$this->id,$id_entrepot,$op[$movement],$movement,$price,$label,$inventorycode,'',$dlc,$dluo,$lot);
3762
3763
			if ($result >= 0)
3764
			{
3765
				$this->db->commit();
3766
				return 1;
3767
			}
3768
			else
3769
			{
3770
			    $this->error=$movementstock->error;
3771
			    $this->errors=$movementstock->errors;
3772
3773
				$this->db->rollback();
3774
				return -1;
3775
			}
3776
		}
3777
	}
3778
3779
	/**
3780
	 *    Load information about stock of a product into ->stock_reel, ->stock_warehouse[] (including stock_warehouse[idwarehouse]->detail_batch for batch products)
3781
	 *    This function need a lot of load. If you use it on list, use a cache to execute it once for each product id.
3782
	 *    If ENTREPOT_EXTRA_STATUS set, filtering on warehouse status possible.
3783
	 *
3784
	 *    @param      string   $option 		'' = Load all stock info, also from closed and internal warehouses,
3785
	 *										'nobatch' = Do not load batch information,
3786
	 *										'novirtual' = Do not load virtual stock,
3787
	 *										'warehouseopen' = Load stock from open warehouses only,
3788
	 *										'warehouseclosed' = Load stock from closed warehouses only,
3789
	 *										'warehouseinternal' = Load stock from warehouses for internal correction/transfer only
3790
	 *    @return     int                   < 0 if KO, > 0 if OK
3791
	 *    @see		  load_virtual_stock, getBatchInfo
3792
	 */
3793
	function load_stock($option='')
3794
	{
3795
		global $conf;
3796
3797
		$this->stock_reel = 0;
3798
		$this->stock_warehouse = array();
3799
		$this->stock_theorique = 0;
3800
3801
		$warehouseStatus = array();
3802
3803
		if (preg_match('/warehouseclosed/', $option))
3804
		{
3805
			$warehouseStatus[] = Entrepot::STATUS_CLOSED;
3806
		}
3807
		if (preg_match('/warehouseopen/', $option))
3808
		{
3809
			$warehouseStatus[] = Entrepot::STATUS_OPEN_ALL;
3810
		}
3811
		if (preg_match('/warehouseinternal/', $option))
3812
		{
3813
			$warehouseStatus[] = Entrepot::STATUS_OPEN_INTERNAL;
3814
		}
3815
3816
		$sql = "SELECT ps.rowid, ps.reel, ps.fk_entrepot";
3817
		$sql.= " FROM ".MAIN_DB_PREFIX."product_stock as ps";
3818
		$sql.= ", ".MAIN_DB_PREFIX."entrepot as w";
3819
		$sql.= " WHERE w.entity IN (".getEntity('stock').")";
3820
		$sql.= " AND w.rowid = ps.fk_entrepot";
3821
		$sql.= " AND ps.fk_product = ".$this->id;
3822
		if ($conf->global->ENTREPOT_EXTRA_STATUS && count($warehouseStatus)) $sql.= " AND w.statut IN (".$this->db->escape(implode(',',$warehouseStatus)).")";
3823
3824
		dol_syslog(get_class($this)."::load_stock", LOG_DEBUG);
3825
		$result = $this->db->query($sql);
3826
		if ($result)
3827
		{
3828
			$num = $this->db->num_rows($result);
3829
			$i=0;
3830
			if ($num > 0)
3831
			{
3832
				while ($i < $num)
3833
				{
3834
					$row = $this->db->fetch_object($result);
3835
					$this->stock_warehouse[$row->fk_entrepot] = new stdClass();
3836
					$this->stock_warehouse[$row->fk_entrepot]->real = $row->reel;
3837
					$this->stock_warehouse[$row->fk_entrepot]->id = $row->rowid;
3838
					if ((! preg_match('/nobatch/', $option)) && $this->hasbatch()) $this->stock_warehouse[$row->fk_entrepot]->detail_batch=Productbatch::findAll($this->db,$row->rowid,1);
3839
					$this->stock_reel+=$row->reel;
3840
					$i++;
3841
				}
3842
			}
3843
			$this->db->free($result);
3844
3845
			if (! preg_match('/novirtual/', $option))
3846
			{
3847
			    $this->load_virtual_stock();		// This also load stats_commande_fournisseur, ...
3848
			}
3849
3850
			return 1;
3851
		}
3852
		else
3853
		{
3854
			$this->error=$this->db->lasterror();
3855
			return -1;
3856
		}
3857
	}
3858
3859
	/**
3860
	 *    Load value ->stock_theorique of a product. Property this->id must be defined.
3861
	 *    This function need a lot of load. If you use it on list, use a cache to execute it one for each product id.
3862
	 *
3863
	 *    @return   int             < 0 if KO, > 0 if OK
3864
	 *    @see		load_stock, getBatchInfo
3865
	 */
3866
    function load_virtual_stock()
3867
    {
3868
        global $conf;
3869
3870
        $stock_commande_client=0;
3871
        $stock_commande_fournisseur=0;
3872
        $stock_sending_client=0;
3873
        $stock_reception_fournisseur=0;
3874
3875
        if (! empty($conf->commande->enabled))
3876
        {
3877
            $result=$this->load_stats_commande(0,'1,2', 1);
3878
            if ($result < 0) dol_print_error($this->db,$this->error);
3879
            $stock_commande_client=$this->stats_commande['qty'];
3880
        }
3881
        if (! empty($conf->expedition->enabled))
3882
        {
3883
            $result=$this->load_stats_sending(0,'1,2', 1);
3884
            if ($result < 0) dol_print_error($this->db,$this->error);
3885
            $stock_sending_client=$this->stats_expedition['qty'];
3886
        }
3887
        if (! empty($conf->fournisseur->enabled))
3888
        {
3889
            $result=$this->load_stats_commande_fournisseur(0,'1,2,3,4', 1);
3890
            if ($result < 0) dol_print_error($this->db,$this->error);
3891
            $stock_commande_fournisseur=$this->stats_commande_fournisseur['qty'];
3892
3893
            $result=$this->load_stats_reception(0,'4', 1);
3894
            if ($result < 0) dol_print_error($this->db,$this->error);
3895
            $stock_reception_fournisseur=$this->stats_reception['qty'];
3896
        }
3897
3898
        // Stock decrease mode
3899
        if (! empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT) || ! empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE)) {
3900
            $this->stock_theorique=$this->stock_reel-$stock_commande_client+$stock_sending_client;
0 ignored issues
show
Documentation Bug introduced by
The property $stock_theorique was declared of type integer, but $this->stock_reel - $sto...+ $stock_sending_client is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
3901
        }
3902
        if (! empty($conf->global->STOCK_CALCULATE_ON_VALIDATE_ORDER)) {
3903
            $this->stock_theorique=$this->stock_reel;
1 ignored issue
show
Documentation Bug introduced by
It seems like $this->stock_reel can also be of type double. However, the property $stock_theorique is declared as type integer. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
3904
        }
3905
        if (! empty($conf->global->STOCK_CALCULATE_ON_BILL)) {
3906
            $this->stock_theorique=$this->stock_reel-$stock_commande_client;
1 ignored issue
show
Documentation Bug introduced by
It seems like $this->stock_reel - $stock_commande_client can also be of type double. However, the property $stock_theorique is declared as type integer. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
3907
        }
3908
        // Stock Increase mode
3909
        if (! empty($conf->global->STOCK_CALCULATE_ON_SUPPLIER_DISPATCH_ORDER)) {
3910
            $this->stock_theorique+=$stock_commande_fournisseur-$stock_reception_fournisseur;
3911
        }
3912
        if (! empty($conf->global->STOCK_CALCULATE_ON_SUPPLIER_VALIDATE_ORDER)) {
3913
            $this->stock_theorique-=$stock_reception_fournisseur;
3914
        }
3915
        if (! empty($conf->global->STOCK_CALCULATE_ON_SUPPLIER_BILL)) {
3916
            $this->stock_theorique+=$stock_commande_fournisseur-$stock_reception_fournisseur;
3917
        }
3918
    }
3919
3920
3921
	/**
3922
	 *  Load existing information about a serial
3923
	 *
3924
	 *	@param		string		$batch		Lot/serial number
3925
	 *  @return     array					Array with record into product_batch
3926
	 *  @see		load_stock, load_virtual_stock
3927
	 */
3928
    function loadBatchInfo($batch)
3929
    {
3930
    	$result=array();
3931
3932
    	$sql = "SELECT pb.batch, pb.eatby, pb.sellby, SUM(pb.qty) FROM ".MAIN_DB_PREFIX."product_batch as pb, ".MAIN_DB_PREFIX."product_stock as ps";
3933
    	$sql.= " WHERE pb.fk_product_stock = ps.rowid AND ps.fk_product = ".$this->id." AND pb.batch = '".$this->db->escape($batch)."'";
3934
    	$sql.= " GROUP BY pb.batch, pb.eatby, pb.sellby";
3935
    	dol_syslog(get_class($this)."::loadBatchInfo load first entry found for lot/serial = ".$batch, LOG_DEBUG);
3936
    	$resql = $this->db->query($sql);
3937
    	if ($resql)
3938
    	{
3939
    		$num = $this->db->num_rows($resql);
3940
    		$i=0;
3941
    		while ($i < $num)
3942
    		{
3943
    			$obj = $this->db->fetch_object($resql);
3944
				$result[]=array('batch'=>$batch, 'eatby'=>$this->db->jdate($obj->eatby), 'sellby'=>$this->db->jdate($obj->sellby), 'qty'=>$obj->qty);
3945
				$i++;
3946
    		}
3947
    		return $result;
3948
    	}
3949
    	else
3950
    	{
3951
    		dol_print_error($this->db);
3952
    		$this->db->rollback();
3953
    		return array();
3954
    	}
3955
    }
3956
3957
3958
	/**
3959
	 *  Move an uploaded file described into $file array into target directory $sdir.
3960
	 *
3961
	 *  @param  string	$sdir       Target directory
3962
	 *  @param  string	$file       Array of file info of file to upload: array('name'=>..., 'tmp_name'=>...)
3963
	 *  @return	int					<0 if KO, >0 if OK
3964
	 */
3965
	function add_photo($sdir, $file)
3966
	{
3967
		global $conf;
3968
3969
		require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
3970
3971
		$result = 0;
3972
3973
		$dir = $sdir;
3974
		if (! empty($conf->global->PRODUCT_USE_OLD_PATH_FOR_PHOTO)) $dir .= '/'. get_exdir($this->id,2,0,0,$this,'product') . $this->id ."/photos";
3975
		else $dir .= '/'.get_exdir(0,0,0,0,$this,'product').dol_sanitizeFileName($this->ref);
3976
3977
		dol_mkdir($dir);
3978
3979
		$dir_osencoded=$dir;
3980
3981
		if (is_dir($dir_osencoded))
3982
		{
3983
			$originImage = $dir . '/' . $file['name'];
3984
3985
			// Cree fichier en taille origine
3986
			$result=dol_move_uploaded_file($file['tmp_name'], $originImage, 1);
3987
3988
			if (file_exists(dol_osencode($originImage)))
3989
			{
3990
				// Create thumbs
3991
				$this->addThumbs($originImage);
3992
			}
3993
		}
3994
3995
		if (is_numeric($result) && $result > 0) return 1;
3996
		else return -1;
3997
	}
3998
3999
	/**
4000
	 *  Return if at least one photo is available
4001
	 *
4002
	 *  @param      string		$sdir       Directory to scan
4003
	 *  @return     boolean     			True if at least one photo is available, False if not
4004
	 */
4005
	function is_photo_available($sdir)
4006
	{
4007
	    include_once DOL_DOCUMENT_ROOT .'/core/lib/files.lib.php';
4008
	    include_once DOL_DOCUMENT_ROOT .'/core/lib/images.lib.php';
4009
4010
		global $conf;
4011
4012
		$dir = $sdir;
4013
		if (! empty($conf->global->PRODUCT_USE_OLD_PATH_FOR_PHOTO)) $dir .= '/'. get_exdir($this->id,2,0,0,$this,'product') . $this->id ."/photos/";
4014
		else $dir .= '/'.get_exdir(0,0,0,0,$this,'product').dol_sanitizeFileName($this->ref).'/';
4015
4016
		$nbphoto=0;
4017
4018
		$dir_osencoded=dol_osencode($dir);
4019
		if (file_exists($dir_osencoded))
4020
		{
4021
			$handle=opendir($dir_osencoded);
4022
			if (is_resource($handle))
4023
			{
4024
			    while (($file = readdir($handle)) !== false)
4025
    			{
4026
    				if (! utf8_check($file)) $file=utf8_encode($file);	// To be sure data is stored in UTF8 in memory
4027
    				if (dol_is_file($dir.$file) && image_format_supported($file) > 0) return true;
4028
    			}
4029
			}
4030
		}
4031
		return false;
4032
	}
4033
4034
4035
	/**
4036
	 *  Show photos of a product (nbmax maximum), into several columns
4037
	 *	TODO Move this into html.formproduct.class.php
4038
	 *
4039
	 *  @param      string	$sdir        	Directory to scan (full absolute path)
4040
	 *  @param      int		$size        	0=original size, 1='small' use thumbnail if possible
4041
	 *  @param      int		$nbmax       	Nombre maximum de photos (0=pas de max)
4042
	 *  @param      int		$nbbyrow     	Number of image per line or -1 to use div. Used only if size=1.
4043
	 * 	@param		int		$showfilename	1=Show filename
4044
	 * 	@param		int		$showaction		1=Show icon with action links (resize, delete)
4045
	 * 	@param		int		$maxHeight		Max height of original image when size='small' (so we can use original even if small requested). If 0, always use 'small' thumb image.
4046
	 * 	@param		int		$maxWidth		Max width of original image when size='small'
4047
	 *  @param      int     $nolink         Do not add a href link to view enlarged imaged into a new tab
4048
	 *  @return     string					Html code to show photo. Number of photos shown is saved in this->nbphoto
4049
	 */
4050
	function show_photos($sdir,$size=0,$nbmax=0,$nbbyrow=5,$showfilename=0,$showaction=0,$maxHeight=120,$maxWidth=160,$nolink=0)
4051
	{
4052
		global $conf,$user,$langs;
4053
4054
		include_once DOL_DOCUMENT_ROOT .'/core/lib/files.lib.php';
4055
		include_once DOL_DOCUMENT_ROOT .'/core/lib/images.lib.php';
4056
4057
		$sortfield='position_name';
4058
		$sortorder='asc';
4059
4060
		$dir = $sdir . '/';
4061
		$pdir = '/';
4062
		$dir .= get_exdir(0,0,0,0,$this,'product').$this->ref.'/';
4063
		$pdir .= get_exdir(0,0,0,0,$this,'product').$this->ref.'/';
4064
4065
		if (! empty($conf->global->PRODUCT_USE_OLD_PATH_FOR_PHOTO))
4066
		{
4067
			$dirold .= get_exdir($this->id,2,0,0,$this,'product') . $this->id ."/photos/";
0 ignored issues
show
Bug introduced by
The variable $dirold does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
4068
			$pdirold .= get_exdir($this->id,2,0,0,$this,'product') . $this->id ."/photos/";
0 ignored issues
show
Bug introduced by
The variable $pdirold does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
4069
		}
4070
4071
		// Defined relative dir to DOL_DATA_ROOT
4072
		$relativedir = '';
4073
		if ($dir)
4074
		{
4075
		    $relativedir = preg_replace('/^'.preg_quote(DOL_DATA_ROOT,'/').'/', '', $dir);
4076
		    $relativedir = preg_replace('/^[\\/]/','',$relativedir);
4077
		    $relativedir = preg_replace('/[\\/]$/','',$relativedir);
4078
		}
4079
4080
		$dirthumb = $dir.'thumbs/';
4081
		$pdirthumb = $pdir.'thumbs/';
4082
4083
		$return ='<!-- Photo -->'."\n";
4084
		$nbphoto=0;
4085
4086
		$filearray=dol_dir_list($dir,"files",0,'','(\.meta|_preview.*\.png)$',$sortfield,(strtolower($sortorder)=='desc'?SORT_DESC:SORT_ASC),1);
4087
4088
		if (! empty($conf->global->PRODUCT_USE_OLD_PATH_FOR_PHOTO))    // For backward compatiblity, we scan also old dirs
4089
		{
4090
		    $filearrayold=dol_dir_list($dirold,"files",0,'','(\.meta|_preview.*\.png)$',$sortfield,(strtolower($sortorder)=='desc'?SORT_DESC:SORT_ASC),1);
4091
		    $filearray=array_merge($filearray, $filearrayold);
4092
		}
4093
4094
		completeFileArrayWithDatabaseInfo($filearray, $relativedir);
4095
4096
        if (count($filearray))
4097
        {
4098
            if ($sortfield && $sortorder)
4099
            {
4100
                $filearray=dol_sort_array($filearray, $sortfield, $sortorder);
4101
            }
4102
4103
            foreach($filearray as $key => $val)
4104
            {
4105
				$photo='';
4106
                $file = $val['name'];
4107
4108
				//if (! utf8_check($file)) $file=utf8_encode($file);	// To be sure file is stored in UTF8 in memory
4109
4110
				//if (dol_is_file($dir.$file) && image_format_supported($file) >= 0)
4111
				if (image_format_supported($file) >= 0)
4112
				{
4113
					$nbphoto++;
4114
					$photo = $file;
4115
					$viewfilename = $file;
4116
4117
					if ($size == 1 || $size == 'small') {   // Format vignette
4118
4119
						// Find name of thumb file
4120
						$photo_vignette=basename(getImageFileNameForSize($dir.$file, '_small'));
4121
						if (! dol_is_file($dirthumb.$photo_vignette)) $photo_vignette='';
4122
4123
						// Get filesize of original file
4124
						$imgarray=dol_getImageSize($dir.$photo);
4125
4126
						if ($nbbyrow > 0)
4127
						{
4128
							if ($nbphoto == 1) $return.= '<table width="100%" valign="top" align="center" border="0" cellpadding="2" cellspacing="2">';
4129
4130
							if ($nbphoto % $nbbyrow == 1) $return.= '<tr align=center valign=middle border=1>';
4131
							$return.= '<td width="'.ceil(100/$nbbyrow).'%" class="photo">';
4132
						}
4133
						else if ($nbbyrow < 0) $return .= '<div class="inline-block">';
4134
4135
						$return.= "\n";
4136
4137
						$relativefile=preg_replace('/^\//', '', $pdir.$photo);
4138
						if (empty($nolink))
4139
						{
4140
						    $urladvanced=getAdvancedPreviewUrl('product', $relativefile, 0, 'entity='.$this->entity);
4141
						    if ($urladvanced) $return.='<a href="'.$urladvanced.'">';
4142
						    else $return.= '<a href="'.DOL_URL_ROOT.'/viewimage.php?modulepart=product&entity='.$this->entity.'&file='.urlencode($pdir.$photo).'" class="aphoto" target="_blank">';
4143
						}
4144
4145
						// Show image (width height=$maxHeight)
4146
						// Si fichier vignette disponible et image source trop grande, on utilise la vignette, sinon on utilise photo origine
4147
						$alt=$langs->transnoentitiesnoconv('File').': '.$relativefile;
4148
						$alt.=' - '.$langs->transnoentitiesnoconv('Size').': '.$imgarray['width'].'x'.$imgarray['height'];
4149
4150
						if (empty($maxHeight) || $photo_vignette && $imgarray['height'] > $maxHeight)
4151
						{
4152
							$return.= '<!-- Show thumb -->';
4153
							$return.= '<img class="photo photowithmargin" border="0" height="'.$maxHeight.'" src="'.DOL_URL_ROOT.'/viewimage.php?modulepart=product&entity='.$this->entity.'&file='.urlencode($pdirthumb.$photo_vignette).'" title="'.dol_escape_htmltag($alt).'">';
4154
						}
4155
						else {
4156
							$return.= '<!-- Show original file -->';
4157
							$return.= '<img class="photo photowithmargin" border="0" height="'.$maxHeight.'" src="'.DOL_URL_ROOT.'/viewimage.php?modulepart=product&entity='.$this->entity.'&file='.urlencode($pdir.$photo).'" title="'.dol_escape_htmltag($alt).'">';
4158
						}
4159
4160
						if (empty($nolink)) $return.= '</a>';
4161
						$return.="\n";
4162
4163
						if ($showfilename) $return.= '<br>'.$viewfilename;
4164
						if ($showaction)
4165
						{
4166
							$return.= '<br>';
4167
							// On propose la generation de la vignette si elle n'existe pas et si la taille est superieure aux limites
4168
							if ($photo_vignette && (image_format_supported($photo) > 0) && ($this->imgWidth > $maxWidth || $this->imgHeight > $maxHeight))
4169
							{
4170
								$return.= '<a href="'.$_SERVER["PHP_SELF"].'?id='.$this->id.'&amp;action=addthumb&amp;file='.urlencode($pdir.$viewfilename).'">'.img_picto($langs->trans('GenerateThumb'),'refresh').'&nbsp;&nbsp;</a>';
4171
							}
4172
							if ($user->rights->produit->creer || $user->rights->service->creer)
4173
							{
4174
								// Link to resize
4175
			               		$return.= '<a href="'.DOL_URL_ROOT.'/core/photos_resize.php?modulepart='.urlencode('produit|service').'&id='.$this->id.'&amp;file='.urlencode($pdir.$viewfilename).'" title="'.dol_escape_htmltag($langs->trans("Resize")).'">'.img_picto($langs->trans("Resize"),DOL_URL_ROOT.'/theme/common/transform-crop-and-resize','',1).'</a> &nbsp; ';
4176
4177
			               		// Link to delete
4178
								$return.= '<a href="'.$_SERVER["PHP_SELF"].'?id='.$this->id.'&amp;action=delete&amp;file='.urlencode($pdir.$viewfilename).'">';
4179
								$return.= img_delete().'</a>';
4180
							}
4181
						}
4182
						$return.= "\n";
4183
4184
						if ($nbbyrow > 0)
4185
						{
4186
							$return.= '</td>';
4187
							if (($nbphoto % $nbbyrow) == 0) $return.= '</tr>';
4188
						}
4189
						else if ($nbbyrow < 0) $return.='</div>';
4190
					}
4191
4192
					if (empty($size)) {     // Format origine
4193
						$return.= '<img class="photo photowithmargin" border="0" src="'.DOL_URL_ROOT.'/viewimage.php?modulepart=product&entity='.$this->entity.'&file='.urlencode($pdir.$photo).'">';
4194
4195
						if ($showfilename) $return.= '<br>'.$viewfilename;
4196
						if ($showaction)
4197
						{
4198
							if ($user->rights->produit->creer || $user->rights->service->creer)
4199
							{
4200
								// Link to resize
4201
			               		$return.= '<a href="'.DOL_URL_ROOT.'/core/photos_resize.php?modulepart='.urlencode('produit|service').'&id='.$this->id.'&amp;file='.urlencode($pdir.$viewfilename).'" title="'.dol_escape_htmltag($langs->trans("Resize")).'">'.img_picto($langs->trans("Resize"),DOL_URL_ROOT.'/theme/common/transform-crop-and-resize','',1).'</a> &nbsp; ';
4202
4203
			               		// Link to delete
4204
			               		$return.= '<a href="'.$_SERVER["PHP_SELF"].'?id='.$this->id.'&amp;action=delete&amp;file='.urlencode($pdir.$viewfilename).'">';
4205
								$return.= img_delete().'</a>';
4206
							}
4207
						}
4208
					}
4209
4210
					// On continue ou on arrete de boucler ?
4211
					if ($nbmax && $nbphoto >= $nbmax) break;
4212
				}
4213
            }
4214
4215
			if ($size==1 || $size=='small')
4216
			{
4217
				if ($nbbyrow > 0)
4218
				{
4219
					// Ferme tableau
4220
					while ($nbphoto % $nbbyrow)
4221
					{
4222
						$return.= '<td width="'.ceil(100/$nbbyrow).'%">&nbsp;</td>';
4223
						$nbphoto++;
4224
					}
4225
4226
					if ($nbphoto) $return.= '</table>';
4227
				}
4228
			}
4229
		}
4230
4231
		$this->nbphoto = $nbphoto;
4232
4233
		return $return;
4234
	}
4235
4236
4237
	/**
4238
	 *  Retourne tableau de toutes les photos du produit
4239
	 *
4240
	 *  @param      string		$dir        Repertoire a scanner
4241
	 *  @param      int			$nbmax      Nombre maximum de photos (0=pas de max)
4242
	 *  @return     array       			Tableau de photos
4243
	 */
4244
	function liste_photos($dir,$nbmax=0)
4245
	{
4246
	    include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
4247
	    include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
4248
4249
		$nbphoto=0;
4250
		$tabobj=array();
4251
4252
		$dir_osencoded=dol_osencode($dir);
4253
		$handle=@opendir($dir_osencoded);
4254
		if (is_resource($handle))
4255
		{
4256
			while (($file = readdir($handle)) !== false)
4257
			{
4258
				if (! utf8_check($file)) $file=utf8_encode($file);	// readdir returns ISO
4259
				if (dol_is_file($dir.$file) && image_format_supported($file) >= 0)
4260
				{
4261
					$nbphoto++;
4262
4263
					// On determine nom du fichier vignette
4264
					$photo=$file;
4265
					$photo_vignette='';
4266
					if (preg_match('/('.$this->regeximgext.')$/i', $photo, $regs))
4267
					{
4268
						$photo_vignette=preg_replace('/'.$regs[0].'/i', '', $photo).'_small'.$regs[0];
4269
					}
4270
4271
					$dirthumb = $dir.'thumbs/';
4272
4273
					// Objet
4274
					$obj=array();
4275
					$obj['photo']=$photo;
4276
					if ($photo_vignette && dol_is_file($dirthumb.$photo_vignette)) $obj['photo_vignette']='thumbs/' . $photo_vignette;
4277
					else $obj['photo_vignette']="";
4278
4279
					$tabobj[$nbphoto-1]=$obj;
4280
4281
					// On continue ou on arrete de boucler ?
4282
					if ($nbmax && $nbphoto >= $nbmax) break;
4283
				}
4284
			}
4285
4286
			closedir($handle);
4287
		}
4288
4289
		return $tabobj;
4290
	}
4291
4292
	/**
4293
	 *  Efface la photo du produit et sa vignette
4294
	 *
4295
	 *  @param  string		$file        Chemin de l'image
4296
	 *  @return	void
4297
	 */
4298
	function delete_photo($file)
4299
	{
4300
	    require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
4301
	    require_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
4302
4303
        $dir = dirname($file).'/'; // Chemin du dossier contenant l'image d'origine
4304
		$dirthumb = $dir.'/thumbs/'; // Chemin du dossier contenant la vignette
4305
		$filename = preg_replace('/'.preg_quote($dir,'/').'/i','',$file); // Nom du fichier
4306
4307
		// On efface l'image d'origine
4308
		dol_delete_file($file, 0, 0, 0, $this); // For triggers
4309
4310
		// Si elle existe, on efface la vignette
4311
		if (preg_match('/('.$this->regeximgext.')$/i',$filename,$regs))
4312
		{
4313
			$photo_vignette=preg_replace('/'.$regs[0].'/i','',$filename).'_small'.$regs[0];
4314
			if (file_exists(dol_osencode($dirthumb.$photo_vignette)))
4315
			{
4316
				dol_delete_file($dirthumb.$photo_vignette);
4317
			}
4318
4319
			$photo_vignette=preg_replace('/'.$regs[0].'/i','',$filename).'_mini'.$regs[0];
4320
			if (file_exists(dol_osencode($dirthumb.$photo_vignette)))
4321
			{
4322
				dol_delete_file($dirthumb.$photo_vignette);
4323
			}
4324
		}
4325
	}
4326
4327
	/**
4328
	 *  Load size of image file
4329
	 *
4330
	 *  @param  string	$file        Path to file
4331
	 *  @return	void
4332
	 */
4333
	function get_image_size($file)
4334
	{
4335
		$file_osencoded=dol_osencode($file);
4336
		$infoImg = getimagesize($file_osencoded); // Get information on image
4337
		$this->imgWidth = $infoImg[0]; // Largeur de l'image
4338
		$this->imgHeight = $infoImg[1]; // Hauteur de l'image
4339
	}
4340
4341
	/**
4342
	 *  Load indicators this->nb for the dashboard
4343
	 *
4344
	 *  @return    int                 <0 if KO, >0 if OK
4345
	 */
4346
	function load_state_board()
4347
	{
4348
		global $conf, $user, $hookmanager;
4349
4350
		$this->nb=array();
4351
4352
		$sql = "SELECT count(p.rowid) as nb, fk_product_type";
4353
		$sql.= " FROM ".MAIN_DB_PREFIX."product as p";
4354
		$sql.= ' WHERE p.entity IN ('.getEntity($this->element, 1).')';
4355
		// Add where from hooks
4356
		if (is_object($hookmanager))
4357
		{
4358
		    $parameters=array();
4359
		    $reshook=$hookmanager->executeHooks('printFieldListWhere',$parameters);    // Note that $action and $object may have been modified by hook
4360
		    $sql.=$hookmanager->resPrint;
4361
		}
4362
        $sql.= ' GROUP BY fk_product_type';
4363
4364
		$resql=$this->db->query($sql);
4365
		if ($resql)
4366
		{
4367
			while ($obj=$this->db->fetch_object($resql))
4368
			{
4369
				if ($obj->fk_product_type == 1) $this->nb["services"]=$obj->nb;
4370
				else $this->nb["products"]=$obj->nb;
4371
			}
4372
            $this->db->free($resql);
4373
			return 1;
4374
		}
4375
		else
4376
		{
4377
			dol_print_error($this->db);
4378
			$this->error=$this->db->error();
4379
			return -1;
4380
		}
4381
	}
4382
4383
    /**
4384
     * Return if object is a product
4385
     *
4386
     * @return  boolean     True if it's a product
4387
     */
4388
	function isProduct()
4389
	{
4390
		return ($this->type == Product::TYPE_PRODUCT ? true : false);
4391
	}
4392
4393
    /**
4394
     * Return if object is a product
4395
     *
4396
     * @return  boolean     True if it's a service
4397
     */
4398
	function isService()
4399
	{
4400
		return ($this->type == Product::TYPE_SERVICE ? true : false);
4401
	}
4402
4403
    /**
4404
     *  Get a barcode from the module to generate barcode values.
4405
     *  Return value is stored into this->barcode
4406
     *
4407
     *	@param	Product		$object		Object product or service
4408
     *	@param	string		$type		Barcode type (ean, isbn, ...)
4409
     *  @return void
4410
     */
4411
    function get_barcode($object,$type='')
4412
    {
4413
        global $conf;
4414
4415
        $result='';
4416
        if (! empty($conf->global->BARCODE_PRODUCT_ADDON_NUM))
4417
        {
4418
            $dirsociete=array_merge(array('/core/modules/barcode/'),$conf->modules_parts['barcode']);
4419
            foreach ($dirsociete as $dirroot)
4420
            {
4421
                $res=dol_include_once($dirroot.$conf->global->BARCODE_PRODUCT_ADDON_NUM.'.php');
4422
                if ($res) break;
4423
            }
4424
            $var = $conf->global->BARCODE_PRODUCT_ADDON_NUM;
4425
            $mod = new $var;
4426
4427
            $result=$mod->getNextValue($object,$type);
4428
4429
            dol_syslog(get_class($this)."::get_barcode barcode=".$result." module=".$var);
4430
        }
4431
        return $result;
4432
    }
4433
4434
    /**
4435
     *  Initialise an instance with random values.
4436
     *  Used to build previews or test instances.
4437
     *	id must be 0 if object instance is a specimen.
4438
     *
4439
     *  @return	void
4440
     */
4441
    function initAsSpecimen()
4442
    {
4443
        global $user,$langs,$conf,$mysoc;
4444
4445
        $now=dol_now();
4446
4447
        // Initialize parameters
4448
        $this->specimen=1;
4449
        $this->id=0;
4450
        $this->ref = 'PRODUCT_SPEC';
4451
        $this->label = 'PRODUCT SPECIMEN';
4452
        $this->description = 'This is description of this product specimen that was created the '.dol_print_date($now,'dayhourlog').'.';
4453
        $this->specimen=1;
4454
        $this->country_id=1;
4455
        $this->tosell=1;
4456
        $this->tobuy=1;
4457
		$this->tobatch=0;
4458
        $this->note='This is a comment (private)';
4459
        $this->date_creation = $now;
4460
        $this->date_modification = $now;
4461
4462
        $this->weight = 4;
4463
        $this->weight_unit = 1;
4464
4465
        $this->length = 5;
4466
        $this->length_unit = 1;
4467
        $this->width = 6;
4468
        $this->width_unit = 0;
4469
        $this->height = null;
4470
        $this->height_unit = null;
4471
4472
        $this->surface = 30;
4473
        $this->surface_unit = 0;
4474
        $this->volume = 300;
4475
        $this->volume_unit = 0;
4476
4477
        $this->barcode=-1;	// Create barcode automatically
4478
    }
4479
4480
	/**
4481
	 *	Returns the text label from units dictionary
4482
	 *
4483
	 * 	@param	string $type Label type (long or short)
4484
	 *	@return	string|int <0 if ko, label if ok
4485
	 */
4486
	function getLabelOfUnit($type='long')
4487
	{
4488
		global $langs;
4489
4490
		if (!$this->fk_unit) {
4491
			return '';
4492
		}
4493
4494
		$langs->load('products');
4495
4496
		$this->db->begin();
4497
4498
		$label_type = 'label';
4499
4500
		if ($type == 'short')
4501
		{
4502
			$label_type = 'short_label';
4503
		}
4504
4505
		$sql = 'select '.$label_type.' from '.MAIN_DB_PREFIX.'c_units where rowid='.$this->fk_unit;
4506
		$resql = $this->db->query($sql);
4507
		if($resql && $this->db->num_rows($resql) > 0)
4508
		{
4509
			$res = $this->db->fetch_array($resql);
4510
			$label = $res[$label_type];
4511
			$this->db->free($resql);
4512
			return $label;
4513
		}
4514
		else
4515
		{
4516
			$this->error=$this->db->error().' sql='.$sql;
4517
			dol_syslog(get_class($this)."::getLabelOfUnit Error ".$this->error, LOG_ERR);
4518
			return -1;
4519
		}
4520
	}
4521
4522
    /**
4523
     * Return if object has a sell-by date or eat-by date
4524
     *
4525
     * @return  boolean     True if it's has
4526
     */
4527
	function hasbatch()
4528
	{
4529
		return ($this->status_batch == 1 ? true : false);
4530
	}
4531
4532
4533
	/**
4534
     * Return minimum product recommended price
4535
     *
4536
	 * @return	int			Minimum recommanded price that is higher price among all suppliers * PRODUCT_MINIMUM_RECOMMENDED_PRICE
4537
     */
4538
	function min_recommended_price()
4539
	{
4540
		global $conf;
4541
4542
		$maxpricesupplier=0;
4543
4544
		if (! empty($conf->global->PRODUCT_MINIMUM_RECOMMENDED_PRICE))
4545
		{
4546
			require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
4547
			$product_fourn = new ProductFournisseur($this->db);
4548
			$product_fourn_list = $product_fourn->list_product_fournisseur_price($this->id, '', '');
4549
4550
			if (is_array($product_fourn_list) && count($product_fourn_list)>0)
4551
			{
4552
				foreach($product_fourn_list as $productfourn)
4553
				{
4554
					if ($productfourn->fourn_unitprice > $maxpricesupplier)
4555
					{
4556
						$maxpricesupplier = $productfourn->fourn_unitprice;
4557
					}
4558
				}
4559
4560
				$maxpricesupplier *= $conf->global->PRODUCT_MINIMUM_RECOMMENDED_PRICE;
4561
			}
4562
		}
4563
4564
		return $maxpricesupplier;
4565
	}
4566
4567
4568
	/**
4569
	 * Sets object to supplied categories.
4570
	 *
4571
	 * Deletes object from existing categories not supplied.
4572
	 * Adds it to non existing supplied categories.
4573
	 * Existing categories are left untouch.
4574
	 *
4575
	 * @param int[]|int $categories Category or categories IDs
4576
	 */
4577
	public function setCategories($categories) {
4578
		// Handle single category
4579
		if (! is_array($categories)) {
4580
			$categories = array($categories);
4581
		}
4582
4583
		// Get current categories
4584
		require_once DOL_DOCUMENT_ROOT . '/categories/class/categorie.class.php';
4585
		$c = new Categorie($this->db);
4586
		$existing = $c->containing($this->id, Categorie::TYPE_PRODUCT, 'id');
4587
4588
		// Diff
4589
		if (is_array($existing)) {
4590
			$to_del = array_diff($existing, $categories);
4591
			$to_add = array_diff($categories, $existing);
4592
		} else {
4593
			$to_del = array(); // Nothing to delete
4594
			$to_add = $categories;
4595
		}
4596
4597
		// Process
4598
		foreach($to_del as $del) {
4599
			if ($c->fetch($del) > 0) {
4600
				$c->del_type($this, 'product');
4601
			}
4602
		}
4603
		foreach ($to_add as $add) {
4604
			if ($c->fetch($add) > 0) {
4605
				$c->add_type($this, 'product');
4606
			}
4607
		}
4608
4609
		return;
4610
	}
4611
4612
	/**
4613
	 * Function used to replace a thirdparty id with another one.
4614
	 *
4615
	 * @param DoliDB 	$db 			Database handler
4616
	 * @param int 		$origin_id 		Old thirdparty id
4617
	 * @param int 		$dest_id 		New thirdparty id
4618
	 * @return bool
4619
	 */
4620
	public static function replaceThirdparty(DoliDB $db, $origin_id, $dest_id)
4621
	{
4622
		$tables = array(
4623
			'product_customer_price',
4624
			'product_customer_price_log'
4625
		);
4626
4627
		return CommonObject::commonReplaceThirdparty($db, $origin_id, $dest_id, $tables);
4628
	}
4629
4630
	/**
4631
	 * Generates prices for a product based on product multiprice generation rules
4632
	 *
4633
	 * @param User $user User that updates the prices
4634
	 * @param float $baseprice Base price
4635
	 * @param string $price_type Base price type
4636
	 * @param float $price_vat VAT % tax
4637
	 * @param int $npr NPR
4638
	 * @param string $psq ¿?
4639
	 * @return int -1 KO, 1 OK
4640
	 */
4641
	public function generateMultiprices(User $user, $baseprice, $price_type, $price_vat, $npr, $psq)
4642
	{
4643
		global $conf, $db;
4644
4645
		$sql = "SELECT rowid, level, fk_level, var_percent, var_min_percent FROM ".MAIN_DB_PREFIX."product_pricerules";
4646
		$query = $db->query($sql);
4647
4648
		$rules = array();
4649
4650
		while ($result = $db->fetch_object($query)) {
4651
			$rules[$result->level] = $result;
4652
		}
4653
4654
		//Because prices can be based on other level's prices, we temporarily store them
4655
		$prices = array(
4656
			1 => $baseprice
4657
		);
4658
4659
		for ($i = 1; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++) {
4660
4661
			$price = $baseprice;
4662
			$price_min = $baseprice;
4663
4664
			//We have to make sure it does exist and it is > 0
4665
			//First price level only allows changing min_price
4666
			if ($i > 1 && isset($rules[$i]->var_percent) && $rules[$i]->var_percent) {
4667
				$price = $prices[$rules[$i]->fk_level] * (1 + ($rules[$i]->var_percent/100));
4668
			}
4669
4670
			$prices[$i] = $price;
4671
4672
			//We have to make sure it does exist and it is > 0
4673
			if (isset($rules[$i]->var_min_percent) && $rules[$i]->var_min_percent) {
4674
				$price_min = $price * (1 - ($rules[$i]->var_min_percent/100));
4675
			}
4676
4677
			//Little check to make sure the price is modified before triggering generation
4678
			$check_amount = (($price == $this->multiprices[$i]) && ($price_min == $this->multiprices_min[$i]));
4679
			$check_type = ($baseprice == $this->multiprices_base_type[$i]);
4680
4681
			if ($check_amount && $check_type) {
4682
				continue;
4683
			}
4684
4685
			if ($this->updatePrice($price, $price_type, $user, $price_vat, $price_min, $i, $npr, $psq, true) < 0) {
4686
				return -1;
4687
			}
4688
		}
4689
4690
		return 1;
4691
	}
4692
4693
	/**
4694
	 * Returns the rights used for this class
4695
	 * @return stdClass
4696
	 */
4697
	public function getRights()
4698
	{
4699
		global $user;
4700
4701
		if ($this->isProduct()) {
4702
			return $user->rights->produit;
4703
		} else {
4704
			return $user->rights->service;
4705
		}
4706
	}
4707
4708
    /**
4709
     *  Load information for tab info
4710
     *
4711
     *  @param  int		$id     Id of thirdparty to load
4712
     *  @return	void
4713
     */
4714
    function info($id)
4715
    {
4716
        $sql = "SELECT p.rowid, p.ref, p.datec as date_creation, p.tms as date_modification,";
4717
        $sql.= " p.fk_user_author, p.fk_user_modif";
4718
        $sql.= " FROM ".MAIN_DB_PREFIX.$this->table_element." as p";
4719
        $sql.= " WHERE p.rowid = ".$id;
4720
4721
        $result=$this->db->query($sql);
4722
        if ($result)
4723
        {
4724
            if ($this->db->num_rows($result))
4725
            {
4726
                $obj = $this->db->fetch_object($result);
4727
4728
                $this->id = $obj->rowid;
4729
4730
                if ($obj->fk_user_author) {
4731
                    $cuser = new User($this->db);
4732
                    $cuser->fetch($obj->fk_user_author);
4733
                    $this->user_creation     = $cuser;
4734
                }
4735
4736
                if ($obj->fk_user_modif) {
4737
                    $muser = new User($this->db);
4738
                    $muser->fetch($obj->fk_user_modif);
4739
                    $this->user_modification = $muser;
4740
                }
4741
4742
                $this->ref			     = $obj->ref;
4743
                $this->date_creation     = $this->db->jdate($obj->date_creation);
4744
                $this->date_modification = $this->db->jdate($obj->date_modification);
4745
            }
4746
4747
            $this->db->free($result);
4748
4749
        }
4750
        else
4751
		{
4752
            dol_print_error($this->db);
4753
        }
4754
    }
4755
4756
}
4757