Passed
Push — CHECK_API ( 78ffdd...fa4f39 )
by Rafael
41:29
created

MouvementStock::_create()   F

Complexity

Conditions 107
Paths > 20000

Size

Total Lines 465
Code Lines 309

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 107
eloc 309
nc 1820790419
nop 17
dl 0
loc 465
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
3
/* Copyright (C) 2003-2006  Rodolphe Quiedeville        <[email protected]>
4
 * Copyright (C) 2005-2015  Laurent Destailleur         <[email protected]>
5
 * Copyright (C) 2011       Jean Heimburger             <[email protected]>
6
 * Copyright (C) 2014	    Cedric GROSS	            <[email protected]>
7
 * Copyright (C) 2024		MDW							<[email protected]>
8
 * Copyright (C) 2024       Frédéric France             <[email protected]>
9
 * Copyright (C) 2024       Rafael San José             <[email protected]>
10
 *
11
 * This program is free software; you can redistribute it and/or modify
12
 * it under the terms of the GNU General Public License as published by
13
 * the Free Software Foundation; either version 3 of the License, or
14
 * (at your option) any later version.
15
 *
16
 * This program is distributed in the hope that it will be useful,
17
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
 * GNU General Public License for more details.
20
 *
21
 * You should have received a copy of the GNU General Public License
22
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
23
 */
24
25
namespace Dolibarr\Code\Product\Classes;
26
27
use Dolibarr\Code\Adherents\Classes\Adherent;
28
use Dolibarr\Code\User\Classes\User;
29
use Dolibarr\Core\Base\CommonObject;
30
use DoliDB;
31
32
/**
33
 *  \file       htdocs/product/stock/class/mouvementstock.class.php
34
 *  \ingroup    stock
35
 *  \brief      File of class to manage stock movement (input or output)
36
 */
37
38
39
/**
40
 *  Class to manage stock movements
41
 */
