Passed
Push — EXTRACT_CLASSES ( a2ff75...ae6b5c )
by Rafael
34:15
created

MouvementStock::_create()   F

Complexity

Conditions 107
Paths > 20000

Size

Total Lines 467
Code Lines 311

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 107
eloc 311
nc 1820790419
nop 17
dl 0
loc 467
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\Core\Base\CommonObject;
28
29
/**
30
 *  \file       htdocs/product/stock/class/mouvementstock.class.php
31
 *  \ingroup    stock
32
 *  \brief      File of class to manage stock movement (input or output)
33
 */
34
35
36
/**
37
 *  Class to manage stock movements
38
 */
39
class MouvementStock extends CommonObject
40
{
41
    /**
42
     * @var string Id to identify managed objects
43
     */
44
    public $element = 'stockmouvement';
45
46
    /**
47
     * @var string Name of table without prefix where object is stored
48
     */
49
    public $table_element = 'stock_mouvement';
50
51
52
    /**
53
     * @var int ID product
54
     */
55
    public $product_id;
56
57
    /**
58
     * @var int ID warehouse
59
     * @deprecated
60
     * @see $warehouse_id
61
     */
62
    public $entrepot_id;
63
64
    /**
65
     * @var int ID warehouse
66
     */
67
    public $warehouse_id;
68
69
    /**
70
     * @var float Quantity
71
     */
72
    public $qty;
73
74
    /**
75
     * @var int Type of movement
76
     * 0=input (stock increase by a manual/direct stock transfer, correction or inventory),
77
     * 1=output (stock decrease after by a manual/direct stock transfer, correction or inventory),
78
     * 2=output (stock decrease after a business event like sale, shipment or manufacturing, ...),
79
     * 3=input (stock increase after a business event like purchase, reception or manufacturing, ...)
80
     * Note that qty should be > 0 with 0 or 3, < 0 with 1 or 2.
81
     */
82
    public $type;
83
84
    public $datem = '';
85
    public $price;
86
87
    /**
88
     * @var int ID user author
89
     */
90
    public $fk_user_author;
91
92
    /**
93
     * @var string stock movements label
94
     */
95
    public $label;
96
97
    /**
98
     * @var int ID
99
     * @deprecated
100
     * @see $origin_id
101
     */
102
    public $fk_origin;
103
104
    /**
105
     * @var int     Origin id
106
     */
107
    public $origin_id;
108
109
    /**
110
     * @var string  origintype
111
     * @deprecated
112
     * see $origin_type
113
     */
114
    public $origintype;
115
116
    /**
117
     * @var string Origin type ('project', ...)
118
     */
119
    public $origin_type;
120
    public $line_id_oject_src;
121
    public $line_id_oject_origin;
122
123
124
    public $inventorycode;
125
    public $batch;
126
127
    public $line_id_object_src;
128
    public $line_id_object_origin;
129
130
    public $eatby;
131
    public $sellby;
132
133
134
135
    public $fields = array(
136
        'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 10, 'showoncombobox' => 1),
137
        'tms' => array('type' => 'timestamp', 'label' => 'DateModification', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 15),
138
        'datem' => array('type' => 'datetime', 'label' => 'Datem', 'enabled' => 1, 'visible' => -1, 'position' => 20),
139
        'fk_product' => array('type' => 'integer:Product:product/class/product.class.php:1', 'label' => 'Product', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 25),
140
        'fk_entrepot' => array('type' => 'integer:Entrepot:product/stock/class/entrepot.class.php', 'label' => 'Warehouse', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 30),
141
        'value' => array('type' => 'double', 'label' => 'Value', 'enabled' => 1, 'visible' => -1, 'position' => 35),
142
        'price' => array('type' => 'double(24,8)', 'label' => 'Price', 'enabled' => 1, 'visible' => -1, 'position' => 40),
143
        'type_mouvement' => array('type' => 'smallint(6)', 'label' => 'Type mouvement', 'enabled' => 1, 'visible' => -1, 'position' => 45),
144
        'fk_user_author' => array('type' => 'integer:User:user/class/user.class.php', 'label' => 'Fk user author', 'enabled' => 1, 'visible' => -1, 'position' => 50),
145
        'label' => array('type' => 'varchar(255)', 'label' => 'Label', 'enabled' => 1, 'visible' => -1, 'position' => 55),
146
        'fk_origin' => array('type' => 'integer', 'label' => 'Fk origin', 'enabled' => 1, 'visible' => -1, 'position' => 60),
147
        'origintype' => array('type' => 'varchar(32)', 'label' => 'Origintype', 'enabled' => 1, 'visible' => -1, 'position' => 65),
148
        'model_pdf' => array('type' => 'varchar(255)', 'label' => 'Model pdf', 'enabled' => 1, 'visible' => 0, 'position' => 70),
149
        '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),
150
        'inventorycode' => array('type' => 'varchar(128)', 'label' => 'InventoryCode', 'enabled' => 1, 'visible' => -1, 'position' => 80),
151
        'batch' => array('type' => 'varchar(30)', 'label' => 'Batch', 'enabled' => 1, 'visible' => -1, 'position' => 85),
152
        'eatby' => array('type' => 'date', 'label' => 'Eatby', 'enabled' => 1, 'visible' => -1, 'position' => 90),
153
        'sellby' => array('type' => 'date', 'label' => 'Sellby', 'enabled' => 1, 'visible' => -1, 'position' => 95),
154
        'fk_project' => array('type' => 'integer:Project:projet/class/project.class.php:1:(fk_statut:=:1)', 'label' => 'Fk project', 'enabled' => 1, 'visible' => -1, 'position' => 100),
155
    );
156
157
158
159
    /**
160
     *  Constructor
161
     *
162
     *  @param      DoliDB      $db      Database handler
163
     */
164
    public function __construct($db)
165
    {
166
        $this->db = $db;
167
    }
168
169
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.PublicUnderscore
170
    /**
171
     *  Add a movement of stock (in one direction only).
172
     *  This is the lowest level method to record a stock change. There is no control if warehouse is open or not.
173
     *  $this->origin_type and $this->origin_id can be also be set to save the source object of movement.
174
     *
175
     *  @param      User            $user               User object
176
     *  @param      int             $fk_product         Id of product
177
     *  @param      int             $entrepot_id        Id of warehouse
178
     *  @param      float           $qty                Qty of movement (can be <0 or >0 depending on parameter type)
179
     *  @param      int             $type               Direction of movement:
180
     *                                                  0=input (stock increase by a stock transfer), 1=output (stock decrease by a stock transfer),
181
     *                                                  2=output (stock decrease), 3=input (stock increase)
182
     *                                                  Note that qty should be > 0 with 0 or 3, < 0 with 1 or 2.
183
     *  @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.
184
     *  @param      string          $label              Label of stock movement
185
     *  @param      string          $inventorycode      Inventory code
186
     *  @param      integer|string  $datem              Force date of movement
187
     *  @param      integer|string  $eatby              eat-by date. Will be used if lot does not exists yet and will be created.
188
     *  @param      integer|string  $sellby             sell-by date. Will be used if lot does not exists yet and will be created.
189
     *  @param      string          $batch              batch number
190
     *  @param      boolean         $skip_batch         If set to true, stock movement is done without impacting batch record
191
     *  @param      int             $id_product_batch   Id product_batch (when skip_batch is false and we already know which record of product_batch to use)
192
     *  @param      int             $disablestockchangeforsubproduct    Disable stock change for sub-products of kit (useful only if product is a subproduct)
193
     *  @param      int             $donotcleanemptylines               Do not clean lines in stock table with qty=0 (because we want to have this done by the caller)
194
     *  @param      boolean         $force_update_batch Allows to add batch stock movement even if $product doesn't use batch anymore
195
     *  @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
196
     */
197
    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)
198
    {
199
		// phpcs:enable
200
        global $conf, $langs;
201
202
        require_once constant('DOL_DOCUMENT_ROOT') . '/product/class/product.class.php';
203
        require_once constant('DOL_DOCUMENT_ROOT') . '/product/stock/class/productlot.class.php';
204
205
        $error = 0;
206
        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));
207
208
        // Call hook at beginning
209
        global $action, $hookmanager;
210
        $hookmanager->initHooks(array('mouvementstock'));
211
212
        if (is_object($hookmanager)) {
213
            $parameters = array(
214
                'currentcontext'   => 'mouvementstock',
215
                'user'             => &$user,
216
                'fk_product'       => &$fk_product,
217
                'entrepot_id'      => &$entrepot_id,
218
                'qty'              => &$qty,
219
                'type'             => &$type,
220
                'price'            => &$price,
221
                'label'            => &$label,
222
                'inventorycode'    => &$inventorycode,
223
                'datem'            => &$datem,
224
                'eatby'            => &$eatby,
225
                'sellby'           => &$sellby,
226
                'batch'            => &$batch,
227
                'skip_batch'       => &$skip_batch,
228
                'id_product_batch' => &$id_product_batch
229
            );
230
            $reshook = $hookmanager->executeHooks('stockMovementCreate', $parameters, $this, $action);    // Note that $action and $object may have been modified by some hooks
231
232
            if ($reshook < 0) {
233
                if (!empty($hookmanager->resPrint)) {
234
                    dol_print_error(null, $hookmanager->resPrint);
235
                }
236
                return $reshook;
237
            } elseif ($reshook > 0) {
238
                return $hookmanager->resPrint;
239
            }
240
        }
241
        // end hook at beginning
242
243
        // Clean parameters
244
        $price = price2num($price, 'MU'); // Clean value for the casse we receive a float zero value, to have it a real zero value.
245
        if (empty($price)) {
246
            $price = 0;
247
        }
248
        $now = (!empty($datem) ? $datem : dol_now());
249
250
        // Check parameters
251
        if (!($fk_product > 0)) {
252
            return 0;
253
        }
254
        if (!($entrepot_id > 0)) {
255
            return 0;
256
        }
257
258
        if (is_numeric($eatby) && $eatby < 0) {
259
            dol_syslog(get_class($this) . "::_create start ErrorBadValueForParameterEatBy eatby = " . $eatby);
260
            $this->errors[] = 'ErrorBadValueForParameterEatBy';
261
            return -1;
262
        }
263
        if (is_numeric($sellby) && $sellby < 0) {
264
            dol_syslog(get_class($this) . "::_create start ErrorBadValueForParameterSellBy sellby = " . $sellby);
265
            $this->errors[] = 'ErrorBadValueForParameterSellBy';
266
            return -1;
267
        }
268
269
        // Set properties of movement
270
        $this->product_id = $fk_product;
271
        $this->entrepot_id = $entrepot_id; // deprecated
272
        $this->warehouse_id = $entrepot_id;
273
        $this->qty = $qty;
274
        $this->type = $type;
275
        $this->price = price2num($price);
276
        $this->label = $label;
277
        $this->inventorycode = $inventorycode;
278
        $this->datem = $now;
279
        $this->batch = $batch;
280
281
        $mvid = 0;
282
283
        $product = new Product($this->db);
284
285
        $result = $product->fetch($fk_product);
286
        if ($result < 0) {
287
            $this->error = $product->error;
288
            $this->errors = $product->errors;
289
            dol_print_error(null, "Failed to fetch product");
290
            return -1;
291
        }
292
        if ($product->id <= 0) {    // Can happen if database is corrupted (a product id exist in stock with product that has been removed)
293
            return 0;
294
        }
295
296
        // Define if we must make the stock change (If product type is a service or if stock is used also for services)
297
        // Only record into stock tables will be disabled by this (the rest like writing into lot table or movement of subproucts are done)
298
        $movestock = 0;
299
        if ($product->type != Product::TYPE_SERVICE || getDolGlobalString('STOCK_SUPPORTS_SERVICES')) {
300
            $movestock = 1;
301
        }
302
303
        $this->db->begin();
304
305
        // Set value $product->stock_reel and detail per warehouse into $product->stock_warehouse array
306
        if ($movestock) {
307
            $product->load_stock('novirtual');
308
        }
309
310
        // Test if product require batch data. If yes, and there is not or values are not correct, we throw an error.