42
class MouvementStock extends CommonObject
43
{
44
    /**
45
     * @var string Id to identify managed objects
46
     */
47
    public $element = 'stockmouvement';
48
49
    /**
50
     * @var string Name of table without prefix where object is stored
51
     */
52
    public $table_element = 'stock_mouvement';
53
54
55
    /**
56
     * @var int ID product
57
     */
58
    public $product_id;
59
60
    /**
61
     * @var int ID warehouse
62
     * @deprecated
63
     * @see $warehouse_id
64
     */
65
    public $entrepot_id;
66
67
    /**
68
     * @var int ID warehouse
69
     */
70
    public $warehouse_id;
71
72
    /**
73
     * @var float Quantity
74
     */
75
    public $qty;
76
77
    /**
78
     * @var int Type of movement
79
     * 0=input (stock increase by a manual/direct stock transfer, correction or inventory),
80
     * 1=output (stock decrease after by a manual/direct stock transfer, correction or inventory),
81
     * 2=output (stock decrease after a business event like sale, shipment or manufacturing, ...),
82
     * 3=input (stock increase after a business event like purchase, reception or manufacturing, ...)
83
     * Note that qty should be > 0 with 0 or 3, < 0 with 1 or 2.
84
     */
85
    public $type;
86
87
    public $datem = '';
88
    public $price;
89
90
    /**
91
     * @var int ID user author
92
     */
93
    public $fk_user_author;
94
95
    /**
96
     * @var string stock movements label
97
     */
98
    public $label;
99
100
    /**
101
     * @var int ID
102
     * @deprecated
103
     * @see $origin_id
104
     */
105
    public $fk_origin;
106
107
    /**
108
     * @var int     Origin id
109
     */
110
    public $origin_id;
111
112
    /**
113
     * @var string  origintype
114
     * @deprecated
115
     * see $origin_type
116
     */
117
    public $origintype;
118
119
    /**
120
     * @var string Origin type ('project', ...)
121
     */
122
    public $origin_type;
123
    public $line_id_oject_src;
124
    public $line_id_oject_origin;
125
126
127
    public $inventorycode;
128
    public $batch;
129
130
    public $line_id_object_src;
131
    public $line_id_object_origin;
132
133
    public $eatby;
134
    public $sellby;
135
136
137
138
    public $fields = array(
139
        'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 10, 'showoncombobox' => 1),
140
        'tms' => array('type' => 'timestamp', 'label' => 'DateModification', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 15),
141
        'datem' => array('type' => 'datetime', 'label' => 'Datem', 'enabled' => 1, 'visible' => -1, 'position' => 20),
142
        'fk_product' => array('type' => 'integer:Product:product/class/product.class.php:1', 'label' => 'Product', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 25),
143
        'fk_entrepot' => array('type' => 'integer:Entrepot:product/stock/class/entrepot.class.php', 'label' => 'Warehouse', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 30),
144
        'value' => array('type' => 'double', 'label' => 'Value', 'enabled' => 1, 'visible' => -1, 'position' => 35),
145
        'price' => array('type' => 'double(24,8)', 'label' => 'Price', 'enabled' => 1, 'visible' => -1, 'position' => 40),
146
        'type_mouvement' => array('type' => 'smallint(6)', 'label' => 'Type mouvement', 'enabled' => 1, 'visible' => -1, 'position' => 45),
147
        'fk_user_author' => array('type' => 'integer:User:user/class/user.class.php', 'label' => 'Fk user author', 'enabled' => 1, 'visible' => -1, 'position' => 50),
148
        'label' => array('type' => 'varchar(255)', 'label' => 'Label', 'enabled' => 1, 'visible' => -1, 'position' => 55),
149
        'fk_origin' => array('type' => 'integer', 'label' => 'Fk origin', 'enabled' => 1, 'visible' => -1, 'position' => 60),
150
        'origintype' => array('type' => 'varchar(32)', 'label' => 'Origintype', 'enabled' => 1, 'visible' => -1, 'position' => 65),
151
        'model_pdf' => array('type' => 'varchar(255)', 'label' => 'Model pdf', 'enabled' => 1, 'visible' => 0, 'position' => 70),
152
        'fk_projet' => array('type' => 'integer:Project:projet/class/project.class.php:1:(fk_statut:=:1)', 'label' => 'Project', 'enabled' => '$conf->project->enabled', 'visible' => -1, 'notnull' => 1, 'position' => 75),
153
        'inventorycode' => array('type' => 'varchar(128)', 'label' => 'InventoryCode', 'enabled' => 1, 'visible' => -1, 'position' => 80),
154
        'batch' => array('type' => 'varchar(30)', 'label' => 'Batch', 'enabled' => 1, 'visible' => -1, 'position' => 85),
155
        'eatby' => array('type' => 'date', 'label' => 'Eatby', 'enabled' => 1, 'visible' => -1, 'position' => 90),
156
        'sellby' => array('type' => 'date', 'label' => 'Sellby', 'enabled' => 1, 'visible' => -1, 'position' => 95),
157
        'fk_project' => array('type' => 'integer:Project:projet/class/project.class.php:1:(fk_statut:=:1)', 'label' => 'Fk project', 'enabled' => 1, 'visible' => -1, 'position' => 100),
158
    );
159
160
161
162
    /**
163
     *  Constructor
164
     *
165
     *  @param      DoliDB      $db      Database handler
166
     */
167
    public function __construct($db)
168
    {
169
        $this->db = $db;
170
    }
171
172
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.PublicUnderscore
173
    /**
174
     *  Add a movement of stock (in one direction only).
175
     *  This is the lowest level method to record a stock change. There is no control if warehouse is open or not.
176
     *  $this->origin_type and $this->origin_id can be also be set to save the source object of movement.
177
     *
178
     *  @param      User            $user               User object
179
     *  @param      int             $fk_product         Id of product
180
     *  @param      int             $entrepot_id        Id of warehouse
181
     *  @param      float           $qty                Qty of movement (can be <0 or >0 depending on parameter type)
182
     *  @param      int             $type               Direction of movement:
183
     *                                                  0=input (stock increase by a stock transfer), 1=output (stock decrease by a stock transfer),
184
     *                                                  2=output (stock decrease), 3=input (stock increase)
185
     *                                                  Note that qty should be > 0 with 0 or 3, < 0 with 1 or 2.
186
     *  @param      int             $price              Unit price HT of product, used to calculate average weighted price (AWP or PMP in french). If 0, average weighted price is not changed.
187
     *  @param      string          $label              Label of stock movement
188
     *  @param      string          $inventorycode      Inventory code
189
     *  @param      integer|string  $datem              Force date of movement
190
     *  @param      integer|string  $eatby              eat-by date. Will be used if lot does not exists yet and will be created.
191
     *  @param      integer|string  $sellby             sell-by date. Will be used if lot does not exists yet and will be created.
192
     *  @param      string          $batch              batch number
193
     *  @param      boolean         $skip_batch         If set to true, stock movement is done without impacting batch record
194
     *  @param      int             $id_product_batch   Id product_batch (when skip_batch is false and we already know which record of product_batch to use)
195
     *  @param      int             $disablestockchangeforsubproduct    Disable stock change for sub-products of kit (useful only if product is a subproduct)
196
     *  @param      int             $donotcleanemptylines               Do not clean lines in stock table with qty=0 (because we want to have this done by the caller)
197
     *  @param      boolean         $force_update_batch Allows to add batch stock movement even if $product doesn't use batch anymore
198
     *  @return     int|string                          Return integer <0 if KO, 0 if fk_product is null or product id does not exists, >0 if OK, or printabl result of hook
199
     */
200
    public function _create($user, $fk_product, $entrepot_id, $qty, $type, $price = 0, $label = '', $inventorycode = '', $datem = '', $eatby = '', $sellby = '', $batch = '', $skip_batch = false, $id_product_batch = 0, $disablestockchangeforsubproduct = 0, $donotcleanemptylines = 0, $force_update_batch = false)
201
    {
202
		// phpcs:enable
203
        global $conf, $langs;
204
205
206
        $error = 0;
207
        dol_syslog(get_class($this) . "::_create start userid=$user->id, fk_product=$fk_product, warehouse_id=$entrepot_id, qty=$qty, type=$type, price=$price, label=$label, inventorycode=$inventorycode, datem=" . $datem . ", eatby=" . $eatby . ", sellby=" . $sellby . ", batch=" . $batch . ", skip_batch=" . json_encode($skip_batch));
208
209
        // Call hook at beginning
210
        global $action, $hookmanager;
211
        $hookmanager->initHooks(array('mouvementstock'));
212
213
        if (is_object($hookmanager)) {
214
            $parameters = array(
215
                'currentcontext'   => 'mouvementstock',
216
                'user'             => &$user,
217
                'fk_product'       => &$fk_product,
218
                'entrepot_id'      => &$entrepot_id,
219
                'qty'              => &$qty,
220
                'type'             => &$type,
221
                'price'            => &$price,
222
                'label'            => &$label,
223
                'inventorycode'    => &$inventorycode,
224
                'datem'            => &$datem,
225
                'eatby'            => &$eatby,
226
                'sellby'           => &$sellby,
227
                'batch'            => &$batch,
228
                'skip_batch'       => &$skip_batch,
229
                'id_product_batch' => &$id_product_batch
230
            );
231
            $reshook = $hookmanager->executeHooks('stockMovementCreate', $parameters, $this, $action);    // Note that $action and $object may have been modified by some hooks
232
233
            if ($reshook < 0) {
234
                if (!empty($hookmanager->resPrint)) {
235
                    dol_print_error(null, $hookmanager->resPrint);
236
                }
237
                return $reshook;
238
            } elseif ($reshook > 0) {
239
                return $hookmanager->resPrint;
240
            }
241
        }
242
        // end hook at beginning
243
244
        // Clean parameters
245
        $price = price2num($price, 'MU'); // Clean value for the casse we receive a float zero value, to have it a real zero value.
246
        if (empty($price)) {
247
            $price = 0;
248
        }
249
        $now = (!empty($datem) ? $datem : dol_now());
250
251
        // Check parameters
252
        if (!($fk_product > 0)) {
253
            return 0;
254
        }
255
        if (!($entrepot_id > 0)) {
256
            return 0;
257
        }
258
259
        if (is_numeric($eatby) && $eatby < 0) {
260
            dol_syslog(get_class($this) . "::_create start ErrorBadValueForParameterEatBy eatby = " . $eatby);
261
            $this->errors[] = 'ErrorBadValueForParameterEatBy';
262
            return -1;
263
        }
264
        if (is_numeric($sellby) && $sellby < 0) {
265
            dol_syslog(get_class($this) . "::_create start ErrorBadValueForParameterSellBy sellby = " . $sellby);
266
            $this->errors[] = 'ErrorBadValueForParameterSellBy';
267
            return -1;
268
        }
269
270
        // Set properties of movement
271
        $this->product_id = $fk_product;
272
        $this->entrepot_id = $entrepot_id; // deprecated
273
        $this->warehouse_id = $entrepot_id;
274
        $this->qty = $qty;
275
        $this->type = $type;
276
        $this->price = price2num($price);
277
        $this->label = $label;
278
        $this->inventorycode = $inventorycode;
279
        $this->datem = $now;
280
        $this->batch = $batch;
281
282
        $mvid = 0;
283
284
        $product = new Product($this->db);
285
286
        $result = $product->fetch($fk_product);
287
        if ($result < 0) {
288
            $this->error = $product->error;
289
            $this->errors = $product->errors;
290
            dol_print_error(null, "Failed to fetch product");
291
            return -1;
292
        }
293
        if ($product->id <= 0) {    // Can happen if database is corrupted (a product id exist in stock with product that has been removed)
294
            return 0;
295
        }
296
297
        // Define if we must make the stock change (If product type is a service or if stock is used also for services)
298
        // Only record into stock tables will be disabled by this (the rest like writing into lot table or movement of subproucts are done)
299
        $movestock = 0;
300
        if ($product->type != Product::TYPE_SERVICE || getDolGlobalString('STOCK_SUPPORTS_SERVICES')) {
301
            $movestock = 1;
302
        }
303
304
        $this->db->begin();
305
306
        // Set value $product->stock_reel and detail per warehouse into $product->stock_warehouse array
307
        if ($movestock) {
308
            $product->load_stock('novirtual');
309
        }
310
311
        // Test if product require batch data. If yes, and there is not or values are not correct, we throw an error.
312
        if (isModEnabled('productbatch') && $product->hasbatch() && !$skip_batch) {
313
            if (empty($batch)) {
314
                $langs->load("errors");
315
                $this->errors[] = $langs->transnoentitiesnoconv("ErrorTryToMakeMoveOnProductRequiringBatchData", $product->ref);
316
                dol_syslog("Try to make a movement of a product with status_batch on without any batch data", LOG_ERR);
317
318
                $this->db->rollback();
319
                return -2;
320
            }
321
322
            // Check table llx_product_lot from batchnumber for same product
323
            // If found and eatby/sellby defined into table and provided and differs, return error
324
            // If found and eatby/sellby defined into table and not provided, we take value from table
325
            // If found and eatby/sellby not defined into table and provided, we update table
326
            // If found and eatby/sellby not defined into table and not provided, we do nothing
327
            // If not found, we add record
328
            $sql = "SELECT pb.rowid, pb.batch, pb.eatby, pb.sellby FROM " . $this->db->prefix() . "product_lot as pb";
329
            $sql .= " WHERE pb.fk_product = " . ((int) $fk_product) . " AND pb.batch = '" . $this->db->escape($batch) . "'";
330
331
            dol_syslog(get_class($this) . "::_create scan serial for this product to check if eatby and sellby match", LOG_DEBUG);
332
333
            $resql = $this->db->query($sql);
334
            if ($resql) {
335
                $num = $this->db->num_rows($resql);
336
                $i = 0;
337
                if ($num > 0) {
338
                    while ($i < $num) {
339
                        $obj = $this->db->fetch_object($resql);
340
                        if ($obj->eatby) {
341
                            if ($eatby) {
342
                                $tmparray = dol_getdate($eatby, true);
343
                                $eatbywithouthour = dol_mktime(0, 0, 0, $tmparray['mon'], $tmparray['mday'], $tmparray['year']);
344
                                if ($this->db->jdate($obj->eatby) != $eatby && $this->db->jdate($obj->eatby) != $eatbywithouthour) {    // We test date without hours and with hours for backward compatibility
345
                                    // If found and eatby/sellby defined into table and provided and differs, return error
346
                                    $langs->load("stocks");
347
                                    $this->errors[] = $langs->transnoentitiesnoconv("ThisSerialAlreadyExistWithDifferentDate", $batch, dol_print_date($this->db->jdate($obj->eatby), 'dayhour'), dol_print_date($eatbywithouthour, 'dayhour'));
348
                                    dol_syslog("ThisSerialAlreadyExistWithDifferentDate batch=" . $batch . ", eatby found into product_lot = " . $obj->eatby . " = " . dol_print_date($this->db->jdate($obj->eatby), 'dayhourrfc') . " so eatbywithouthour = " . $eatbywithouthour . " = " . dol_print_date($eatbywithouthour) . " - eatby provided = " . $eatby . " = " . dol_print_date($eatby, 'dayhourrfc'), LOG_ERR);
349
                                    $this->db->rollback();
350
                                    return -3;
351
                                }
352
                            } else {
353
                                $eatby = $obj->eatby; // If found and eatby/sellby defined into table and not provided, we take value from table
354
                            }
355
                        } else {
356
                            if ($eatby) { // If found and eatby/sellby not defined into table and provided, we update table
357
                                $productlot = new Productlot($this->db);
358
                                $result = $productlot->fetch($obj->rowid);
359
                                $productlot->eatby = $eatby;
360
                                $result = $productlot->update($user);
361
                                if ($result <= 0) {
362
                                    $this->error = $productlot->error;
363
                                    $this->errors = $productlot->errors;
364
                                    $this->db->rollback();
365
                                    return -5;
366
                                }
367
                            }
368
                        }
369
                        if ($obj->sellby) {
370
                            if ($sellby) {
371
                                $tmparray = dol_getdate($sellby, true);
372
                                $sellbywithouthour = dol_mktime(0, 0, 0, $tmparray['mon'], $tmparray['mday'], $tmparray['year']);
373
                                if ($this->db->jdate($obj->sellby) != $sellby && $this->db->jdate($obj->sellby) != $sellbywithouthour) {    // We test date without hours and with hours for backward compatibility
374
                                    // If found and eatby/sellby defined into table and provided and differs, return error
375
                                    $this->errors[] = $langs->transnoentitiesnoconv("ThisSerialAlreadyExistWithDifferentDate", $batch, dol_print_date($this->db->jdate($obj->sellby)), dol_print_date($sellby));
376
                                    dol_syslog($langs->transnoentities("ThisSerialAlreadyExistWithDifferentDate", $batch, dol_print_date($this->db->jdate($obj->sellby)), dol_print_date($sellby)), LOG_ERR);
377
                                    $this->db->rollback();
378
                                    return -3;
379
                                }
380
                            } else {
381
                                $sellby = $obj->sellby; // If found and eatby/sellby defined into table and not provided, we take value from table
382
                            }
383
                        } else {
384
                            if ($sellby) { // If found and eatby/sellby not defined into table and provided, we update table
385
                                $productlot = new Productlot($this->db);
386
                                $result = $productlot->fetch($obj->rowid);
387
                                $productlot->sellby = $sellby;
388
                                $result = $productlot->update($user);
389
                                if ($result <= 0) {
390
                                    $this->error = $productlot->error;
391
                                    $this->errors = $productlot->errors;
392
                                    $this->db->rollback();
393
                                    return -5;
394
                                }
395
                            }
396
                        }
397
398
                        $i++;
399
                    }
400
                } else { // If not found, we add record
401
                    $productlot = new Productlot($this->db);
402
                    $productlot->origin = !empty($this->origin_type) ? $this->origin_type : '';
0 ignored issues
show
Deprecated Code introduced by
The property Dolibarr\Core\Base\CommonObject::$origin has been deprecated: Use $origin_type and $origin_id instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

402
                    /** @scrutinizer ignore-deprecated */ $productlot->origin = !empty($this->origin_type) ? $this->origin_type : '';

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
403
                    $productlot->origin_id = !empty($this->origin_id) ? $this->origin_id : 0;
404
                    $productlot->entity = $conf->entity;
405
                    $productlot->fk_product = $fk_product;
406
                    $productlot->batch = $batch;
407
                    // If we are here = first time we manage this batch, so we used dates provided by users to create lot
408
                    $productlot->eatby = $eatby;
409
                    $productlot->sellby = $sellby;
410
                    $result = $productlot->create($user);
411
                    if ($result <= 0) {
412
                        $this->error = $productlot->error;
413
                        $this->errors = $productlot->errors;
414
                        $this->db->rollback();
415
                        return -4;
416
                    }
417
                }
418
            } else {
419
                dol_print_error($this->db);
420
                $this->db->rollback();
421
                return -1;
422
            }
423
        }
424
425
        // Check if stock is enough when qty is < 0
426
        // Note that qty should be > 0 with type 0 or 3, < 0 with type 1 or 2.
427
        if ($movestock && $qty < 0 && !getDolGlobalInt('STOCK_ALLOW_NEGATIVE_TRANSFER')) {
428
            if (isModEnabled('productbatch') && $product->hasbatch() && !$skip_batch) {
429
                $foundforbatch = 0;
430
                $qtyisnotenough = 0;
431
                if (isset($product->stock_warehouse[$entrepot_id])) {
432
                    foreach ($product->stock_warehouse[$entrepot_id]->detail_batch as $batchcursor => $prodbatch) {
433
                        if ((string) $batch != (string) $batchcursor) {        // Lot '59' must be different than lot '59c'
434
                            continue;
435
                        }
436
437
                        $foundforbatch = 1;
438
                        if ($prodbatch->qty < abs($qty)) {
439
                            $qtyisnotenough = $prodbatch->qty;
440
                        }
441
                        break;
442
                    }
443
                }
444
                if (!$foundforbatch || $qtyisnotenough) {
445
                    $langs->load("stocks");
446
                    include_once DOL_DOCUMENT_ROOT . '/product/stock/class/entrepot.class.php';
447
                    $tmpwarehouse = new Entrepot($this->db);
448
                    $tmpwarehouse->fetch($entrepot_id);
449
450
                    $this->error = $langs->trans('qtyToTranferLotIsNotEnough', $product->ref, $batch, $qtyisnotenough, $tmpwarehouse->ref);
451
                    $this->errors[] = $langs->trans('qtyToTranferLotIsNotEnough', $product->ref, $batch, $qtyisnotenough, $tmpwarehouse->ref);
452
                    $this->db->rollback();
453
                    return -8;
454
                }
455
            } else {
456
                if (isset($product->stock_warehouse[$entrepot_id]) && (empty($product->stock_warehouse[$entrepot_id]->real) || $product->stock_warehouse[$entrepot_id]->real < abs($qty))) {
457
                    $langs->load("stocks");
458
                    $this->error = $langs->trans('qtyToTranferIsNotEnough') . ' : ' . $product->ref;
459
                    $this->errors[] = $langs->trans('qtyToTranferIsNotEnough') . ' : ' . $product->ref;
460
                    $this->db->rollback();
461
                    return -8;
462
                }
463
            }
464
        }
465
466
        if ($movestock) {   // Change stock for current product, change for subproduct is done after
467
            // Set $origin_type, origin_id and fk_project
468
            $fk_project = $this->fk_project;
469
            if (!empty($this->origin_type)) {           // This is set by caller for tracking reason
470
                $origin_type = $this->origin_type;
471
                $origin_id = $this->origin_id;
472
                if (empty($fk_project) && $origin_type == 'project') {
473
                    $fk_project = $origin_id;
474
                    $origin_type = '';
475
                    $origin_id = 0;
476
                }
477
            } else {
478
                $fk_project = 0;
479
                $origin_type = '';
480
                $origin_id = 0;
481
            }
482
483
            $sql = "INSERT INTO " . $this->db->prefix() . "stock_mouvement(";
484
            $sql .= " datem, fk_product, batch, eatby, sellby,";
485
            $sql .= " fk_entrepot, value, type_mouvement, fk_user_author, label, inventorycode, price, fk_origin, origintype, fk_projet";
486
            $sql .= ")";
487
            $sql .= " VALUES ('" . $this->db->idate($this->datem) . "', " . ((int) $this->product_id) . ", ";
488
            $sql .= " " . ($batch ? "'" . $this->db->escape($batch) . "'" : "null") . ", ";
489
            $sql .= " " . ($eatby ? "'" . $this->db->idate($eatby) . "'" : "null") . ", ";
490
            $sql .= " " . ($sellby ? "'" . $this->db->idate($sellby) . "'" : "null") . ", ";
491
            $sql .= " " . ((int) $this->entrepot_id) . ", " . ((float) $this->qty) . ", " . ((int) $this->type) . ",";
492
            $sql .= " " . ((int) $user->id) . ",";
493
            $sql .= " '" . $this->db->escape($label) . "',";
494
            $sql .= " " . ($inventorycode ? "'" . $this->db->escape($inventorycode) . "'" : "null") . ",";
495
            $sql .= " " . ((float) price2num($price)) . ",";
496
            $sql .= " " . ((int) $origin_id) . ",";
497
            $sql .= " '" . $this->db->escape($origin_type) . "',";
498
            $sql .= " " . ((int) $fk_project);
499
            $sql .= ")";
500
501
            dol_syslog(get_class($this) . "::_create insert record into stock_mouvement", LOG_DEBUG);
502
            $resql = $this->db->query($sql);
503
504
            if ($resql) {
505
                $mvid = $this->db->last_insert_id($this->db->prefix() . "stock_mouvement");
506
                $this->id = $mvid;
507
            } else {
508
                $this->error = $this->db->lasterror();
509
                $this->errors[] = $this->error;
510
                $error = -1;
511
            }
512
513
            // Define current values for qty and pmp
514
            $oldqty = $product->stock_reel;
515
            $oldpmp = $product->pmp;
516
            $oldqtywarehouse = 0;
517
518
            // Test if there is already a record for couple (warehouse / product), so later we will make an update or create.
519
            $alreadyarecord = 0;
520
            if (!$error) {
521
                $sql = "SELECT rowid, reel FROM " . $this->db->prefix() . "product_stock";
522
                $sql .= " WHERE fk_entrepot = " . ((int) $entrepot_id) . " AND fk_product = " . ((int) $fk_product); // This is a unique key
523
524
                dol_syslog(get_class($this) . "::_create check if a record already exists in product_stock", LOG_DEBUG);
525
                $resql = $this->db->query($sql);
526
                if ($resql) {
527
                    $obj = $this->db->fetch_object($resql);
528
                    if ($obj) {
529
                        $alreadyarecord = 1;
530
                        $oldqtywarehouse = $obj->reel;
531
                        $fk_product_stock = $obj->rowid;
532
                    }
533
                    $this->db->free($resql);
534
                } else {
535
                    $this->errors[] = $this->db->lasterror();
536
                    $error = -2;
537
                }
538
            }
539
540
            // Calculate new AWP (PMP)
541
            $newpmp = 0;
542
            if (!$error) {
543
                if ($type == 0 || $type == 3) {
544
                    // After a stock increase
545
                    // Note: PMP is calculated on stock input only (type of movement = 0 or 3). If type == 0 or 3, qty should be > 0.
546
                    // Note: Price should always be >0 or 0. PMP should be always >0 (calculated on input)
547
                    if ($price > 0 || (getDolGlobalString('STOCK_UPDATE_AWP_EVEN_WHEN_ENTRY_PRICE_IS_NULL') && $price == 0 && in_array($this->origin_type, array('order_supplier', 'invoice_supplier')))) {
548
                        $oldqtytouse = ($oldqty >= 0 ? $oldqty : 0);
549
                        // We make a test on oldpmp>0 to avoid to use normal rule on old data with no pmp field defined
550
                        if ($oldpmp > 0) {
551
                            $newpmp = price2num((($oldqtytouse * $oldpmp) + ($qty * $price)) / ($oldqtytouse + $qty), 'MU');
552
                        } else {
553
                            $newpmp = $price; // For this product, PMP was not yet set. We set it to input price.
554
                        }
555
                        //print "oldqtytouse=".$oldqtytouse." oldpmp=".$oldpmp." oldqtywarehousetouse=".$oldqtywarehousetouse." ";
556
                        //print "qty=".$qty." newpmp=".$newpmp;
557
                        //exit;
558
                    } else {
559
                        $newpmp = $oldpmp;
560
                    }
561
                } else {
562
                    // ($type == 1 || $type == 2)
563
                    //   -> After a stock decrease, we don't change value of the AWP/PMP of a product.
564
                    // else
565
                    //   Type of movement unknown
566
                    $newpmp = $oldpmp;
567
                }
568
            }
569
            // Update stock quantity
570
            if (!$error) {
571
                if ($alreadyarecord > 0) {
572
                    $sql = "UPDATE " . $this->db->prefix() . "product_stock SET reel = " . ((float) $oldqtywarehouse + (float) $qty);
573
                    $sql .= " WHERE fk_entrepot = " . ((int) $entrepot_id) . " AND fk_product = " . ((int) $fk_product);
574
                } else {
575
                    $sql = "INSERT INTO " . $this->db->prefix() . "product_stock";
576
                    $sql .= " (reel, fk_entrepot, fk_product) VALUES ";
577
                    $sql .= " (" . ((float) $qty) . ", " . ((int) $entrepot_id) . ", " . ((int) $fk_product) . ")";
578
                }
579
580
                dol_syslog(get_class($this) . "::_create update stock value", LOG_DEBUG);
581
                $resql = $this->db->query($sql);
582
                if (!$resql) {
583
                    $this->errors[] = $this->db->lasterror();
584
                    $error = -3;
585
                } elseif (empty($fk_product_stock)) {
586
                    $fk_product_stock = $this->db->last_insert_id($this->db->prefix() . "product_stock");
587
                }
588
            }
589
590
            // Update detail of stock for the lot.
591
            if (!$error && isModEnabled('productbatch') && (($product->hasbatch() && !$skip_batch) || $force_update_batch)) {
592
                if ($id_product_batch > 0) {
593
                    $result = $this->createBatch($id_product_batch, $qty);
594
                    if ($result == -2 && $fk_product_stock > 0) {   // The entry for this product batch does not exists anymore, bu we already have a llx_product_stock, so we recreate the batch entry in product_batch
595
                        $param_batch = array('fk_product_stock' => $fk_product_stock, 'batchnumber' => $batch);
596
                        $result = $this->createBatch($param_batch, $qty);
597
                    }
598
                } else {
599
                    $param_batch = array('fk_product_stock' => $fk_product_stock, 'batchnumber' => $batch);
600
                    $result = $this->createBatch($param_batch, $qty);
601
                }
602
                if ($result < 0) {
603
                    $error++;
604
                }
605
            }
606
607
            // Update PMP and denormalized value of stock qty at product level
608
            if (!$error) {
609
                $newpmp = price2num($newpmp, 'MU');
610
611
                // $sql = "UPDATE ".$this->db->prefix()."product SET pmp = ".$newpmp.", stock = ".$this->db->ifsql("stock IS NULL", 0, "stock") . " + ".$qty;
612
                // $sql.= " WHERE rowid = ".((int) $fk_product);
613
                // Update pmp + denormalized fields because we change content of produt_stock. Warning: Do not use "SET p.stock", does not works with pgsql
614
                $sql = "UPDATE " . $this->db->prefix() . "product as p SET pmp = " . ((float) $newpmp) . ",";
615
                $sql .= " stock=(SELECT SUM(ps.reel) FROM " . $this->db->prefix() . "product_stock as ps WHERE ps.fk_product = p.rowid)";
616
                $sql .= " WHERE rowid = " . ((int) $fk_product);
617
618
                dol_syslog(get_class($this) . "::_create update AWP", LOG_DEBUG);
619
                $resql = $this->db->query($sql);
620
                if (!$resql) {
621
                    $this->errors[] = $this->db->lasterror();
622
                    $error = -4;
623
                }
624
            }
625
626
            if (empty($donotcleanemptylines)) {
627
                // If stock is now 0, we can remove entry into llx_product_stock, but only if there is no child lines into llx_product_batch (detail of batch, because we can imagine
628
                // having a lot1/qty=X and lot2/qty=-X, so 0 but we must not loose repartition of different lot.
629
                $sql = "DELETE FROM " . $this->db->prefix() . "product_stock WHERE reel = 0 AND rowid NOT IN (SELECT fk_product_stock FROM " . $this->db->prefix() . "product_batch as pb)";
630
                $resql = $this->db->query($sql);
631
                // We do not test error, it can fails if there is child in batch details
632
            }
633
        }
634
635
        // Add movement for sub products (recursive call)
636
        if (!$error && getDolGlobalString('PRODUIT_SOUSPRODUITS') && !getDolGlobalString('INDEPENDANT_SUBPRODUCT_STOCK') && empty($disablestockchangeforsubproduct)) {
637
            $error = $this->_createSubProduct($user, $fk_product, $entrepot_id, $qty, $type, 0, $label, $inventorycode, $datem); // we use 0 as price, because AWP must not change for subproduct
638
        }
639
640
        if ($movestock && !$error) {
641
            // Call trigger
642
            $result = $this->call_trigger('STOCK_MOVEMENT', $user);
643
            if ($result < 0) {
644
                $error++;
645
            }
646
            // End call triggers
647
            // Check unicity for serial numbered equipment once all movement were done.
648
            if (!$error && isModEnabled('productbatch') && $product->hasbatch() && !$skip_batch) {
649
                if ($product->status_batch == 2 && $qty > 0) {  // We check only if we increased qty
650
                    if ($this->getBatchCount($fk_product, $batch) > 1) {
651
                        $error++;
652
                        $this->errors[] = $langs->trans("TooManyQtyForSerialNumber", $product->ref, $batch);
653
                    }
654
                }
655
            }
656
        }
657
658
        if (!$error) {
659
            $this->db->commit();
660
            return $mvid;
661
        } else {
662
            $this->db->rollback();
663
            dol_syslog(get_class($this) . "::_create error code=" . $error, LOG_ERR);
664
            return -6;
665
        }
666
    }
667
668
669
670
    /**
671
     * Load object in memory from the database
672
     *
673
     * @param int    $id  Id object
674
     *
675
     * @return int Return integer <0 if KO, 0 if not found, >0 if OK
676
     */
677
    public function fetch($id)
678
    {
679
        dol_syslog(__METHOD__, LOG_DEBUG);
680
681
        $sql = "SELECT";
682
        $sql .= " t.rowid,";
683
        $sql .= " t.tms,";
684
        $sql .= " t.datem,";
685
        $sql .= " t.fk_product,";
686
        $sql .= " t.fk_entrepot,";
687
        $sql .= " t.value,";
688
        $sql .= " t.price,";
689
        $sql .= " t.type_mouvement,";
690
        $sql .= " t.fk_user_author,";
691
        $sql .= " t.label,";
692
        $sql .= " t.fk_origin as origin_id,";
693
        $sql .= " t.origintype as origin_type,";
694
        $sql .= " t.inventorycode,";
695
        $sql .= " t.batch,";
696
        $sql .= " t.eatby,";
697
        $sql .= " t.sellby,";
698
        $sql .= " t.fk_projet as fk_project";
699
        $sql .= " FROM " . $this->db->prefix() . $this->table_element . " as t";
700
        $sql .= " WHERE t.rowid = " . ((int) $id);
701
702
        $resql = $this->db->query($sql);
703
        if ($resql) {
704
            $numrows = $this->db->num_rows($resql);
705
            if ($numrows) {
706
                $obj = $this->db->fetch_object($resql);
707
708
                $this->id = $obj->rowid;
709
710
                $this->product_id = $obj->fk_product;
711
                $this->warehouse_id = $obj->fk_entrepot;
712
                $this->qty = $obj->value;
713
                $this->type = $obj->type_mouvement;
714
715
                $this->tms = $this->db->jdate($obj->tms);
0 ignored issues
show
Deprecated Code introduced by
The property Dolibarr\Core\Base\CommonObject::$tms has been deprecated: Use $date_modification ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

715
                /** @scrutinizer ignore-deprecated */ $this->tms = $this->db->jdate($obj->tms);

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
Documentation Bug introduced by
It seems like $this->db->jdate($obj->tms) can also be of type string. However, the property $tms 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...
716
                $this->datem = $this->db->jdate($obj->datem);
717
                $this->price = $obj->price;
718
                $this->fk_user_author = $obj->fk_user_author;
719
                $this->label = $obj->label;
720
                $this->fk_origin = $obj->origin_id;     // For backward compatibility
721
                $this->origintype = $obj->origin_type;  // For backward compatibility
722
                $this->origin_id = $obj->origin_id;
723
                $this->origin_type = $obj->origin_type;
724
                $this->inventorycode = $obj->inventorycode;
725
                $this->batch = $obj->batch;
726
                $this->eatby = $this->db->jdate($obj->eatby);
727
                $this->sellby = $this->db->jdate($obj->sellby);
728
                $this->fk_project = $obj->fk_project;
729
            }
730
731
            // Retrieve all extrafield
732
            $this->fetch_optionals();
733
734
            // $this->fetch_lines();
735
736
            $this->db->free($resql);
737
738
            if ($numrows) {
739
                return 1;
740
            } else {
741
                return 0;
742
            }
743
        } else {
744
            $this->errors[] = 'Error ' . $this->db->lasterror();
745
            dol_syslog(__METHOD__ . ' ' . implode(',', $this->errors), LOG_ERR);
746
747
            return -1;
748
        }
749
    }
750
751
752
753
754
    /**
755
     *  Create movement in database for all subproducts
756
     *
757
     *  @param      User            $user           Object user
758
     *  @param      int             $idProduct      Id product
759
     *  @param      int             $entrepot_id    Warehouse id
760
     *  @param      float           $qty            Quantity
761
     *  @param      int             $type           Type
762
     *  @param      int             $price          Price
763
     *  @param      string          $label          Label of movement
764
     *  @param      string          $inventorycode  Inventory code
765
     *  @param      integer|string  $datem          Force date of movement
766
     *  @return     int             Return integer <0 if KO, 0 if OK
767
     */
768
    private function _createSubProduct($user, $idProduct, $entrepot_id, $qty, $type, $price = 0, $label = '', $inventorycode = '', $datem = '')
769
    {
770
        global $langs;
771
772
        $error = 0;
773
        $pids = array();
774
        $pqtys = array();
775
776
        $sql = "SELECT fk_product_pere, fk_product_fils, qty";
777
        $sql .= " FROM " . $this->db->prefix() . "product_association";
778
        $sql .= " WHERE fk_product_pere = " . ((int) $idProduct);
779
        $sql .= " AND incdec = 1";
780
781
        dol_syslog(get_class($this) . "::_createSubProduct for parent product " . $idProduct, LOG_DEBUG);
782
        $resql = $this->db->query($sql);
783
        if ($resql) {
784
            $i = 0;
785
            while ($obj = $this->db->fetch_object($resql)) {
786
                $pids[$i] = $obj->fk_product_fils;
787
                $pqtys[$i] = $obj->qty;
788
                $i++;
789
            }
790
            $this->db->free($resql);
791
        } else {
792
            $error = -2;
793
        }
794
795
        // Create movement for each subproduct
796
        foreach ($pids as $key => $value) {
797
            if (!$error) {
798
                $tmpmove = dol_clone($this, 1);
799
800
                $result = $tmpmove->_create($user, $pids[$key], $entrepot_id, ($qty * $pqtys[$key]), $type, 0, $label, $inventorycode, $datem); // This will also call _createSubProduct making this recursive
801
                if ($result < 0) {
802
                    $this->error = $tmpmove->error;
803
                    $this->errors = array_merge($this->errors, $tmpmove->errors);
804
                    if ($result == -2) {
805
                        $this->errors[] = $langs->trans("ErrorNoteAlsoThatSubProductCantBeFollowedByLot");
806
                    }
807
                    $error = $result;
808
                }
809
                unset($tmpmove);
810
            }
811
        }
812
813
        return $error;
814
    }
815
816
817
    /**
818
     *  Decrease stock for product and subproducts
819
     *
820
     *  @param      User            $user                   Object user
821
     *  @param      int             $fk_product             Id product
822
     *  @param      int             $entrepot_id            Warehouse id
823
     *  @param      float           $qty                    Quantity
824
     *  @param      int             $price                  Price
825
     *  @param      string          $label                  Label of stock movement
826
     *  @param      int|string      $datem                  Force date of movement
827
     *  @param      int|string      $eatby                  eat-by date
828
     *  @param      int|string      $sellby                 sell-by date
829
     *  @param      string          $batch                  batch number
830
     *  @param      int             $id_product_batch       Id product_batch
831
     *  @param      string          $inventorycode          Inventory code
832
     *  @param      int             $donotcleanemptylines   Do not clean lines that remains in stock table with qty=0 (because we want to have this done by the caller)
833
     *  @return     int                                     Return integer <0 if KO, >0 if OK
834
     */
835
    public function livraison($user, $fk_product, $entrepot_id, $qty, $price = 0, $label = '', $datem = '', $eatby = '', $sellby = '', $batch = '', $id_product_batch = 0, $inventorycode = '', $donotcleanemptylines = 0)
836
    {
837
        global $conf;
838
839
        $skip_batch = empty($conf->productbatch->enabled);
840
841
        return $this->_create($user, $fk_product, $entrepot_id, (0 - $qty), 2, $price, $label, $inventorycode, $datem, $eatby, $sellby, $batch, $skip_batch, $id_product_batch, 0, $donotcleanemptylines);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_create($u... $donotcleanemptylines) also could return the type string which is incompatible with the documented return type integer.
Loading history...
842
    }
843
844
    /**
845
     *  Increase stock for product and subproducts
846
     *
847
     *  @param      User            $user                   Object user
848
     *  @param      int             $fk_product             Id product
849
     *  @param      int             $entrepot_id            Warehouse id
850
     *  @param      float           $qty                    Quantity
851
     *  @param      int             $price                  Price
852
     *  @param      string          $label                  Label of stock movement
853
     *  @param      integer|string  $eatby                  eat-by date
854
     *  @param      integer|string  $sellby                 sell-by date
855
     *  @param      string          $batch                  batch number
856
     *  @param      integer|string  $datem                  Force date of movement
857
     *  @param      int             $id_product_batch       Id product_batch
858
     *  @param      string          $inventorycode          Inventory code
859
     *  @param      int             $donotcleanemptylines   Do not clean lines that remains in stock table with qty=0 (because we want to have this done by the caller)
860
     *  @return     int                                     Return integer <0 if KO, >0 if OK
861
     */
862
    public function reception($user, $fk_product, $entrepot_id, $qty, $price = 0, $label = '', $eatby = '', $sellby = '', $batch = '', $datem = '', $id_product_batch = 0, $inventorycode = '', $donotcleanemptylines = 0)
863
    {
864
        global $conf;
865
866
        $skip_batch = empty($conf->productbatch->enabled);
867
868
        return $this->_create($user, $fk_product, $entrepot_id, $qty, 3, $price, $label, $inventorycode, $datem, $eatby, $sellby, $batch, $skip_batch, $id_product_batch, 0, $donotcleanemptylines);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_create($u... $donotcleanemptylines) also could return the type string which is incompatible with the documented return type integer.
Loading history...
869
    }
870
871
    /**
872
     * Count number of product in stock before a specific date
873
     *
874
     * @param   int         $productidselected      Id of product to count
875
     * @param   integer     $datebefore             Date limit
876
     * @return  int         Number
877
     */
878
    public function calculateBalanceForProductBefore($productidselected, $datebefore)
879
    {
880
        $nb = 0;
881
882
        $sql = "SELECT SUM(value) as nb from " . $this->db->prefix() . "stock_mouvement";
883
        $sql .= " WHERE fk_product = " . ((int) $productidselected);
884
        $sql .= " AND datem < '" . $this->db->idate($datebefore) . "'";
885
886
        dol_syslog(get_class($this) . __METHOD__, LOG_DEBUG);
887
        $resql = $this->db->query($sql);
888
        if ($resql) {
889
            $obj = $this->db->fetch_object($resql);
890
            if ($obj) {
891
                $nb = $obj->nb;
892
            }
893
            return (empty($nb) ? 0 : $nb);
894
        } else {
895
            dol_print_error($this->db);
896
            return -1;
897
        }
898
    }
899
900
    /**
901
     * Create or update batch record (update table llx_product_batch). No check is done here, done by parent.
902
     *
903
     * @param   array|int   $dluo         Could be either
904
     *                                    - int if row id of product_batch table (for update)
905
     *                                    - or complete array('fk_product_stock'=>, 'batchnumber'=>)
906
     * @param   float       $qty          Quantity of product with batch number. May be a negative amount.
907
     * @return  int                       Return integer <0 if KO, -2 if we try to update a product_batchid that does not exist, else return productbatch id
908
     */
909
    private function createBatch($dluo, $qty)
910
    {
911
        global $user, $langs;
912
913
        $langs->load('productbatch');
914
915
        $pdluo = new Productbatch($this->db);
916
917
        $result = 0;
918
919
        // Try to find an existing record with same batch number or id
920
        if (is_numeric($dluo)) {
921
            $result = $pdluo->fetch($dluo);
922
            if (empty($pdluo->id)) {
923
                // We didn't find the line. May be it was deleted before by a previous move in same transaction.
924
                $this->error = $langs->trans('CantMoveNonExistantSerial');
925
                $this->errors[] = $this->error;
926
                $result = -2;
927
            }
928
        } elseif (is_array($dluo)) {
929
            if (isset($dluo['fk_product_stock'])) {
930
                $vfk_product_stock = $dluo['fk_product_stock'];
931
                $vbatchnumber = $dluo['batchnumber'];
932
933
                $result = $pdluo->find($vfk_product_stock, '', '', $vbatchnumber); // Search on batch number only (eatby and sellby are deprecated here)
934
            } else {
935
                dol_syslog(get_class($this) . "::createBatch array param dluo must contain at least key fk_product_stock", LOG_ERR);
936
                $result = -1;
937
            }
938
        } else {
939
            dol_syslog(get_class($this) . "::createBatch error invalid param dluo", LOG_ERR);
940
            $result = -1;
941
        }
942
943
        if ($result >= 0) {
944
            // No error
945
            if ($pdluo->id > 0) {   // product_batch record found
946
                //print "Avant ".$pdluo->qty." Apres ".($pdluo->qty + $qty)."<br>";
947
                $pdluo->qty += $qty;
948
                if ($pdluo->qty == 0) {
949
                    $result = $pdluo->delete($user, 1);
950
                } else {
951
                    $result = $pdluo->update($user, 1);
952
                }
953
            } else {                    // product_batch record not found
954
                $pdluo->fk_product_stock = $vfk_product_stock;
955
                $pdluo->qty = $qty;
956
                $pdluo->eatby = empty($dluo['eatby']) ? '' : $dluo['eatby'];        // No more used. Now eatby date is store in table of lot, no more into prouct_batch table.
957
                $pdluo->sellby = empty($dluo['sellby']) ? '' : $dluo['sellby'];     // No more used. Now sellby date is store in table of lot, no more into prouct_batch table.
958
                $pdluo->batch = $vbatchnumber;
959
960
                $result = $pdluo->create($user, 1);
961
                if ($result < 0) {
962
                    $this->error = $pdluo->error;
963
                    $this->errors = $pdluo->errors;
964
                }
965
            }
966
        }
967
968
        return $result;
969
    }
970
971
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
972
    /**
973
     * Return Url link of origin object
974
     *
975
     * @param  int      $origin_id      Id origin
976
     * @param  string   $origin_type    Type origin ('project', 'xxx@MODULENAME', etc)
977
     * @return string
978
     */
979
    public function get_origin($origin_id, $origin_type)
980
    {
981
		// phpcs:enable
982
        $origin = '';
983
984
        switch ($origin_type) {
985
            case 'commande':
986
                $origin = new Commande($this->db);
987
                break;
988
            case 'shipping':
989
                $origin = new Expedition($this->db);
990
                break;
991
            case 'facture':
992
                $origin = new Facture($this->db);
993
                break;
994
            case 'order_supplier':
995
                $origin = new CommandeFournisseur($this->db);
996
                break;
997
            case 'invoice_supplier':
998
                $origin = new FactureFournisseur($this->db);
999
                break;
1000
            case 'project':
1001
                $origin = new Project($this->db);
1002
                break;
1003
            case 'mo':
1004
                            $origin = new Mo($this->db);
1005
                break;
1006
            case 'user':
1007
                $origin = new User($this->db);
1008
                break;
1009
            case 'reception':
1010
                $origin = new Reception($this->db);
1011
                break;
1012
            case 'inventory':
1013
                require_once constant('DOL_DOCUMENT_ROOT') . '/product/inventory/class/inventory.class.php';
1014
                $origin = new Inventory($this->db);
1015
                break;
1016
            default:
1017
                if ($origin_type) {
1018
                    // Separate origin_type with "@" : left part is class name, right part is module name
1019
                    $origin_type_array = explode('@', $origin_type);
1020
                    $classname = $origin_type_array[0];
1021
                    $modulename = empty($origin_type_array[1]) ? strtolower($classname) : $origin_type_array[1];
1022
1023
                    $result = dol_include_once('/' . $modulename . '/class/' . $classname . '.class.php');
1024
1025
                    if ($result) {
1026
                        $classname = ucfirst($classname);
1027
                        $origin = new $classname($this->db);
1028
                    }
1029
                }
1030
                break;
1031
        }
1032
1033
        if (empty($origin) || !is_object($origin)) {
1034
            return '';
1035
        }
1036
1037
        if ($origin->fetch($origin_id) > 0) {
1038
            return $origin->getNomUrl(1);
1039
        }
1040
1041
        return '';
1042
    }
1043
1044
    /**
1045
     * Set attribute origin_type and fk_origin to object
1046
     *
1047
     * @param   string  $origin_element     Type of element
1048
     * @param   int     $origin_id          Id of element
1049
     * @param   int     $line_id_object_src Id line of element Source
1050
     * @param   int     $line_id_object_origin  Id line of element Origin
1051
     *
1052
     * @return  void
1053
     */
1054
    public function setOrigin($origin_element, $origin_id, $line_id_object_src = 0, $line_id_object_origin = 0)
1055
    {
1056
        $this->origin_type = $origin_element;
1057
        $this->origin_id = $origin_id;
1058
        $this->line_id_object_src = $line_id_object_src;
1059
        $this->line_id_object_origin = $line_id_object_origin;
1060
        // For backward compatibility
1061
        $this->origintype = $origin_element;
1062
        $this->fk_origin = $origin_id;
1063
    }
1064
1065
1066
    /**
1067
     *  Initialise an instance with random values.
1068
     *  Used to build previews or test instances.
1069
     *  id must be 0 if object instance is a specimen.
1070
     *
1071
     *  @return int
1072
     */
1073
    public function initAsSpecimen()
1074
    {
1075
        // Initialize parameters
1076
        $this->id = 0;
1077
1078
        // There is no specific properties. All data into insert are provided as method parameter.
1079
1080
        return 1;
1081
    }
1082
1083
    /**
1084
     *  Return html string with picto for type of movement
1085
     *
1086
     *  @param  int     $withlabel          With label
1087
     *  @return string                      String with URL
1088
     */
1089
    public function getTypeMovement($withlabel = 0)
1090
    {
1091
        global $langs;
1092
1093
        $s = '';
1094
        switch ($this->type) {
1095
            case "0":
1096
                $s = '<span class="fa fa-level-down-alt stockmovemententry stockmovementtransfer" title="' . $langs->trans('StockIncreaseAfterCorrectTransfer') . '"></span>';
1097
                if ($withlabel) {
1098
                    $s .= $langs->trans('StockIncreaseAfterCorrectTransfer');
1099
                }
1100
                break;
1101
            case "1":
1102
                $s = '<span class="fa fa-level-up-alt stockmovementexit stockmovementtransfer" title="' . $langs->trans('StockDecreaseAfterCorrectTransfer') . '"></span>';
1103
                if ($withlabel) {
1104
                    $s .= $langs->trans('StockDecreaseAfterCorrectTransfer');
1105
                }
1106
                break;
1107
            case "2":
1108
                $s = '<span class="fa fa-long-arrow-alt-up stockmovementexit stockmovement" title="' . $langs->trans('StockDecrease') . '"></span>';
1109
                if ($withlabel) {
1110
                    $s .= $langs->trans('StockDecrease');
1111
                }
1112
                break;
1113
            case "3":
1114
                $s = '<span class="fa fa-long-arrow-alt-down stockmovemententry stockmovement" title="' . $langs->trans('StockIncrease') . '"></span>';
1115
                if ($withlabel) {
1116
                    $s .= $langs->trans('StockIncrease');
1117
                }
1118
                break;
1119
        }
1120
1121
        return $s;
1122
    }
1123
1124
    /**
1125
     *  Return a link (with optionally the picto)
1126
     *  Use this->id,this->lastname, this->firstname
1127
     *
1128
     *  @param  int     $withpicto          Include picto in link (0=No picto, 1=Include picto into link, 2=Only picto)
1129
     *  @param  string  $option             On what the link point to ('' = Tab of stock movement of warehouse, 'movements' = list of movements)
1130
     *  @param  integer $notooltip          1=Disable tooltip
1131
     *  @param  int     $maxlen             Max length of visible user name
1132
     *  @param  string  $morecss            Add more css on link
1133
     *  @return string                      String with URL
1134
     */
1135
    public function getNomUrl($withpicto = 0, $option = '', $notooltip = 0, $maxlen = 24, $morecss = '')
1136
    {
1137
        global $langs, $conf, $db;
1138
1139
        $result = '';
1140
1141
        $label = img_picto('', 'stock', 'class="pictofixedwidth"') . '<u>' . $langs->trans("StockMovement") . '</u>';
1142
        $label .= '<div width="100%">';
1143
        $label .= '<b>' . $langs->trans('Ref') . ':</b> ' . $this->id;
1144
        $label .= '<br><b>' . $langs->trans('Label') . ':</b> ' . $this->label;
1145
        $qtylabel = (($this->qty > 0) ? '<span class="stockmovemententry">+' : '<span class="stockmovementexit">') . $this->qty . '</span>';
1146
        $label .= '<br><b>' . $langs->trans('Qty') . ':</b> ' . $qtylabel;
1147
        if ($this->batch) {
1148
            $label .= '<br><b>' . $langs->trans('Batch') . ':</b> ' . $this->batch;
1149
        }
1150
        /* TODO Get also warehouse label in a property instead of id
1151
        if ($this->warehouse_id > 0) {
1152
            $label .= '<br><b>'.$langs->trans('Warehouse').':</b> '.$this->warehouse_id;
1153
        }*/
1154
        $label .= '</div>';
1155
1156
        // Link to page of warehouse tab
1157
        if ($option == 'movements') {
1158
            $url = constant('BASE_URL') . '/product/stock/movement_list.php?search_ref=' . $this->id;
1159
        } else {
1160
            $url = constant('BASE_URL') . '/product/stock/movement_list.php?id=' . $this->warehouse_id . '&msid=' . $this->id;
1161
        }
1162
1163
        $link = '<a href="' . $url . '"' . ($notooltip ? '' : ' title="' . dol_escape_htmltag($label, 1) . '" class="classfortooltip' . ($morecss ? ' ' . $morecss : '') . '"');
1164
        $link .= '>';
1165
        $linkend = '</a>';
1166
1167
        if ($withpicto) {
1168
            $result .= ($link . img_object(($notooltip ? '' : $label), 'stock', ($notooltip ? '' : 'class="classfortooltip"')) . $linkend);
1169
            if ($withpicto != 2) {
1170
                $result .= ' ';
1171
            }
1172
        }
1173
        $result .= $link . $this->id . $linkend;
1174
        return $result;
1175
    }
1176
1177
    /**
1178
     *  Return label statut
1179
     *
1180
     *  @param  int     $mode          0=libelle long, 1=libelle court, 2=Picto + Libelle court, 3=Picto, 4=Picto + Libelle long, 5=Libelle court + Picto
1181
     *  @return string                 Label of status
1182
     */
1183
    public function getLibStatut($mode = 0)
1184
    {
1185
        return $this->LibStatut($mode);
1186
    }
1187
1188
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1189
    /**
1190
     *  Return the label of the status
1191
     *
1192
     *  @param  int     $mode          0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=Short label + Picto, 6=Long label + Picto
1193
     *  @return string                 Label of status
1194
     */
1195
    public function LibStatut($mode = 0)
1196
    {
1197
		// phpcs:enable
1198
        global $langs;
1199
1200
        if ($mode == 0 || $mode == 1) {
1201
            return $langs->trans('StatusNotApplicable');
1202
        } elseif ($mode == 2) {
1203
            return img_picto($langs->trans('StatusNotApplicable'), 'statut9') . ' ' . $langs->trans('StatusNotApplicable');
1204
        } elseif ($mode == 3) {
1205
            return img_picto($langs->trans('StatusNotApplicable'), 'statut9');
1206
        } elseif ($mode == 4) {
1207
            return img_picto($langs->trans('StatusNotApplicable'), 'statut9') . ' ' . $langs->trans('StatusNotApplicable');
1208
        } elseif ($mode == 5) {
1209
            return $langs->trans('StatusNotApplicable') . ' ' . img_picto($langs->trans('StatusNotApplicable'), 'statut9');
1210
        }
1211
1212
        return 'Bad value for mode';
1213
    }
1214
1215
    /**
1216
     *  Create object on disk
1217
     *
1218
     *  @param     string       $modele         force le modele a utiliser ('' to not force)
1219
     *  @param     Translate    $outputlangs    Object langs to use for output
1220
     *  @param     int          $hidedetails    Hide details of lines
1221
     *  @param     int          $hidedesc       Hide description
1222
     *  @param     int          $hideref        Hide ref
1223
     *  @return    int                          0 if KO, 1 if OK
1224
     */
1225
    public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0)