311
        if (isModEnabled('productbatch') && $product->hasbatch() && !$skip_batch) {
312
            if (empty($batch)) {
313
                $langs->load("errors");
314
                $this->errors[] = $langs->transnoentitiesnoconv("ErrorTryToMakeMoveOnProductRequiringBatchData", $product->ref);
315
                dol_syslog("Try to make a movement of a product with status_batch on without any batch data", LOG_ERR);
316
317
                $this->db->rollback();
318
                return -2;
319
            }
320
321
            // Check table llx_product_lot from batchnumber for same product
322
            // If found and eatby/sellby defined into table and provided and differs, return error
323
            // If found and eatby/sellby defined into table and not provided, we take value from table
324
            // If found and eatby/sellby not defined into table and provided, we update table
325
            // If found and eatby/sellby not defined into table and not provided, we do nothing
326
            // If not found, we add record
327
            $sql = "SELECT pb.rowid, pb.batch, pb.eatby, pb.sellby FROM " . $this->db->prefix() . "product_lot as pb";
328
            $sql .= " WHERE pb.fk_product = " . ((int) $fk_product) . " AND pb.batch = '" . $this->db->escape($batch) . "'";
329
330
            dol_syslog(get_class($this) . "::_create scan serial for this product to check if eatby and sellby match", LOG_DEBUG);
331
332
            $resql = $this->db->query($sql);
333
            if ($resql) {
334
                $num = $this->db->num_rows($resql);
335
                $i = 0;
336
                if ($num > 0) {
337
                    while ($i < $num) {
338
                        $obj = $this->db->fetch_object($resql);
339
                        if ($obj->eatby) {
340
                            if ($eatby) {
341
                                $tmparray = dol_getdate($eatby, true);
342
                                $eatbywithouthour = dol_mktime(0, 0, 0, $tmparray['mon'], $tmparray['mday'], $tmparray['year']);
343
                                if ($this->db->jdate($obj->eatby) != $eatby && $this->db->jdate($obj->eatby) != $eatbywithouthour) {    // We test date without hours and with hours for backward compatibility
344
                                    // If found and eatby/sellby defined into table and provided and differs, return error
345
                                    $langs->load("stocks");
346
                                    $this->errors[] = $langs->transnoentitiesnoconv("ThisSerialAlreadyExistWithDifferentDate", $batch, dol_print_date($this->db->jdate($obj->eatby), 'dayhour'), dol_print_date($eatbywithouthour, 'dayhour'));
347
                                    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);
348
                                    $this->db->rollback();
349
                                    return -3;
350
                                }
351
                            } else {
352
                                $eatby = $obj->eatby; // If found and eatby/sellby defined into table and not provided, we take value from table
353
                            }
354
                        } else {
355
                            if ($eatby) { // If found and eatby/sellby not defined into table and provided, we update table
356
                                $productlot = new Productlot($this->db);
357
                                $result = $productlot->fetch($obj->rowid);
358
                                $productlot->eatby = $eatby;
359
                                $result = $productlot->update($user);
360
                                if ($result <= 0) {
361
                                    $this->error = $productlot->error;
362
                                    $this->errors = $productlot->errors;
363
                                    $this->db->rollback();
364
                                    return -5;
365
                                }
366
                            }
367
                        }
368
                        if ($obj->sellby) {
369
                            if ($sellby) {
370
                                $tmparray = dol_getdate($sellby, true);
371
                                $sellbywithouthour = dol_mktime(0, 0, 0, $tmparray['mon'], $tmparray['mday'], $tmparray['year']);
372
                                if ($this->db->jdate($obj->sellby) != $sellby && $this->db->jdate($obj->sellby) != $sellbywithouthour) {    // We test date without hours and with hours for backward compatibility
373
                                    // If found and eatby/sellby defined into table and provided and differs, return error
374
                                    $this->errors[] = $langs->transnoentitiesnoconv("ThisSerialAlreadyExistWithDifferentDate", $batch, dol_print_date($this->db->jdate($obj->sellby)), dol_print_date($sellby));
375
                                    dol_syslog($langs->transnoentities("ThisSerialAlreadyExistWithDifferentDate", $batch, dol_print_date($this->db->jdate($obj->sellby)), dol_print_date($sellby)), LOG_ERR);
376
                                    $this->db->rollback();
377
                                    return -3;
378
                                }
379
                            } else {
380
                                $sellby = $obj->sellby; // If found and eatby/sellby defined into table and not provided, we take value from table
381
                            }
382
                        } else {
383
                            if ($sellby) { // If found and eatby/sellby not defined into table and provided, we update table
384
                                $productlot = new Productlot($this->db);
385
                                $result = $productlot->fetch($obj->rowid);
386
                                $productlot->sellby = $sellby;
387
                                $result = $productlot->update($user);
388
                                if ($result <= 0) {
389
                                    $this->error = $productlot->error;
390
                                    $this->errors = $productlot->errors;
391
                                    $this->db->rollback();
392
                                    return -5;
393
                                }
394
                            }
395
                        }
396
397
                        $i++;
398
                    }
399
                } else { // If not found, we add record
400
                    $productlot = new Productlot($this->db);
401
                    $productlot->origin = !empty($this->origin_type) ? $this->origin_type : '';
402
                    $productlot->origin_id = !empty($this->origin_id) ? $this->origin_id : 0;
403
                    $productlot->entity = $conf->entity;
404
                    $productlot->fk_product = $fk_product;
405
                    $productlot->batch = $batch;
406
                    // If we are here = first time we manage this batch, so we used dates provided by users to create lot
407
                    $productlot->eatby = $eatby;
408
                    $productlot->sellby = $sellby;
409
                    $result = $productlot->create($user);
410
                    if ($result <= 0) {
411
                        $this->error = $productlot->error;
412
                        $this->errors = $productlot->errors;
413
                        $this->db->rollback();
414
                        return -4;
415
                    }
416
                }
417
            } else {
418
                dol_print_error($this->db);
419
                $this->db->rollback();
420
                return -1;
421
            }
422
        }
423
424
        // Check if stock is enough when qty is < 0
425
        // Note that qty should be > 0 with type 0 or 3, < 0 with type 1 or 2.
426
        if ($movestock && $qty < 0 && !getDolGlobalInt('STOCK_ALLOW_NEGATIVE_TRANSFER')) {
427
            if (isModEnabled('productbatch') && $product->hasbatch() && !$skip_batch) {
428
                $foundforbatch = 0;
429
                $qtyisnotenough = 0;
430
                if (isset($product->stock_warehouse[$entrepot_id])) {
431
                    foreach ($product->stock_warehouse[$entrepot_id]->detail_batch as $batchcursor => $prodbatch) {
432
                        if ((string) $batch != (string) $batchcursor) {        // Lot '59' must be different than lot '59c'
433
                            continue;
434
                        }
435
436
                        $foundforbatch = 1;
437
                        if ($prodbatch->qty < abs($qty)) {
438
                            $qtyisnotenough = $prodbatch->qty;
439
                        }
440
                        break;
441
                    }
442
                }
443
                if (!$foundforbatch || $qtyisnotenough) {
444
                    $langs->load("stocks");
445
                    include_once DOL_DOCUMENT_ROOT . '/product/stock/class/entrepot.class.php';
446
                    $tmpwarehouse = new Entrepot($this->db);
447
                    $tmpwarehouse->fetch($entrepot_id);
448
449
                    $this->error = $langs->trans('qtyToTranferLotIsNotEnough', $product->ref, $batch, $qtyisnotenough, $tmpwarehouse->ref);
450
                    $this->errors[] = $langs->trans('qtyToTranferLotIsNotEnough', $product->ref, $batch, $qtyisnotenough, $tmpwarehouse->ref);
451
                    $this->db->rollback();
452
                    return -8;
453
                }
454
            } else {
455
                if (isset($product->stock_warehouse[$entrepot_id]) && (empty($product->stock_warehouse[$entrepot_id]->real) || $product->stock_warehouse[$entrepot_id]->real < abs($qty))) {
456
                    $langs->load("stocks");
457
                    $this->error = $langs->trans('qtyToTranferIsNotEnough') . ' : ' . $product->ref;
458
                    $this->errors[] = $langs->trans('qtyToTranferIsNotEnough') . ' : ' . $product->ref;
459
                    $this->db->rollback();
460
                    return -8;
461
                }
462
            }
463
        }
464
465
        if ($movestock) {   // Change stock for current product, change for subproduct is done after
466
            // Set $origin_type, origin_id and fk_project
467
            $fk_project = $this->fk_project;
468
            if (!empty($this->origin_type)) {           // This is set by caller for tracking reason
469
                $origin_type = $this->origin_type;
470
                $origin_id = $this->origin_id;
471
                if (empty($fk_project) && $origin_type == 'project') {
472
                    $fk_project = $origin_id;
473
                    $origin_type = '';
474
                    $origin_id = 0;
475
                }
476
            } else {
477
                $fk_project = 0;
478
                $origin_type = '';
479
                $origin_id = 0;
480
            }
481
482
            $sql = "INSERT INTO " . $this->db->prefix() . "stock_mouvement(";
483
            $sql .= " datem, fk_product, batch, eatby, sellby,";
484
            $sql .= " fk_entrepot, value, type_mouvement, fk_user_author, label, inventorycode, price, fk_origin, origintype, fk_projet";
485
            $sql .= ")";
486
            $sql .= " VALUES ('" . $this->db->idate($this->datem) . "', " . ((int) $this->product_id) . ", ";
487
            $sql .= " " . ($batch ? "'" . $this->db->escape($batch) . "'" : "null") . ", ";
488
            $sql .= " " . ($eatby ? "'" . $this->db->idate($eatby) . "'" : "null") . ", ";
489
            $sql .= " " . ($sellby ? "'" . $this->db->idate($sellby) . "'" : "null") . ", ";
490
            $sql .= " " . ((int) $this->entrepot_id) . ", " . ((float) $this->qty) . ", " . ((int) $this->type) . ",";
491
            $sql .= " " . ((int) $user->id) . ",";
492
            $sql .= " '" . $this->db->escape($label) . "',";
493
            $sql .= " " . ($inventorycode ? "'" . $this->db->escape($inventorycode) . "'" : "null") . ",";
494
            $sql .= " " . ((float) price2num($price)) . ",";
495
            $sql .= " " . ((int) $origin_id) . ",";
496
            $sql .= " '" . $this->db->escape($origin_type) . "',";
497
            $sql .= " " . ((int) $fk_project);
498
            $sql .= ")";
499
500
            dol_syslog(get_class($this) . "::_create insert record into stock_mouvement", LOG_DEBUG);
501
            $resql = $this->db->query($sql);
502
503
            if ($resql) {
504
                $mvid = $this->db->last_insert_id($this->db->prefix() . "stock_mouvement");
505
                $this->id = $mvid;
506
            } else {
507
                $this->error = $this->db->lasterror();
508
                $this->errors[] = $this->error;
509
                $error = -1;
510
            }
511
512
            // Define current values for qty and pmp
513
            $oldqty = $product->stock_reel;
514
            $oldpmp = $product->pmp;
515
            $oldqtywarehouse = 0;
516
517
            // Test if there is already a record for couple (warehouse / product), so later we will make an update or create.
518
            $alreadyarecord = 0;
519
            if (!$error) {
520
                $sql = "SELECT rowid, reel FROM " . $this->db->prefix() . "product_stock";
521
                $sql .= " WHERE fk_entrepot = " . ((int) $entrepot_id) . " AND fk_product = " . ((int) $fk_product); // This is a unique key
522
523
                dol_syslog(get_class($this) . "::_create check if a record already exists in product_stock", LOG_DEBUG);
524
                $resql = $this->db->query($sql);
525
                if ($resql) {
526
                    $obj = $this->db->fetch_object($resql);
527
                    if ($obj) {
528
                        $alreadyarecord = 1;
529
                        $oldqtywarehouse = $obj->reel;
530
                        $fk_product_stock = $obj->rowid;
531
                    }
532
                    $this->db->free($resql);
533
                } else {
534
                    $this->errors[] = $this->db->lasterror();
535
                    $error = -2;
536
                }
537
            }
538
539
            // Calculate new AWP (PMP)
540
            $newpmp = 0;
541
            if (!$error) {
542
                if ($type == 0 || $type == 3) {
543
                    // After a stock increase
544
                    // Note: PMP is calculated on stock input only (type of movement = 0 or 3). If type == 0 or 3, qty should be > 0.
545
                    // Note: Price should always be >0 or 0. PMP should be always >0 (calculated on input)
546
                    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')))) {
547
                        $oldqtytouse = ($oldqty >= 0 ? $oldqty : 0);
548
                        // We make a test on oldpmp>0 to avoid to use normal rule on old data with no pmp field defined
549
                        if ($oldpmp > 0) {
550
                            $newpmp = price2num((($oldqtytouse * $oldpmp) + ($qty * $price)) / ($oldqtytouse + $qty), 'MU');
551
                        } else {
552
                            $newpmp = $price; // For this product, PMP was not yet set. We set it to input price.
553
                        }
554
                        //print "oldqtytouse=".$oldqtytouse." oldpmp=".$oldpmp." oldqtywarehousetouse=".$oldqtywarehousetouse." ";
555
                        //print "qty=".$qty." newpmp=".$newpmp;
556
                        //exit;
557
                    } else {
558
                        $newpmp = $oldpmp;
559
                    }
560
                } else {
561
                    // ($type == 1 || $type == 2)
562
                    //   -> After a stock decrease, we don't change value of the AWP/PMP of a product.
563
                    // else
564
                    //   Type of movement unknown
565
                    $newpmp = $oldpmp;
566
                }
567
            }
568
            // Update stock quantity
569
            if (!$error) {
570
                if ($alreadyarecord > 0) {
571
                    $sql = "UPDATE " . $this->db->prefix() . "product_stock SET reel = " . ((float) $oldqtywarehouse + (float) $qty);
572
                    $sql .= " WHERE fk_entrepot = " . ((int) $entrepot_id) . " AND fk_product = " . ((int) $fk_product);
573
                } else {
574
                    $sql = "INSERT INTO " . $this->db->prefix() . "product_stock";
575
                    $sql .= " (reel, fk_entrepot, fk_product) VALUES ";
576
                    $sql .= " (" . ((float) $qty) . ", " . ((int) $entrepot_id) . ", " . ((int) $fk_product) . ")";
577
                }
578
579
                dol_syslog(get_class($this) . "::_create update stock value", LOG_DEBUG);
580
                $resql = $this->db->query($sql);
581
                if (!$resql) {
582
                    $this->errors[] = $this->db->lasterror();
583
                    $error = -3;
584
                } elseif (empty($fk_product_stock)) {
585
                    $fk_product_stock = $this->db->last_insert_id($this->db->prefix() . "product_stock");
586
                }
587
            }
588
589
            // Update detail of stock for the lot.
590
            if (!$error && isModEnabled('productbatch') && (($product->hasbatch() && !$skip_batch) || $force_update_batch)) {
591
                if ($id_product_batch > 0) {
592
                    $result = $this->createBatch($id_product_batch, $qty);
593
                    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
594
                        $param_batch = array('fk_product_stock' => $fk_product_stock, 'batchnumber' => $batch);
595
                        $result = $this->createBatch($param_batch, $qty);
596
                    }
597
                } else {
598
                    $param_batch = array('fk_product_stock' => $fk_product_stock, 'batchnumber' => $batch);
599
                    $result = $this->createBatch($param_batch, $qty);
600
                }
601
                if ($result < 0) {
602
                    $error++;
603
                }
604
            }
605
606
            // Update PMP and denormalized value of stock qty at product level
607
            if (!$error) {
608
                $newpmp = price2num($newpmp, 'MU');
609
610
                // $sql = "UPDATE ".$this->db->prefix()."product SET pmp = ".$newpmp.", stock = ".$this->db->ifsql("stock IS NULL", 0, "stock") . " + ".$qty;
611
                // $sql.= " WHERE rowid = ".((int) $fk_product);
612
                // Update pmp + denormalized fields because we change content of produt_stock. Warning: Do not use "SET p.stock", does not works with pgsql
613
                $sql = "UPDATE " . $this->db->prefix() . "product as p SET pmp = " . ((float) $newpmp) . ",";
614
                $sql .= " stock=(SELECT SUM(ps.reel) FROM " . $this->db->prefix() . "product_stock as ps WHERE ps.fk_product = p.rowid)";
615
                $sql .= " WHERE rowid = " . ((int) $fk_product);
616
617
                dol_syslog(get_class($this) . "::_create update AWP", LOG_DEBUG);
618
                $resql = $this->db->query($sql);
619
                if (!$resql) {
620
                    $this->errors[] = $this->db->lasterror();
621
                    $error = -4;
622
                }
623
            }