1226
    {
1227
        global $conf, $user, $langs;
1228
1229
        $langs->load("stocks");
1230
        $outputlangs->load("products");
1231
1232
        if (!dol_strlen($modele)) {
1233
            $modele = 'stdmovement';
1234
1235
            if ($this->model_pdf) {
1236
                $modele = $this->model_pdf;
1237
            } elseif (getDolGlobalString('MOUVEMENT_ADDON_PDF')) {
1238
                $modele = getDolGlobalString('MOUVEMENT_ADDON_PDF');
1239
            }
1240
        }
1241
1242
        $modelpath = "core/modules/stock/doc/";
1243
1244
        return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref);
1245
    }
1246
1247
    /**
1248
     * Delete object in database
1249
     *
1250
     * @param User  $user       User that deletes
1251
     * @param int   $notrigger  0=launch triggers after, 1=disable triggers
1252
     * @return int              Return integer <0 if KO, >0 if OK
1253
     */
1254
    public function delete(User $user, $notrigger = 0)
1255
    {
1256
        return $this->deleteCommon($user, $notrigger);
1257
        //return $this->deleteCommon($user, $notrigger, 1);
1258
    }
1259
1260
    /**
1261
     * Retrieve number of equipment for a product lot/serial
1262
     *
1263
     * @param   int         $fk_product     Product id
1264
     * @param   string      $batch          batch number
1265
     * @return  int                         Return integer <0 if KO, number of equipment found if OK
1266
     */