624
625
            if (empty($donotcleanemptylines)) {
626
                // 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
627
                // having a lot1/qty=X and lot2/qty=-X, so 0 but we must not loose repartition of different lot.
628
                $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)";
629
                $resql = $this->db->query($sql);
630
                // We do not test error, it can fails if there is child in batch details
631
            }
632
        }
633
634
        // Add movement for sub products (recursive call)
635
        if (!$error && getDolGlobalString('PRODUIT_SOUSPRODUITS') && !getDolGlobalString('INDEPENDANT_SUBPRODUCT_STOCK') && empty($disablestockchangeforsubproduct)) {
636
            $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
637
        }
638
639
        if ($movestock && !$error) {
640
            // Call trigger
641
            $result = $this->call_trigger('STOCK_MOVEMENT', $user);
642
            if ($result < 0) {
643
                $error++;
644
            }
645
            // End call triggers
646
            // Check unicity for serial numbered equipment once all movement were done.
647
            if (!$error && isModEnabled('productbatch') && $product->hasbatch() && !$skip_batch) {
648
                if ($product->status_batch == 2 && $qty > 0) {  // We check only if we increased qty
649
                    if ($this->getBatchCount($fk_product, $batch) > 1) {
650
                        $error++;
651
                        $this->errors[] = $langs->trans("TooManyQtyForSerialNumber", $product->ref, $batch);
652
                    }
653
                }
654
            }
655
        }
656
657
        if (!$error) {
658
            $this->db->commit();
659
            return $mvid;
660
        } else {
661
            $this->db->rollback();
662
            dol_syslog(get_class($this) . "::_create error code=" . $error, LOG_ERR);
663
            return -6;
664
        }
665
    }
666
667
668
669
    /**
670
     * Load object in memory from the database
671
     *
672
     * @param int    $id  Id object
673
     *
674
     * @return int Return integer <0 if KO, 0 if not found, >0 if OK
675
     */
676
    public function fetch($id)
677
    {
678
        dol_syslog(__METHOD__, LOG_DEBUG);
679
680
        $sql = "SELECT";
681
        $sql .= " t.rowid,";
682
        $sql .= " t.tms,";
683
        $sql .= " t.datem,";
684
        $sql .= " t.fk_product,";
685
        $sql .= " t.fk_entrepot,";
686
        $sql .= " t.value,";
687
        $sql .= " t.price,";
688
        $sql .= " t.type_mouvement,";
689
        $sql .= " t.fk_user_author,";
690
        $sql .= " t.label,";
691
        $sql .= " t.fk_origin as origin_id,";
692
        $sql .= " t.origintype as origin_type,";
693
        $sql .= " t.inventorycode,";
694
        $sql .= " t.batch,";
695
        $sql .= " t.eatby,";
696
        $sql .= " t.sellby,";
697
        $sql .= " t.fk_projet as fk_project";
698
        $sql .= " FROM " . $this->db->prefix() . $this->table_element . " as t";
699
        $sql .= " WHERE t.rowid = " . ((int) $id);
700
701
        $resql = $this->db->query($sql);
702
        if ($resql) {
703
            $numrows = $this->db->num_rows($resql);
704
            if ($numrows) {
705
                $obj = $this->db->fetch_object($resql);
706
707
                $this->id = $obj->rowid;
708
709
                $this->product_id = $obj->fk_product;
710
                $this->warehouse_id = $obj->fk_entrepot;
711
                $this->qty = $obj->value;
712
                $this->type = $obj->type_mouvement;
713
714
                $this->tms = $this->db->jdate($obj->tms);
715
                $this->datem = $this->db->jdate($obj->datem);
716
                $this->price = $obj->price;
717
                $this->fk_user_author = $obj->fk_user_author;
718
                $this->label = $obj->label;
719
                $this->fk_origin = $obj->origin_id;     // For backward compatibility
720
                $this->origintype = $obj->origin_type;  // For backward compatibility
721
                $this->origin_id = $obj->origin_id;
722
                $this->origin_type = $obj->origin_type;
723
                $this->inventorycode = $obj->inventorycode;
724
                $this->batch = $obj->batch;
725
                $this->eatby = $this->db->jdate($obj->eatby);
726
                $this->sellby = $this->db->jdate($obj->sellby);
727
                $this->fk_project = $obj->fk_project;
728
            }
729
730
            // Retrieve all extrafield
731
            $this->fetch_optionals();
732
733
            // $this->fetch_lines();
734
735
            $this->db->free($resql);
736
737
            if ($numrows) {
738
                return 1;
739
            } else {
740
                return 0;
741
            }
742
        } else {
743
            $this->errors[] = 'Error ' . $this->db->lasterror();
744
            dol_syslog(__METHOD__ . ' ' . implode(',', $this->errors), LOG_ERR);
745
746
            return -1;
747
        }
748
    }
749
750
751
752
753
    /**
754
     *  Create movement in database for all subproducts
755
     *
756
     *  @param      User            $user           Object user
757
     *  @param      int             $idProduct      Id product
758
     *  @param      int             $entrepot_id    Warehouse id
759
     *  @param      float           $qty            Quantity
760
     *  @param      int             $type           Type
761
     *  @param      int             $price          Price
762
     *  @param      string          $label          Label of movement
763
     *  @param      string          $inventorycode  Inventory code
764
     *  @param      integer|string  $datem          Force date of movement
765
     *  @return     int             Return integer <0 if KO, 0 if OK
766
     */
767
    private function _createSubProduct($user, $idProduct, $entrepot_id, $qty, $type, $price = 0, $label = '', $inventorycode = '', $datem = '')
768
    {
769
        global $langs;
770
771
        $error = 0;
772
        $pids = array();
773
        $pqtys = array();
774
775
        $sql = "SELECT fk_product_pere, fk_product_fils, qty";
776
        $sql .= " FROM " . $this->db->prefix() . "product_association";
777
        $sql .= " WHERE fk_product_pere = " . ((int) $idProduct);
778
        $sql .= " AND incdec = 1";
779
780
        dol_syslog(get_class($this) . "::_createSubProduct for parent product " . $idProduct, LOG_DEBUG);
781
        $resql = $this->db->query($sql);
782
        if ($resql) {
783
            $i = 0;
784
            while ($obj = $this->db->fetch_object($resql)) {
785
                $pids[$i] = $obj->fk_product_fils;
786
                $pqtys[$i] = $obj->qty;
787
                $i++;
788
            }
789
            $this->db->free($resql);
790
        } else {
791
            $error = -2;
792
        }
793
794
        // Create movement for each subproduct
795
        foreach ($pids as $key => $value) {
796
            if (!$error) {
797
                $tmpmove = dol_clone($this, 1);
798
799
                $result = $tmpmove->_create($user, $pids[$key], $entrepot_id, ($qty * $pqtys[$key]), $type, 0, $label, $inventorycode, $datem); // This will also call _createSubProduct making this recursive
800
                if ($result < 0) {
801
                    $this->error = $tmpmove->error;
802
                    $this->errors = array_merge($this->errors, $tmpmove->errors);
803
                    if ($result == -2) {
804
                        $this->errors[] = $langs->trans("ErrorNoteAlsoThatSubProductCantBeFollowedByLot");
805
                    }
806
                    $error = $result;
807
                }
808
                unset($tmpmove);
809
            }
810
        }
811
812
        return $error;
813
    }
814
815
816
    /**
817
     *  Decrease stock for product and subproducts
818
     *
819
     *  @param      User            $user                   Object user
820
     *  @param      int             $fk_product             Id product
821
     *  @param      int             $entrepot_id            Warehouse id
822
     *  @param      float           $qty                    Quantity
823
     *  @param      int             $price                  Price
824
     *  @param      string          $label                  Label of stock movement
825
     *  @param      int|string      $datem                  Force date of movement
826
     *  @param      int|string      $eatby                  eat-by date
827
     *  @param      int|string      $sellby                 sell-by date
828
     *  @param      string          $batch                  batch number
829
     *  @param      int             $id_product_batch       Id product_batch
830
     *  @param      string          $inventorycode          Inventory code
831
     *  @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)
832
     *  @return     int                                     Return integer <0 if KO, >0 if OK
833
     */
834
    public function livraison($user, $fk_product, $entrepot_id, $qty, $price = 0, $label = '', $datem = '', $eatby = '', $sellby = '', $batch = '', $id_product_batch = 0, $inventorycode = '', $donotcleanemptylines = 0)
835
    {
836
        global $conf;
837
838
        $skip_batch = empty($conf->productbatch->enabled);
839
840
        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);
841
    }
842
843
    /**
844
     *  Increase stock for product and subproducts
845
     *
846
     *  @param      User            $user                   Object user
847
     *  @param      int             $fk_product             Id product
848
     *  @param      int             $entrepot_id            Warehouse id
849
     *  @param      float           $qty                    Quantity
850
     *  @param      int             $price                  Price
851
     *  @param      string          $label                  Label of stock movement
852
     *  @param      integer|string  $eatby                  eat-by date
853
     *  @param      integer|string  $sellby                 sell-by date
854
     *  @param      string          $batch                  batch number
855
     *  @param      integer|string  $datem                  Force date of movement
856
     *  @param      int             $id_product_batch       Id product_batch
857
     *  @param      string          $inventorycode          Inventory code
858
     *  @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)
859
     *  @return     int                                     Return integer <0 if KO, >0 if OK
860
     */
861
    public function reception($user, $fk_product, $entrepot_id, $qty, $price = 0, $label = '', $eatby = '', $sellby = '', $batch = '', $datem = '', $id_product_batch = 0, $inventorycode = '', $donotcleanemptylines = 0)
862
    {
863
        global $conf;
864
865
        $skip_batch = empty($conf->productbatch->enabled);
866
867
        return $this->_create($user, $fk_product, $entrepot_id, $qty, 3, $price, $label, $inventorycode, $datem, $eatby, $sellby, $batch, $skip_batch, $id_product_batch, 0, $donotcleanemptylines);
868
    }
869
870
    /**
871
     * Count number of product in stock before a specific date
872
     *
873
     * @param   int         $productidselected      Id of product to count
874
     * @param   integer     $datebefore             Date limit
875
     * @return  int         Number
876
     */
877
    public function calculateBalanceForProductBefore($productidselected, $datebefore)
878
    {
879
        $nb = 0;
880
881
        $sql = "SELECT SUM(value) as nb from " . $this->db->prefix() . "stock_mouvement";
882
        $sql .= " WHERE fk_product = " . ((int) $productidselected);
883
        $sql .= " AND datem < '" . $this->db->idate($datebefore) . "'";
884
885
        dol_syslog(get_class($this) . __METHOD__, LOG_DEBUG);
886
        $resql = $this->db->query($sql);
887
        if ($resql) {
888
            $obj = $this->db->fetch_object($resql);
889
            if ($obj) {
890
                $nb = $obj->nb;
891
            }
892
            return (empty($nb) ? 0 : $nb);
893
        } else {
894
            dol_print_error($this->db);
895
            return -1;
896
        }
897
    }
898
899
    /**
900
     * Create or update batch record (update table llx_product_batch). No check is done here, done by parent.
901
     *
902
     * @param   array|int   $dluo         Could be either
903
     *                                    - int if row id of product_batch table (for update)
904
     *                                    - or complete array('fk_product_stock'=>, 'batchnumber'=>)
905
     * @param   float       $qty          Quantity of product with batch number. May be a negative amount.
906
     * @return  int                       Return integer <0 if KO, -2 if we try to update a product_batchid that does not exist, else return productbatch id
907
     */
908
    private function createBatch($dluo, $qty)
909
    {
910
        global $user, $langs;
911
912
        $langs->load('productbatch');
913
914
        $pdluo = new Productbatch($this->db);
915
916
        $result = 0;
917
918
        // Try to find an existing record with same batch number or id
919
        if (is_numeric($dluo)) {
920
            $result = $pdluo->fetch($dluo);
921
            if (empty($pdluo->id)) {
922
                // We didn't find the line. May be it was deleted before by a previous move in same transaction.
923
                $this->error = $langs->trans('CantMoveNonExistantSerial');
924
                $this->errors[] = $this->error;
925
                $result = -2;
926
            }
927
        } elseif (is_array($dluo)) {
928
            if (isset($dluo['fk_product_stock'])) {
929
                $vfk_product_stock = $dluo['fk_product_stock'];
930
                $vbatchnumber = $dluo['batchnumber'];
931
932
                $result = $pdluo->find($vfk_product_stock, '', '', $vbatchnumber); // Search on batch number only (eatby and sellby are deprecated here)
933
            } else {
934
                dol_syslog(get_class($this) . "::createBatch array param dluo must contain at least key fk_product_stock", LOG_ERR);
935
                $result = -1;
936
            }
937
        } else {
938
            dol_syslog(get_class($this) . "::createBatch error invalid param dluo", LOG_ERR);
939
            $result = -1;
940
        }
941
942
        if ($result >= 0) {
943
            // No error
944
            if ($pdluo->id > 0) {   // product_batch record found
945
                //print "Avant ".$pdluo->qty." Apres ".($pdluo->qty + $qty)."<br>";
946
                $pdluo->qty += $qty;
947
                if ($pdluo->qty == 0) {
948
                    $result = $pdluo->delete($user, 1);
949
                } else {
950
                    $result = $pdluo->update($user, 1);
951
                }
952
            } else {                    // product_batch record not found
953
                $pdluo->fk_product_stock = $vfk_product_stock;
954
                $pdluo->qty = $qty;
955
                $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.
956
                $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.
957
                $pdluo->batch = $vbatchnumber;
958
959
                $result = $pdluo->create($user, 1);
960
                if ($result < 0) {
961
                    $this->error = $pdluo->error;
962
                    $this->errors = $pdluo->errors;
963
                }
964
            }
965
        }
966
967
        return $result;
968
    }
969
970
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
971
    /**
972
     * Return Url link of origin object
973
     *
974
     * @param  int      $origin_id      Id origin
975
     * @param  string   $origin_type    Type origin ('project', 'xxx@MODULENAME', etc)
976
     * @return string
977
     */
978
    public function get_origin($origin_id, $origin_type)