1267
    private function getBatchCount($fk_product, $batch)
1268
    {
1269
        $cpt = 0;
1270
1271
        $sql = "SELECT sum(pb.qty) as cpt";
1272
        $sql .= " FROM " . $this->db->prefix() . "product_batch as pb";
1273
        $sql .= " INNER JOIN " . $this->db->prefix() . "product_stock as ps ON ps.rowid = pb.fk_product_stock";
1274
        $sql .= " WHERE ps.fk_product = " . ((int) $fk_product);
1275
        $sql .= " AND pb.batch = '" . $this->db->escape($batch) . "'";
1276
1277
        $result = $this->db->query($sql);
1278
        if ($result) {
1279
            if ($this->db->num_rows($result)) {
1280
                $obj = $this->db->fetch_object($result);
1281
                $cpt = $obj->cpt;
1282
            }
1283
1284
            $this->db->free($result);
1285
        } else {
1286
            dol_print_error($this->db);
1287
            return -1;
1288
        }
1289
1290
        return $cpt;
1291
    }
1292
1293
    /**
1294
     * reverse movement for object by updating infos
1295
     * @return int    1 if OK,-1 if KO
1296
     */
1297
    public function reverseMouvement()
1298
    {
1299
        $formattedDate = "REVERTMV" . dol_print_date($this->datem, '%Y%m%d%His');
1300
        if ($this->label == 'Annulation movement ID' . $this->id) {
1301
            return -1;
1302
        }
1303
        if ($this->inventorycode == $formattedDate) {
1304
            return -1;
1305
        }
1306
1307
        $sql = "UPDATE " . $this->db->prefix() . "stock_mouvement SET";
1308
        $sql .= " label = 'Annulation movement ID " . ((int) $this->id) . "',";
1309
        $sql .= "inventorycode = '" . ($formattedDate) . "'";
1310
        $sql .= " WHERE rowid = " . ((int) $this->id);
1311
1312
        $resql = $this->db->query($sql);
1313
1314
        if ($resql) {
1315
            $this->db->commit();
1316
            return 1;
1317
        } else {
1318
            $this->db->rollback();
1319
            return -1;
1320
        }
1321
    }
1322
}
1323