979
    {
980
		// phpcs:enable
981
        $origin = '';
982
983
        switch ($origin_type) {
984
            case 'commande':
985
                use Dolibarr\Code\Adherents\Classes\Adherent;
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected T_USE on line 985 at column 16
Loading history...
986
                $origin = new Commande($this->db);
987
                break;
988
            case 'shipping':
989
                require_once constant('DOL_DOCUMENT_ROOT') . '/expedition/class/expedition.class.php';
990
                $origin = new Expedition($this->db);
991
                break;
992
            case 'facture':
993
                require_once constant('DOL_DOCUMENT_ROOT') . '/compta/facture/class/facture.class.php';
994
                $origin = new Facture($this->db);
995
                break;
996
            case 'order_supplier':
997
                require_once constant('DOL_DOCUMENT_ROOT') . '/fourn/class/fournisseur.commande.class.php';
998
                $origin = new CommandeFournisseur($this->db);
999
                break;
1000
            case 'invoice_supplier':
1001
                require_once constant('DOL_DOCUMENT_ROOT') . '/fourn/class/fournisseur.facture.class.php';
1002
                $origin = new FactureFournisseur($this->db);
1003
                break;
1004
            case 'project':
1005
                require_once constant('DOL_DOCUMENT_ROOT') . '/projet/class/project.class.php';
1006
                $origin = new Project($this->db);
1007
                break;
1008
            case 'mo':
1009
                require_once constant('DOL_DOCUMENT_ROOT') . '/mrp/class/mo.class.php';
1010
                $origin = new Mo($this->db);
1011
                break;
1012
            case 'user':
1013
                require_once constant('DOL_DOCUMENT_ROOT') . '/user/class/user.class.php';
1014
                $origin = new User($this->db);
1015
                break;
1016
            case 'reception':
1017
                require_once constant('DOL_DOCUMENT_ROOT') . '/reception/class/reception.class.php';
1018
                $origin = new Reception($this->db);
1019
                break;
1020
            case 'inventory':
1021
                require_once constant('DOL_DOCUMENT_ROOT') . '/product/inventory/class/inventory.class.php';
1022
                $origin = new Inventory($this->db);
1023
                break;
1024
            default:
1025
                if ($origin_type) {
1026
                    // Separate origin_type with "@" : left part is class name, right part is module name
1027
                    $origin_type_array = explode('@', $origin_type);
1028
                    $classname = $origin_type_array[0];
1029
                    $modulename = empty($origin_type_array[1]) ? strtolower($classname) : $origin_type_array[1];
1030
1031
                    $result = dol_include_once('/' . $modulename . '/class/' . $classname . '.class.php');
1032
1033
                    if ($result) {
1034
                        $classname = ucfirst($classname);
1035
                        $origin = new $classname($this->db);
1036
                    }
1037
                }
1038
                break;
1039
        }
1040
1041
        if (empty($origin) || !is_object($origin)) {
1042
            return '';
1043
        }
1044
1045
        if ($origin->fetch($origin_id) > 0) {
1046
            return $origin->getNomUrl(1);
1047
        }
1048
1049
        return '';
1050
    }
1051
1052
    /**
1053
     * Set attribute origin_type and fk_origin to object
1054
     *
1055
     * @param   string  $origin_element     Type of element
1056
     * @param   int     $origin_id          Id of element
1057
     * @param   int     $line_id_object_src Id line of element Source
1058
     * @param   int     $line_id_object_origin  Id line of element Origin
1059
     *
1060
     * @return  void
1061
     */
1062
    public function setOrigin($origin_element, $origin_id, $line_id_object_src = 0, $line_id_object_origin = 0)
1063
    {
1064
        $this->origin_type = $origin_element;
1065
        $this->origin_id = $origin_id;
1066
        $this->line_id_object_src = $line_id_object_src;
1067
        $this->line_id_object_origin = $line_id_object_origin;
1068
        // For backward compatibility
1069
        $this->origintype = $origin_element;
1070
        $this->fk_origin = $origin_id;
1071
    }
1072
1073
1074
    /**
1075
     *  Initialise an instance with random values.
1076
     *  Used to build previews or test instances.
1077
     *  id must be 0 if object instance is a specimen.
1078
     *
1079
     *  @return int
1080
     */
1081
    public function initAsSpecimen()
1082
    {
1083
        // Initialize parameters
1084
        $this->id = 0;
1085
1086
        // There is no specific properties. All data into insert are provided as method parameter.
1087
1088
        return 1;
1089
    }
1090
1091
    /**
1092
     *  Return html string with picto for type of movement
1093
     *
1094
     *  @param  int     $withlabel          With label
1095
     *  @return string                      String with URL
1096
     */
1097
    public function getTypeMovement($withlabel = 0)
1098
    {
1099
        global $langs;
1100
1101
        $s = '';
1102
        switch ($this->type) {
1103
            case "0":
1104
                $s = '<span class="fa fa-level-down-alt stockmovemententry stockmovementtransfer" title="' . $langs->trans('StockIncreaseAfterCorrectTransfer') . '"></span>';
1105
                if ($withlabel) {
1106
                    $s .= $langs->trans('StockIncreaseAfterCorrectTransfer');
1107
                }
1108
                break;
1109
            case "1":
1110
                $s = '<span class="fa fa-level-up-alt stockmovementexit stockmovementtransfer" title="' . $langs->trans('StockDecreaseAfterCorrectTransfer') . '"></span>';
1111
                if ($withlabel) {
1112
                    $s .= $langs->trans('StockDecreaseAfterCorrectTransfer');
1113
                }
1114
                break;
1115
            case "2":
1116
                $s = '<span class="fa fa-long-arrow-alt-up stockmovementexit stockmovement" title="' . $langs->trans('StockDecrease') . '"></span>';
1117
                if ($withlabel) {
1118
                    $s .= $langs->trans('StockDecrease');
1119
                }
1120
                break;
1121
            case "3":
1122
                $s = '<span class="fa fa-long-arrow-alt-down stockmovemententry stockmovement" title="' . $langs->trans('StockIncrease') . '"></span>';
1123
                if ($withlabel) {
1124
                    $s .= $langs->trans('StockIncrease');
1125
                }
1126
                break;
1127
        }
1128
1129
        return $s;
1130
    }
1131
1132
    /**
1133
     *  Return a link (with optionally the picto)
1134
     *  Use this->id,this->lastname, this->firstname
1135
     *
1136
     *  @param  int     $withpicto          Include picto in link (0=No picto, 1=Include picto into link, 2=Only picto)
1137
     *  @param  string  $option             On what the link point to ('' = Tab of stock movement of warehouse, 'movements' = list of movements)
1138
     *  @param  integer $notooltip          1=Disable tooltip
1139
     *  @param  int     $maxlen             Max length of visible user name
1140
     *  @param  string  $morecss            Add more css on link
1141
     *  @return string                      String with URL
1142
     */
1143
    public function getNomUrl($withpicto = 0, $option = '', $notooltip = 0, $maxlen = 24, $morecss = '')
1144
    {
1145
        global $langs, $conf, $db;
1146
1147
        $result = '';
1148
1149
        $label = img_picto('', 'stock', 'class="pictofixedwidth"') . '<u>' . $langs->trans("StockMovement") . '</u>';
1150
        $label .= '<div width="100%">';
1151
        $label .= '<b>' . $langs->trans('Ref') . ':</b> ' . $this->id;
1152
        $label .= '<br><b>' . $langs->trans('Label') . ':</b> ' . $this->label;
1153
        $qtylabel = (($this->qty > 0) ? '<span class="stockmovemententry">+' : '<span class="stockmovementexit">') . $this->qty . '</span>';
1154
        $label .= '<br><b>' . $langs->trans('Qty') . ':</b> ' . $qtylabel;
1155
        if ($this->batch) {
1156
            $label .= '<br><b>' . $langs->trans('Batch') . ':</b> ' . $this->batch;
1157
        }
1158
        /* TODO Get also warehouse label in a property instead of id
1159
        if ($this->warehouse_id > 0) {
1160
            $label .= '<br><b>'.$langs->trans('Warehouse').':</b> '.$this->warehouse_id;
1161
        }*/
1162
        $label .= '</div>';
1163
1164
        // Link to page of warehouse tab
1165
        if ($option == 'movements') {
1166
            $url = constant('BASE_URL') . '/product/stock/movement_list.php?search_ref=' . $this->id;
1167
        } else {
1168
            $url = constant('BASE_URL') . '/product/stock/movement_list.php?id=' . $this->warehouse_id . '&msid=' . $this->id;
1169
        }
1170
1171
        $link = '<a href="' . $url . '"' . ($notooltip ? '' : ' title="' . dol_escape_htmltag($label, 1) . '" class="classfortooltip' . ($morecss ? ' ' . $morecss : '') . '"');
1172
        $link .= '>';
1173
        $linkend = '</a>';
1174
1175
        if ($withpicto) {
1176
            $result .= ($link . img_object(($notooltip ? '' : $label), 'stock', ($notooltip ? '' : 'class="classfortooltip"')) . $linkend);
1177
            if ($withpicto != 2) {
1178
                $result .= ' ';
1179
            }
1180
        }
1181
        $result .= $link . $this->id . $linkend;
1182
        return $result;
1183
    }
1184
1185
    /**
1186
     *  Return label statut
1187
     *
1188
     *  @param  int     $mode          0=libelle long, 1=libelle court, 2=Picto + Libelle court, 3=Picto, 4=Picto + Libelle long, 5=Libelle court + Picto
1189
     *  @return string                 Label of status
1190
     */
1191
    public function getLibStatut($mode = 0)
1192
    {
1193
        return $this->LibStatut($mode);
1194
    }
1195
1196
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1197
    /**
1198
     *  Return the label of the status
1199
     *
1200
     *  @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
1201
     *  @return string                 Label of status
1202
     */
1203
    public function LibStatut($mode = 0)
1204
    {
1205
		// phpcs:enable
1206
        global $langs;
1207
1208
        if ($mode == 0 || $mode == 1) {
1209
            return $langs->trans('StatusNotApplicable');
1210
        } elseif ($mode == 2) {
1211
            return img_picto($langs->trans('StatusNotApplicable'), 'statut9') . ' ' . $langs->trans('StatusNotApplicable');
1212
        } elseif ($mode == 3) {
1213
            return img_picto($langs->trans('StatusNotApplicable'), 'statut9');
1214
        } elseif ($mode == 4) {
1215
            return img_picto($langs->trans('StatusNotApplicable'), 'statut9') . ' ' . $langs->trans('StatusNotApplicable');
1216
        } elseif ($mode == 5) {
1217
            return $langs->trans('StatusNotApplicable') . ' ' . img_picto($langs->trans('StatusNotApplicable'), 'statut9');
1218
        }
1219
1220
        return 'Bad value for mode';
1221
    }
1222
1223
    /**
1224
     *  Create object on disk
1225
     *
1226
     *  @param     string       $modele         force le modele a utiliser ('' to not force)
1227
     *  @param     Translate    $outputlangs    Object langs to use for output
1228
     *  @param     int          $hidedetails    Hide details of lines
1229
     *  @param     int          $hidedesc       Hide description
1230
     *  @param     int          $hideref        Hide ref
1231
     *  @return    int                          0 if KO, 1 if OK
1232
     */
1233
    public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0)
1234
    {
1235
        global $conf, $user, $langs;
1236
1237
        $langs->load("stocks");
1238
        $outputlangs->load("products");
1239
1240
        if (!dol_strlen($modele)) {
1241
            $modele = 'stdmovement';
1242
1243
            if ($this->model_pdf) {
1244
                $modele = $this->model_pdf;
1245
            } elseif (getDolGlobalString('MOUVEMENT_ADDON_PDF')) {
1246
                $modele = getDolGlobalString('MOUVEMENT_ADDON_PDF');
1247
            }
1248
        }
1249
1250
        $modelpath = "core/modules/stock/doc/";
1251
1252
        return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref);
1253
    }
1254
1255
    /**
1256
     * Delete object in database
1257
     *
1258
     * @param User  $user       User that deletes
1259
     * @param int   $notrigger  0=launch triggers after, 1=disable triggers
1260
     * @return int              Return integer <0 if KO, >0 if OK
1261
     */
1262
    public function delete(User $user, $notrigger = 0)
1263
    {
1264
        return $this->deleteCommon($user, $notrigger);
1265
        //return $this->deleteCommon($user, $notrigger, 1);
1266
    }
1267
1268
    /**
1269
     * Retrieve number of equipment for a product lot/serial
1270
     *
1271
     * @param   int         $fk_product     Product id
1272
     * @param   string      $batch          batch number
1273
     * @return  int                         Return integer <0 if KO, number of equipment found if OK
1274
     */
1275
    private function getBatchCount($fk_product, $batch)
1276
    {
1277
        $cpt = 0;
1278
1279
        $sql = "SELECT sum(pb.qty) as cpt";
1280
        $sql .= " FROM " . $this->db->prefix() . "product_batch as pb";
1281
        $sql .= " INNER JOIN " . $this->db->prefix() . "product_stock as ps ON ps.rowid = pb.fk_product_stock";
1282
        $sql .= " WHERE ps.fk_product = " . ((int) $fk_product);
1283
        $sql .= " AND pb.batch = '" . $this->db->escape($batch) . "'";
1284
1285
        $result = $this->db->query($sql);
1286
        if ($result) {
1287
            if ($this->db->num_rows($result)) {
1288
                $obj = $this->db->fetch_object($result);
1289
                $cpt = $obj->cpt;
1290
            }
1291
1292
            $this->db->free($result);
1293
        } else {
1294
            dol_print_error($this->db);
1295
            return -1;
1296
        }
1297
1298
        return $cpt;
1299
    }
1300
1301
    /**
1302
     * reverse movement for object by updating infos
1303
     * @return int    1 if OK,-1 if KO
1304
     */
1305
    public function reverseMouvement()
1306
    {
1307
        $formattedDate = "REVERTMV" . dol_print_date($this->datem, '%Y%m%d%His');
1308
        if ($this->label == 'Annulation movement ID' . $this->id) {
1309
            return -1;
1310
        }
1311
        if ($this->inventorycode == $formattedDate) {
1312
            return -1;
1313
        }
1314
1315
        $sql = "UPDATE " . $this->db->prefix() . "stock_mouvement SET";
1316
        $sql .= " label = 'Annulation movement ID " . ((int) $this->id) . "',";
1317
        $sql .= "inventorycode = '" . ($formattedDate) . "'";
1318
        $sql .= " WHERE rowid = " . ((int) $this->id);
1319
1320
        $resql = $this->db->query($sql);
1321
1322
        if ($resql) {
1323
            $this->db->commit();
1324
            return 1;
1325
        } else {
1326
            $this->db->rollback();
1327
            return -1;
1328
        }
1329
    }
1330
}
1331