dolibarr 19.0.4
mouvementstock.class.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2003-2006 Rodolphe Quiedeville <rodolphe@quiedeville.org>
3 * Copyright (C) 2005-2015 Laurent Destailleur <eldy@users.sourceforge.net>
4 * Copyright (C) 2011 Jean Heimburger <jean@tiaris.info>
5 * Copyright (C) 2014 Cedric GROSS <c.gross@kreiz-it.fr>
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 */
20
32{
36 public $element = 'stockmouvement';
37
41 public $table_element = 'stock_mouvement';
42
43
47 public $product_id;
48
54 public $entrepot_id;
55
59 public $warehouse_id;
60
61 public $qty;
62
71 public $type;
72
73 public $tms = '';
74 public $datem = '';
75 public $price;
76
80 public $fk_user_author;
81
85 public $label;
86
92 public $fk_origin;
93
97 public $origin_id;
98
104 public $origintype;
105
109 public $origin_type;
110 public $line_id_oject_src;
111 public $line_id_oject_origin;
112
113
114 public $inventorycode;
115 public $batch;
116
117 public $line_id_object_src;
118 public $line_id_object_origin;
119
120 public $eatby;
121 public $sellby;
122
123
124
125 public $fields = array(
126 'rowid' =>array('type'=>'integer', 'label'=>'TechnicalID', 'enabled'=>1, 'visible'=>-1, 'notnull'=>1, 'position'=>10, 'showoncombobox'=>1),
127 'tms' =>array('type'=>'timestamp', 'label'=>'DateModification', 'enabled'=>1, 'visible'=>-1, 'notnull'=>1, 'position'=>15),
128 'datem' =>array('type'=>'datetime', 'label'=>'Datem', 'enabled'=>1, 'visible'=>-1, 'position'=>20),
129 'fk_product' =>array('type'=>'integer:Product:product/class/product.class.php:1', 'label'=>'Product', 'enabled'=>1, 'visible'=>-1, 'notnull'=>1, 'position'=>25),
130 'fk_entrepot' =>array('type'=>'integer:Entrepot:product/stock/class/entrepot.class.php', 'label'=>'Warehouse', 'enabled'=>1, 'visible'=>-1, 'notnull'=>1, 'position'=>30),
131 'value' =>array('type'=>'double', 'label'=>'Value', 'enabled'=>1, 'visible'=>-1, 'position'=>35),
132 'price' =>array('type'=>'double(24,8)', 'label'=>'Price', 'enabled'=>1, 'visible'=>-1, 'position'=>40),
133 'type_mouvement' =>array('type'=>'smallint(6)', 'label'=>'Type mouvement', 'enabled'=>1, 'visible'=>-1, 'position'=>45),
134 'fk_user_author' =>array('type'=>'integer:User:user/class/user.class.php', 'label'=>'Fk user author', 'enabled'=>1, 'visible'=>-1, 'position'=>50),
135 'label' =>array('type'=>'varchar(255)', 'label'=>'Label', 'enabled'=>1, 'visible'=>-1, 'position'=>55),
136 'fk_origin' =>array('type'=>'integer', 'label'=>'Fk origin', 'enabled'=>1, 'visible'=>-1, 'position'=>60),
137 'origintype' =>array('type'=>'varchar(32)', 'label'=>'Origintype', 'enabled'=>1, 'visible'=>-1, 'position'=>65),
138 'model_pdf' =>array('type'=>'varchar(255)', 'label'=>'Model pdf', 'enabled'=>1, 'visible'=>0, 'position'=>70),
139 '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),
140 'inventorycode' =>array('type'=>'varchar(128)', 'label'=>'InventoryCode', 'enabled'=>1, 'visible'=>-1, 'position'=>80),
141 'batch' =>array('type'=>'varchar(30)', 'label'=>'Batch', 'enabled'=>1, 'visible'=>-1, 'position'=>85),
142 'eatby' =>array('type'=>'date', 'label'=>'Eatby', 'enabled'=>1, 'visible'=>-1, 'position'=>90),
143 'sellby' =>array('type'=>'date', 'label'=>'Sellby', 'enabled'=>1, 'visible'=>-1, 'position'=>95),
144 'fk_project' =>array('type'=>'integer:Project:projet/class/project.class.php:1:(fk_statut:=:1)', 'label'=>'Fk project', 'enabled'=>1, 'visible'=>-1, 'position'=>100),
145 );
146
147
148
154 public function __construct($db)
155 {
156 $this->db = $db;
157 }
158
159 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.PublicUnderscore
187 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)
188 {
189 // phpcs:enable
190 global $conf, $langs;
191
192 require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
193 require_once DOL_DOCUMENT_ROOT.'/product/stock/class/productlot.class.php';
194
195 $error = 0;
196 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=".$skip_batch);
197
198 // Call hook at beginning
199 global $action, $hookmanager;
200 $hookmanager->initHooks(array('mouvementstock'));
201
202 if (is_object($hookmanager)) {
203 $parameters = array(
204 'currentcontext' => 'mouvementstock',
205 'user' => &$user,
206 'fk_product' => &$fk_product,
207 'entrepot_id' => &$entrepot_id,
208 'qty' => &$qty,
209 'type' => &$type,
210 'price' => &$price,
211 'label' => &$label,
212 'inventorycode' => &$inventorycode,
213 'datem' => &$datem,
214 'eatby' => &$eatby,
215 'sellby' => &$sellby,
216 'batch' => &$batch,
217 'skip_batch' => &$skip_batch,
218 'id_product_batch' => &$id_product_batch
219 );
220 $reshook = $hookmanager->executeHooks('stockMovementCreate', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
221
222 if ($reshook < 0) {
223 if (!empty($hookmanager->resPrint)) {
224 dol_print_error('', $hookmanager->resPrint);
225 }
226 return $reshook;
227 } elseif ($reshook > 0) {
228 return $hookmanager->resPrint;
229 }
230 }
231 // end hook at beginning
232
233 // Clean parameters
234 $price = price2num($price, 'MU'); // Clean value for the casse we receive a float zero value, to have it a real zero value.
235 if (empty($price)) {
236 $price = 0;
237 }
238 $now = (!empty($datem) ? $datem : dol_now());
239
240 // Check parameters
241 if (!($fk_product > 0)) {
242 return 0;
243 }
244 if (!($entrepot_id > 0)) {
245 return 0;
246 }
247
248 if (is_numeric($eatby) && $eatby < 0) {
249 dol_syslog(get_class($this)."::_create start ErrorBadValueForParameterEatBy eatby = ".$eatby);
250 $this->errors[] = 'ErrorBadValueForParameterEatBy';
251 return -1;
252 }
253 if (is_numeric($sellby) && $sellby < 0) {
254 dol_syslog(get_class($this)."::_create start ErrorBadValueForParameterSellBy sellby = ".$sellby);
255 $this->errors[] = 'ErrorBadValueForParameterSellBy';
256 return -1;
257 }
258
259 // Set properties of movement
260 $this->product_id = $fk_product;
261 $this->entrepot_id = $entrepot_id; // deprecated
262 $this->warehouse_id = $entrepot_id;
263 $this->qty = $qty;
264 $this->type = $type;
265 $this->price = price2num($price);
266 $this->label = $label;
267 $this->inventorycode = $inventorycode;
268 $this->datem = $now;
269 $this->batch = $batch;
270
271 $mvid = 0;
272
273 $product = new Product($this->db);
274
275 $result = $product->fetch($fk_product);
276 if ($result < 0) {
277 $this->error = $product->error;
278 $this->errors = $product->errors;
279 dol_print_error('', "Failed to fetch product");
280 return -1;
281 }
282 if ($product->id <= 0) { // Can happen if database is corrupted (a product id exist in stock with product that has been removed)
283 return 0;
284 }
285
286 // Define if we must make the stock change (If product type is a service or if stock is used also for services)
287 // Only record into stock tables wil be disabled by this (the rest like writing into lot table or movement of subproucts are done)
288 $movestock = 0;
289 if ($product->type != Product::TYPE_SERVICE || getDolGlobalString('STOCK_SUPPORTS_SERVICES')) {
290 $movestock = 1;
291 }
292
293 $this->db->begin();
294
295 // Set value $product->stock_reel and detail per warehouse into $product->stock_warehouse array
296 if ($movestock) {
297 $product->load_stock('novirtual');
298 }
299
300 // Test if product require batch data. If yes, and there is not or values are not correct, we throw an error.
301 if (isModEnabled('productbatch') && $product->hasbatch() && !$skip_batch) {
302 if (empty($batch)) {
303 $langs->load("errors");
304 $this->errors[] = $langs->transnoentitiesnoconv("ErrorTryToMakeMoveOnProductRequiringBatchData", $product->ref);
305 dol_syslog("Try to make a movement of a product with status_batch on without any batch data");
306
307 $this->db->rollback();
308 return -2;
309 }
310
311 // Check table llx_product_lot from batchnumber for same product
312 // If found and eatby/sellby defined into table and provided and differs, return error
313 // If found and eatby/sellby defined into table and not provided, we take value from table
314 // If found and eatby/sellby not defined into table and provided, we update table
315 // If found and eatby/sellby not defined into table and not provided, we do nothing
316 // If not found, we add record
317 $sql = "SELECT pb.rowid, pb.batch, pb.eatby, pb.sellby FROM ".$this->db->prefix()."product_lot as pb";
318 $sql .= " WHERE pb.fk_product = ".((int) $fk_product)." AND pb.batch = '".$this->db->escape($batch)."'";
319
320 dol_syslog(get_class($this)."::_create scan serial for this product to check if eatby and sellby match", LOG_DEBUG);
321
322 $resql = $this->db->query($sql);
323 if ($resql) {
324 $num = $this->db->num_rows($resql);
325 $i = 0;
326 if ($num > 0) {
327 while ($i < $num) {
328 $obj = $this->db->fetch_object($resql);
329 if ($obj->eatby) {
330 if ($eatby) {
331 $tmparray = dol_getdate($eatby, true);
332 $eatbywithouthour = dol_mktime(0, 0, 0, $tmparray['mon'], $tmparray['mday'], $tmparray['year']);
333 if ($this->db->jdate($obj->eatby) != $eatby && $this->db->jdate($obj->eatby) != $eatbywithouthour) { // We test date without hours and with hours for backward compatibility
334 // If found and eatby/sellby defined into table and provided and differs, return error
335 $langs->load("stocks");
336 $this->errors[] = $langs->transnoentitiesnoconv("ThisSerialAlreadyExistWithDifferentDate", $batch, dol_print_date($this->db->jdate($obj->eatby), 'dayhour'), dol_print_date($eatbywithouthour, 'dayhour'));
337 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);
338 $this->db->rollback();
339 return -3;
340 }
341 } else {
342 $eatby = $obj->eatby; // If found and eatby/sellby defined into table and not provided, we take value from table
343 }
344 } else {
345 if ($eatby) { // If found and eatby/sellby not defined into table and provided, we update table
346 $productlot = new Productlot($this->db);
347 $result = $productlot->fetch($obj->rowid);
348 $productlot->eatby = $eatby;
349 $result = $productlot->update($user);
350 if ($result <= 0) {
351 $this->error = $productlot->error;
352 $this->errors = $productlot->errors;
353 $this->db->rollback();
354 return -5;
355 }
356 }
357 }
358 if ($obj->sellby) {
359 if ($sellby) {
360 $tmparray = dol_getdate($sellby, true);
361 $sellbywithouthour = dol_mktime(0, 0, 0, $tmparray['mon'], $tmparray['mday'], $tmparray['year']);
362 if ($this->db->jdate($obj->sellby) != $sellby && $this->db->jdate($obj->sellby) != $sellbywithouthour) { // We test date without hours and with hours for backward compatibility
363 // If found and eatby/sellby defined into table and provided and differs, return error
364 $this->errors[] = $langs->transnoentitiesnoconv("ThisSerialAlreadyExistWithDifferentDate", $batch, dol_print_date($this->db->jdate($obj->sellby)), dol_print_date($sellby));
365 dol_syslog($langs->transnoentities("ThisSerialAlreadyExistWithDifferentDate", $batch, dol_print_date($this->db->jdate($obj->sellby)), dol_print_date($sellby)), LOG_ERR);
366 $this->db->rollback();
367 return -3;
368 }
369 } else {
370 $sellby = $obj->sellby; // If found and eatby/sellby defined into table and not provided, we take value from table
371 }
372 } else {
373 if ($sellby) { // If found and eatby/sellby not defined into table and provided, we update table
374 $productlot = new Productlot($this->db);
375 $result = $productlot->fetch($obj->rowid);
376 $productlot->sellby = $sellby;
377 $result = $productlot->update($user);
378 if ($result <= 0) {
379 $this->error = $productlot->error;
380 $this->errors = $productlot->errors;
381 $this->db->rollback();
382 return -5;
383 }
384 }
385 }
386
387 $i++;
388 }
389 } else { // If not found, we add record
390 $productlot = new Productlot($this->db);
391 $productlot->origin = !empty($this->origin_type) ? $this->origin_type : '';
392 $productlot->origin_id = !empty($this->origin_id) ? $this->origin_id : 0;
393 $productlot->entity = $conf->entity;
394 $productlot->fk_product = $fk_product;
395 $productlot->batch = $batch;
396 // If we are here = first time we manage this batch, so we used dates provided by users to create lot
397 $productlot->eatby = $eatby;
398 $productlot->sellby = $sellby;
399 $result = $productlot->create($user);
400 if ($result <= 0) {
401 $this->error = $productlot->error;
402 $this->errors = $productlot->errors;
403 $this->db->rollback();
404 return -4;
405 }
406 }
407 } else {
408 dol_print_error($this->db);
409 $this->db->rollback();
410 return -1;
411 }
412 }
413
414 // Check if stock is enough when qty is < 0
415 // Note that qty should be > 0 with type 0 or 3, < 0 with type 1 or 2.
416 if ($movestock && $qty < 0 && !getDolGlobalInt('STOCK_ALLOW_NEGATIVE_TRANSFER')) {
417 if (isModEnabled('productbatch') && $product->hasbatch() && !$skip_batch) {
418 $foundforbatch = 0;
419 $qtyisnotenough = 0;
420
421 foreach ($product->stock_warehouse[$entrepot_id]->detail_batch as $batchcursor => $prodbatch) {
422 if ((string) $batch != (string) $batchcursor) { // Lot '59' must be different than lot '59c'
423 continue;
424 }
425
426 $foundforbatch = 1;
427 if ($prodbatch->qty < abs($qty)) {
428 $qtyisnotenough = $prodbatch->qty;
429 }
430 break;
431 }
432 if (!$foundforbatch || $qtyisnotenough) {
433 $langs->load("stocks");
434 include_once DOL_DOCUMENT_ROOT.'/product/stock/class/entrepot.class.php';
435 $tmpwarehouse = new Entrepot($this->db);
436 $tmpwarehouse->fetch($entrepot_id);
437
438 $this->error = $langs->trans('qtyToTranferLotIsNotEnough', $product->ref, $batch, $qtyisnotenough, $tmpwarehouse->ref);
439 $this->errors[] = $langs->trans('qtyToTranferLotIsNotEnough', $product->ref, $batch, $qtyisnotenough, $tmpwarehouse->ref);
440 $this->db->rollback();
441 return -8;
442 }
443 } else {
444 if (empty($product->stock_warehouse[$entrepot_id]->real) || $product->stock_warehouse[$entrepot_id]->real < abs($qty)) {
445 $langs->load("stocks");
446 $this->error = $langs->trans('qtyToTranferIsNotEnough').' : '.$product->ref;
447 $this->errors[] = $langs->trans('qtyToTranferIsNotEnough').' : '.$product->ref;
448 $this->db->rollback();
449 return -8;
450 }
451 }
452 }
453
454 if ($movestock) { // Change stock for current product, change for subproduct is done after
455 // Set $origin_type, origin_id and fk_project
456 $fk_project = $this->fk_project;
457 if (!empty($this->origin_type)) { // This is set by caller for tracking reason
458 $origin_type = $this->origin_type;
459 $origin_id = $this->origin_id;
460 if (empty($fk_project) && $origin_type == 'project') {
461 $fk_project = $origin_id;
462 $origin_type = '';
463 $origin_id = 0;
464 }
465 } else {
466 $fk_project = 0;
467 $origin_type = '';
468 $origin_id = 0;
469 }
470
471 $sql = "INSERT INTO ".$this->db->prefix()."stock_mouvement(";
472 $sql .= " datem, fk_product, batch, eatby, sellby,";
473 $sql .= " fk_entrepot, value, type_mouvement, fk_user_author, label, inventorycode, price, fk_origin, origintype, fk_projet";
474 $sql .= ")";
475 $sql .= " VALUES ('".$this->db->idate($this->datem)."', ".((int) $this->product_id).", ";
476 $sql .= " ".($batch ? "'".$this->db->escape($batch)."'" : "null").", ";
477 $sql .= " ".($eatby ? "'".$this->db->idate($eatby)."'" : "null").", ";
478 $sql .= " ".($sellby ? "'".$this->db->idate($sellby)."'" : "null").", ";
479 $sql .= " ".((int) $this->entrepot_id).", ".((float) $this->qty).", ".((int) $this->type).",";
480 $sql .= " ".((int) $user->id).",";
481 $sql .= " '".$this->db->escape($label)."',";
482 $sql .= " ".($inventorycode ? "'".$this->db->escape($inventorycode)."'" : "null").",";
483 $sql .= " ".((float) price2num($price)).",";
484 $sql .= " ".((int) $origin_id).",";
485 $sql .= " '".$this->db->escape($origin_type)."',";
486 $sql .= " ".((int) $fk_project);
487 $sql .= ")";
488
489 dol_syslog(get_class($this)."::_create insert record into stock_mouvement", LOG_DEBUG);
490 $resql = $this->db->query($sql);
491
492 if ($resql) {
493 $mvid = $this->db->last_insert_id($this->db->prefix()."stock_mouvement");
494 $this->id = $mvid;
495 } else {
496 $this->error = $this->db->lasterror();
497 $this->errors[] = $this->error;
498 $error = -1;
499 }
500
501 // Define current values for qty and pmp
502 $oldqty = $product->stock_reel;
503 $oldpmp = $product->pmp;
504 $oldqtywarehouse = 0;
505
506 // Test if there is already a record for couple (warehouse / product), so later we will make an update or create.
507 $alreadyarecord = 0;
508 $fk_product_stock = 0;
509 if (!$error) {
510 $sql = "SELECT rowid, reel FROM ".$this->db->prefix()."product_stock";
511 $sql .= " WHERE fk_entrepot = ".((int) $entrepot_id)." AND fk_product = ".((int) $fk_product); // This is a unique key
512
513 dol_syslog(get_class($this)."::_create check if a record already exists in product_stock", LOG_DEBUG);
514 $resql = $this->db->query($sql);
515 if ($resql) {
516 $obj = $this->db->fetch_object($resql);
517 if ($obj) {
518 $alreadyarecord = 1;
519 $oldqtywarehouse = $obj->reel;
520 $fk_product_stock = $obj->rowid;
521 }
522 $this->db->free($resql);
523 } else {
524 $this->errors[] = $this->db->lasterror();
525 $error = -2;
526 }
527 }
528
529 // Calculate new AWP (PMP)
530 $newpmp = 0;
531 if (!$error) {
532 if ($type == 0 || $type == 3) {
533 // After a stock increase
534 // Note: PMP is calculated on stock input only (type of movement = 0 or 3). If type == 0 or 3, qty should be > 0.
535 // Note: Price should always be >0 or 0. PMP should be always >0 (calculated on input)
536 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')))) {
537 $oldqtytouse = ($oldqty >= 0 ? $oldqty : 0);
538 // We make a test on oldpmp>0 to avoid to use normal rule on old data with no pmp field defined
539 if ($oldpmp > 0 && ($oldqtytouse + $qty) != 0) {
540 $newpmp = price2num((($oldqtytouse * $oldpmp) + ($qty * $price)) / ($oldqtytouse + $qty), 'MU');
541 } else {
542 $newpmp = $price; // For this product, PMP was not yet set. We set it to input price.
543 }
544 //print "oldqtytouse=".$oldqtytouse." oldpmp=".$oldpmp." oldqtywarehousetouse=".$oldqtywarehousetouse." ";
545 //print "qty=".$qty." newpmp=".$newpmp;
546 //exit;
547 } else {
548 $newpmp = $oldpmp;
549 }
550 } elseif ($type == 1 || $type == 2) {
551 // After a stock decrease, we don't change value of the AWP/PMP of a product.
552 $newpmp = $oldpmp;
553 } else {
554 // Type of movement unknown
555 $newpmp = $oldpmp;
556 }
557 }
558 // Update stock quantity
559 if (!$error) {
560 if ($alreadyarecord > 0) {
561 $sql = "UPDATE ".$this->db->prefix()."product_stock SET reel = " . ((float) $oldqtywarehouse + (float) $qty);
562 $sql .= " WHERE fk_entrepot = ".((int) $entrepot_id)." AND fk_product = ".((int) $fk_product);
563 } else {
564 $sql = "INSERT INTO ".$this->db->prefix()."product_stock";
565 $sql .= " (reel, fk_entrepot, fk_product) VALUES ";
566 $sql .= " (".((float) $qty).", ".((int) $entrepot_id).", ".((int) $fk_product).")";
567 }
568
569 dol_syslog(get_class($this)."::_create update stock value", LOG_DEBUG);
570 $resql = $this->db->query($sql);
571 if (!$resql) {
572 $this->errors[] = $this->db->lasterror();
573 $error = -3;
574 } elseif (empty($fk_product_stock)) {
575 $fk_product_stock = $this->db->last_insert_id($this->db->prefix()."product_stock");
576 }
577 }
578
579 // Update detail of stock for the lot.
580 if (!$error && isModEnabled('productbatch') && (($product->hasbatch() && !$skip_batch) || $force_update_batch)) {
581 if ($id_product_batch > 0) {
582 $result = $this->createBatch($id_product_batch, $qty);
583 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
584 $param_batch = array('fk_product_stock' =>$fk_product_stock, 'batchnumber'=>$batch);
585 $result = $this->createBatch($param_batch, $qty);
586 }
587 } else {
588 $param_batch = array('fk_product_stock' =>$fk_product_stock, 'batchnumber'=>$batch);
589 $result = $this->createBatch($param_batch, $qty);
590 }
591 if ($result < 0) {
592 $error++;
593 }
594 }
595
596 // Update PMP and denormalized value of stock qty at product level
597 if (!$error) {
598 $newpmp = price2num($newpmp, 'MU');
599
600 // $sql = "UPDATE ".$this->db->prefix()."product SET pmp = ".$newpmp.", stock = ".$this->db->ifsql("stock IS NULL", 0, "stock") . " + ".$qty;
601 // $sql.= " WHERE rowid = ".((int) $fk_product);
602 // Update pmp + denormalized fields because we change content of produt_stock. Warning: Do not use "SET p.stock", does not works with pgsql
603 $sql = "UPDATE ".$this->db->prefix()."product as p SET pmp = ".((float) $newpmp).",";
604 $sql .= " stock=(SELECT SUM(ps.reel) FROM ".$this->db->prefix()."product_stock as ps WHERE ps.fk_product = p.rowid)";
605 $sql .= " WHERE rowid = ".((int) $fk_product);
606
607 dol_syslog(get_class($this)."::_create update AWP", LOG_DEBUG);
608 $resql = $this->db->query($sql);
609 if (!$resql) {
610 $this->errors[] = $this->db->lasterror();
611 $error = -4;
612 }
613 }
614
615 if (empty($donotcleanemptylines)) {
616 // 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
617 // having a lot1/qty=X and lot2/qty=-X, so 0 but we must not loose repartition of different lot.
618 $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)";
619 $resql = $this->db->query($sql);
620 // We do not test error, it can fails if there is child in batch details
621 }
622 }
623
624 // Add movement for sub products (recursive call)
625 if (!$error && getDolGlobalString('PRODUIT_SOUSPRODUITS') && !getDolGlobalString('INDEPENDANT_SUBPRODUCT_STOCK') && empty($disablestockchangeforsubproduct)) {
626 $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
627 }
628
629 if ($movestock && !$error) {
630 // Call trigger
631 $result = $this->call_trigger('STOCK_MOVEMENT', $user);
632 if ($result < 0) {
633 $error++;
634 }
635 // End call triggers
636
637 // Check unicity for serial numbered equipments once all movement were done.
638 if (!$error && isModEnabled('productbatch') && $product->hasbatch() && !$skip_batch) {
639 if ($product->status_batch == 2 && $qty > 0) { // We check only if we increased qty
640 if ($this->getBatchCount($fk_product, $batch) > 1) {
641 $error++;
642 $this->errors[] = $langs->trans("TooManyQtyForSerialNumber", $product->ref, $batch);
643 }
644 }
645 }
646 }
647
648 if (!$error) {
649 $this->db->commit();
650 return $mvid;
651 } else {
652 $this->db->rollback();
653 dol_syslog(get_class($this)."::_create error code=".$error, LOG_ERR);
654 return -6;
655 }
656 }
657
658
659
667 public function fetch($id)
668 {
669 dol_syslog(__METHOD__, LOG_DEBUG);
670
671 $sql = "SELECT";
672 $sql .= " t.rowid,";
673 $sql .= " t.tms,";
674 $sql .= " t.datem,";
675 $sql .= " t.fk_product,";
676 $sql .= " t.fk_entrepot,";
677 $sql .= " t.value,";
678 $sql .= " t.price,";
679 $sql .= " t.type_mouvement,";
680 $sql .= " t.fk_user_author,";
681 $sql .= " t.label,";
682 $sql .= " t.fk_origin as origin_id,";
683 $sql .= " t.origintype as origin_type,";
684 $sql .= " t.inventorycode,";
685 $sql .= " t.batch,";
686 $sql .= " t.eatby,";
687 $sql .= " t.sellby,";
688 $sql .= " t.fk_projet as fk_project";
689 $sql .= " FROM ".$this->db->prefix().$this->table_element." as t";
690 $sql .= " WHERE t.rowid = ".((int) $id);
691
692 $resql = $this->db->query($sql);
693 if ($resql) {
694 $numrows = $this->db->num_rows($resql);
695 if ($numrows) {
696 $obj = $this->db->fetch_object($resql);
697
698 $this->id = $obj->rowid;
699
700 $this->product_id = $obj->fk_product;
701 $this->warehouse_id = $obj->fk_entrepot;
702 $this->qty = $obj->value;
703 $this->type = $obj->type_mouvement;
704
705 $this->tms = $this->db->jdate($obj->tms);
706 $this->datem = $this->db->jdate($obj->datem);
707 $this->price = $obj->price;
708 $this->fk_user_author = $obj->fk_user_author;
709 $this->label = $obj->label;
710 $this->fk_origin = $obj->origin_id; // For backward compatibility
711 $this->origintype = $obj->origin_type; // For backward compatibility
712 $this->origin_id = $obj->origin_id;
713 $this->origin_type = $obj->origin_type;
714 $this->inventorycode = $obj->inventorycode;
715 $this->batch = $obj->batch;
716 $this->eatby = $this->db->jdate($obj->eatby);
717 $this->sellby = $this->db->jdate($obj->sellby);
718 $this->fk_project = $obj->fk_project;
719 }
720
721 // Retrieve all extrafield
722 $this->fetch_optionals();
723
724 // $this->fetch_lines();
725
726 $this->db->free($resql);
727
728 if ($numrows) {
729 return 1;
730 } else {
731 return 0;
732 }
733 } else {
734 $this->errors[] = 'Error '.$this->db->lasterror();
735 dol_syslog(__METHOD__.' '.implode(',', $this->errors), LOG_ERR);
736
737 return -1;
738 }
739 }
740
741
742
743
758 private function _createSubProduct($user, $idProduct, $entrepot_id, $qty, $type, $price = 0, $label = '', $inventorycode = '', $datem = '')
759 {
760 global $langs;
761
762 $error = 0;
763 $pids = array();
764 $pqtys = array();
765
766 $sql = "SELECT fk_product_pere, fk_product_fils, qty";
767 $sql .= " FROM ".$this->db->prefix()."product_association";
768 $sql .= " WHERE fk_product_pere = ".((int) $idProduct);
769 $sql .= " AND incdec = 1";
770
771 dol_syslog(get_class($this)."::_createSubProduct for parent product ".$idProduct, LOG_DEBUG);
772 $resql = $this->db->query($sql);
773 if ($resql) {
774 $i = 0;
775 while ($obj = $this->db->fetch_object($resql)) {
776 $pids[$i] = $obj->fk_product_fils;
777 $pqtys[$i] = $obj->qty;
778 $i++;
779 }
780 $this->db->free($resql);
781 } else {
782 $error = -2;
783 }
784
785 // Create movement for each subproduct
786 foreach ($pids as $key => $value) {
787 if (!$error) {
788 $tmpmove = dol_clone($this, 1);
789
790 $result = $tmpmove->_create($user, $pids[$key], $entrepot_id, ($qty * $pqtys[$key]), $type, 0, $label, $inventorycode, $datem); // This will also call _createSubProduct making this recursive
791 if ($result < 0) {
792 $this->error = $tmpmove->error;
793 $this->errors = array_merge($this->errors, $tmpmove->errors);
794 if ($result == -2) {
795 $this->errors[] = $langs->trans("ErrorNoteAlsoThatSubProductCantBeFollowedByLot");
796 }
797 $error = $result;
798 }
799 unset($tmpmove);
800 }
801 }
802
803 return $error;
804 }
805
806
825 public function livraison($user, $fk_product, $entrepot_id, $qty, $price = 0, $label = '', $datem = '', $eatby = '', $sellby = '', $batch = '', $id_product_batch = 0, $inventorycode = '', $donotcleanemptylines = 0)
826 {
827 global $conf;
828
829 $skip_batch = empty($conf->productbatch->enabled);
830
831 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);
832 }
833
852 public function reception($user, $fk_product, $entrepot_id, $qty, $price = 0, $label = '', $eatby = '', $sellby = '', $batch = '', $datem = '', $id_product_batch = 0, $inventorycode = '', $donotcleanemptylines = 0)
853 {
854 global $conf;
855
856 $skip_batch = empty($conf->productbatch->enabled);
857
858 return $this->_create($user, $fk_product, $entrepot_id, $qty, 3, $price, $label, $inventorycode, $datem, $eatby, $sellby, $batch, $skip_batch, $id_product_batch, 0, $donotcleanemptylines);
859 }
860
868 public function calculateBalanceForProductBefore($productidselected, $datebefore)
869 {
870 $nb = 0;
871
872 $sql = "SELECT SUM(value) as nb from ".$this->db->prefix()."stock_mouvement";
873 $sql .= " WHERE fk_product = ".((int) $productidselected);
874 $sql .= " AND datem < '".$this->db->idate($datebefore)."'";
875
876 dol_syslog(get_class($this).__METHOD__, LOG_DEBUG);
877 $resql = $this->db->query($sql);
878 if ($resql) {
879 $obj = $this->db->fetch_object($resql);
880 if ($obj) {
881 $nb = $obj->nb;
882 }
883 return (empty($nb) ? 0 : $nb);
884 } else {
885 dol_print_error($this->db);
886 return -1;
887 }
888 }
889
899 private function createBatch($dluo, $qty)
900 {
901 global $user, $langs;
902
903 $langs->load('productbatch');
904
905 $pdluo = new Productbatch($this->db);
906
907 $result = 0;
908
909 // Try to find an existing record with same batch number or id
910 if (is_numeric($dluo)) {
911 $result = $pdluo->fetch($dluo);
912 if (empty($pdluo->id)) {
913 // We didn't find the line. May be it was deleted before by a previous move in same transaction.
914 $this->error = $langs->trans('CantMoveNonExistantSerial');
915 $this->errors[] = $this->error;
916 $result = -2;
917 }
918 } elseif (is_array($dluo)) {
919 if (isset($dluo['fk_product_stock'])) {
920 $vfk_product_stock = $dluo['fk_product_stock'];
921 $vbatchnumber = $dluo['batchnumber'];
922
923 $result = $pdluo->find($vfk_product_stock, '', '', $vbatchnumber); // Search on batch number only (eatby and sellby are deprecated here)
924 } else {
925 dol_syslog(get_class($this)."::createBatch array param dluo must contain at least key fk_product_stock", LOG_ERR);
926 $result = -1;
927 }
928 } else {
929 dol_syslog(get_class($this)."::createBatch error invalid param dluo", LOG_ERR);
930 $result = -1;
931 }
932
933 if ($result >= 0) {
934 // No error
935 if ($pdluo->id > 0) { // product_batch record found
936 //print "Avant ".$pdluo->qty." Apres ".($pdluo->qty + $qty)."<br>";
937 $pdluo->qty += $qty;
938 if ($pdluo->qty == 0) {
939 $result = $pdluo->delete($user, 1);
940 } else {
941 $result = $pdluo->update($user, 1);
942 }
943 } else { // product_batch record not found
944 $pdluo->fk_product_stock = $vfk_product_stock;
945 $pdluo->qty = $qty;
946 $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.
947 $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.
948 $pdluo->batch = $vbatchnumber;
949
950 $result = $pdluo->create($user, 1);
951 if ($result < 0) {
952 $this->error = $pdluo->error;
953 $this->errors = $pdluo->errors;
954 }
955 }
956 }
957
958 return $result;
959 }
960
961 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
969 public function get_origin($origin_id, $origin_type)
970 {
971 // phpcs:enable
972 $origin = '';
973
974 switch ($origin_type) {
975 case 'commande':
976 require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
977 $origin = new Commande($this->db);
978 break;
979 case 'shipping':
980 require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
981 $origin = new Expedition($this->db);
982 break;
983 case 'facture':
984 require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
985 $origin = new Facture($this->db);
986 break;
987 case 'order_supplier':
988 require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.commande.class.php';
989 $origin = new CommandeFournisseur($this->db);
990 break;
991 case 'invoice_supplier':
992 require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
993 $origin = new FactureFournisseur($this->db);
994 break;
995 case 'project':
996 require_once DOL_DOCUMENT_ROOT.'/projet/class/project.class.php';
997 $origin = new Project($this->db);
998 break;
999 case 'mo':
1000 require_once DOL_DOCUMENT_ROOT.'/mrp/class/mo.class.php';
1001 $origin = new Mo($this->db);
1002 break;
1003 case 'user':
1004 require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';
1005 $origin = new User($this->db);
1006 break;
1007 case 'reception':
1008 require_once DOL_DOCUMENT_ROOT.'/reception/class/reception.class.php';
1009 $origin = new Reception($this->db);
1010 break;
1011 case 'inventory':
1012 require_once DOL_DOCUMENT_ROOT.'/product/inventory/class/inventory.class.php';
1013 $origin = new Inventory($this->db);
1014 break;
1015 default:
1016 if ($origin_type) {
1017 // Separate originetype with "@" : left part is class name, right part is module name
1018 $origin_type_array = explode('@', $origin_type);
1019 $classname = $origin_type_array[0];
1020 $modulename = empty($origin_type_array[1]) ? strtolower($classname) : $origin_type_array[1];
1021
1022 $result = dol_include_once('/'.$modulename.'/class/'.$classname.'.class.php');
1023
1024 if ($result) {
1025 $classname = ucfirst($classname);
1026 $origin = new $classname($this->db);
1027 }
1028 }
1029 break;
1030 }
1031
1032 if (empty($origin) || !is_object($origin)) {
1033 return '';
1034 }
1035
1036 if ($origin->fetch($origin_id) > 0) {
1037 return $origin->getNomUrl(1);
1038 }
1039
1040 return '';
1041 }
1042
1053 public function setOrigin($origin_element, $origin_id, $line_id_object_src = 0, $line_id_object_origin = 0)
1054 {
1055 $this->origin_type = $origin_element;
1056 $this->origin_id = $origin_id;
1057 $this->line_id_object_src = $line_id_object_src;
1058 $this->line_id_object_origin = $line_id_object_origin;
1059 // For backward compatibility
1060 $this->origintype = $origin_element;
1061 $this->fk_origin = $origin_id;
1062 }
1063
1064
1072 public function initAsSpecimen()
1073 {
1074 global $user, $langs, $conf, $mysoc;
1075
1076 // Initialize parameters
1077 $this->id = 0;
1078
1079 // There is no specific properties. All data into insert are provided as method parameter.
1080 }
1081
1088 public function getTypeMovement($withlabel = 0)
1089 {
1090 global $langs;
1091
1092 $s = '';
1093 switch ($this->type) {
1094 case "0":
1095 $s = '<span class="fa fa-level-down-alt stockmovemententry stockmovementtransfer" title="'.$langs->trans('StockIncreaseAfterCorrectTransfer').'"></span>';
1096 if ($withlabel) {
1097 $s .= $langs->trans('StockIncreaseAfterCorrectTransfer');
1098 }
1099 break;
1100 case "1":
1101 $s = '<span class="fa fa-level-up-alt stockmovementexit stockmovementtransfer" title="'.$langs->trans('StockDecreaseAfterCorrectTransfer').'"></span>';
1102 if ($withlabel) {
1103 $s .= $langs->trans('StockDecreaseAfterCorrectTransfer');
1104 }
1105 break;
1106 case "2":
1107 $s = '<span class="fa fa-long-arrow-alt-up stockmovementexit stockmovement" title="'.$langs->trans('StockDecrease').'"></span>';
1108 if ($withlabel) {
1109 $s .= $langs->trans('StockDecrease');
1110 }
1111 break;
1112 case "3":
1113 $s = '<span class="fa fa-long-arrow-alt-down stockmovemententry stockmovement" title="'.$langs->trans('StockIncrease').'"></span>';
1114 if ($withlabel) {
1115 $s .= $langs->trans('StockIncrease');
1116 }
1117 break;
1118 }
1119
1120 return $s;
1121 }
1122
1134 public function getNomUrl($withpicto = 0, $option = '', $notooltip = 0, $maxlen = 24, $morecss = '')
1135 {
1136 global $langs, $conf, $db;
1137
1138 $result = '';
1139
1140 $label = img_picto('', 'stock', 'class="pictofixedwidth"').'<u>'.$langs->trans("Movement").' '.$this->id.'</u>';
1141 $label .= '<div width="100%">';
1142 $label .= '<b>'.$langs->trans('Label').':</b> '.$this->label;
1143 $label .= '<br><b>'.$langs->trans('Qty').':</b> '.($this->qty > 0 ? '+' : '').$this->qty;
1144 if ($this->batch) {
1145 $label .= '<br><b>'.$langs->trans('Batch').':</b> '.$this->batch;
1146 }
1147 /* TODO Get also warehouse label in a property instead of id
1148 if ($this->warehouse_id > 0) {
1149 $label .= '<br><b>'.$langs->trans('Warehouse').':</b> '.$this->warehouse_id;
1150 }*/
1151 $label .= '</div>';
1152
1153 // Link to page of warehouse tab
1154 if ($option == 'movements') {
1155 $url = DOL_URL_ROOT.'/product/stock/movement_list.php?search_ref='.$this->id;
1156 } else {
1157 $url = DOL_URL_ROOT.'/product/stock/movement_list.php?id='.$this->warehouse_id.'&msid='.$this->id;
1158 }
1159
1160 $link = '<a href="'.$url.'"'.($notooltip ? '' : ' title="'.dol_escape_htmltag($label, 1).'" class="classfortooltip'.($morecss ? ' '.$morecss : '').'"');
1161 $link .= '>';
1162 $linkend = '</a>';
1163
1164 if ($withpicto) {
1165 $result .= ($link.img_object(($notooltip ? '' : $label), 'stock', ($notooltip ? '' : 'class="classfortooltip"')).$linkend);
1166 if ($withpicto != 2) {
1167 $result .= ' ';
1168 }
1169 }
1170 $result .= $link.$this->id.$linkend;
1171 return $result;
1172 }
1173
1180 public function getLibStatut($mode = 0)
1181 {
1182 return $this->LibStatut($mode);
1183 }
1184
1185 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1192 public function LibStatut($mode = 0)
1193 {
1194 // phpcs:enable
1195 global $langs;
1196
1197 if ($mode == 0 || $mode == 1) {
1198 return $langs->trans('StatusNotApplicable');
1199 } elseif ($mode == 2) {
1200 return img_picto($langs->trans('StatusNotApplicable'), 'statut9').' '.$langs->trans('StatusNotApplicable');
1201 } elseif ($mode == 3) {
1202 return img_picto($langs->trans('StatusNotApplicable'), 'statut9');
1203 } elseif ($mode == 4) {
1204 return img_picto($langs->trans('StatusNotApplicable'), 'statut9').' '.$langs->trans('StatusNotApplicable');
1205 } elseif ($mode == 5) {
1206 return $langs->trans('StatusNotApplicable').' '.img_picto($langs->trans('StatusNotApplicable'), 'statut9');
1207 }
1208
1209 return 'Bad value for mode';
1210 }
1211
1222 public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0)
1223 {
1224 global $conf, $user, $langs;
1225
1226 $langs->load("stocks");
1227 $outputlangs->load("products");
1228
1229 if (!dol_strlen($modele)) {
1230 $modele = 'stdmovement';
1231
1232 if ($this->model_pdf) {
1233 $modele = $this->model_pdf;
1234 } elseif (getDolGlobalString('MOUVEMENT_ADDON_PDF')) {
1235 $modele = $conf->global->MOUVEMENT_ADDON_PDF;
1236 }
1237 }
1238
1239 $modelpath = "core/modules/stock/doc/";
1240
1241 return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref);
1242 }
1243
1251 public function delete(User $user, $notrigger = false)
1252 {
1253 return $this->deleteCommon($user, $notrigger);
1254 //return $this->deleteCommon($user, $notrigger, 1);
1255 }
1256
1264 private function getBatchCount($fk_product, $batch)
1265 {
1266 $cpt = 0;
1267
1268 $sql = "SELECT sum(pb.qty) as cpt";
1269 $sql .= " FROM ".$this->db->prefix()."product_batch as pb";
1270 $sql .= " INNER JOIN ".$this->db->prefix()."product_stock as ps ON ps.rowid = pb.fk_product_stock";
1271 $sql .= " WHERE ps.fk_product = " . ((int) $fk_product);
1272 $sql .= " AND pb.batch = '" . $this->db->escape($batch) . "'";
1273
1274 $result = $this->db->query($sql);
1275 if ($result) {
1276 if ($this->db->num_rows($result)) {
1277 $obj = $this->db->fetch_object($result);
1278 $cpt = $obj->cpt;
1279 }
1280
1281 $this->db->free($result);
1282 } else {
1283 dol_print_error($this->db);
1284 return -1;
1285 }
1286
1287 return $cpt;
1288 }
1289
1294 public function reverseMouvement()
1295 {
1296 $formattedDate = "REVERTMV" .dol_print_date($this->datem, '%Y%m%d%His');
1297 if ($this->label == 'Annulation mouvement ID'.$this->id) {
1298 return -1;
1299 }
1300 if ($this->inventorycode == $formattedDate) {
1301 return -1;
1302 }
1303
1304 $sql = "UPDATE ".$this->db->prefix()."stock_mouvement SET";
1305 $sql .= " label = 'Annulation mouvement ID ".((int) $this->id)."',";
1306 $sql .= "inventorycode = '".($formattedDate)."'";
1307 $sql .= " WHERE rowid = ".((int) $this->id);
1308
1309 $resql = $this->db->query($sql);
1310
1311 if ($resql) {
1312 $this->db->commit();
1313 return 1;
1314 } else {
1315 $this->db->rollback();
1316 return -1;
1317 }
1318 }
1319}
Class to manage predefined suppliers products.
Class to manage customers orders.
Parent class of all other business classes (invoices, contracts, proposals, orders,...
fetch_optionals($rowid=null, $optionsArray=null)
Function to get extra fields of an object into $this->array_options This method is in most cases call...
commonGenerateDocument($modelspath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams=null)
Common function for all objects extending CommonObject for generating documents.
deleteCommon(User $user, $notrigger=false, $forcechilddeletion=0)
Delete object in database.
call_trigger($triggerName, $user)
Call trigger based on this instance.
Class to manage warehouses.
Class to manage shipments.
Class to manage suppliers invoices.
Class to manage invoices.
Class for Inventory.
Class for Mo.
Definition mo.class.php:34
Class to manage stock movements.
__construct($db)
Constructor.
generateDocument($modele, $outputlangs, $hidedetails=0, $hidedesc=0, $hideref=0)
Create object on disk.
getNomUrl($withpicto=0, $option='', $notooltip=0, $maxlen=24, $morecss='')
Return a link (with optionaly the picto) Use this->id,this->lastname, this->firstname.
setOrigin($origin_element, $origin_id, $line_id_object_src=0, $line_id_object_origin=0)
Set attribute origin_type and fk_origin to object.
LibStatut($mode=0)
Return the label of the status.
createBatch($dluo, $qty)
Create or update batch record (update table llx_product_batch).
getBatchCount($fk_product, $batch)
Retrieve number of equipments for a product lot/serial.
get_origin($origin_id, $origin_type)
Return Url link of origin object.
livraison($user, $fk_product, $entrepot_id, $qty, $price=0, $label='', $datem='', $eatby='', $sellby='', $batch='', $id_product_batch=0, $inventorycode='', $donotcleanemptylines=0)
Decrease stock for product and subproducts.
_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)
Add a movement of stock (in one direction only).
fetch($id)
Load object in memory from the database.
reception($user, $fk_product, $entrepot_id, $qty, $price=0, $label='', $eatby='', $sellby='', $batch='', $datem='', $id_product_batch=0, $inventorycode='', $donotcleanemptylines=0)
Increase stock for product and subproducts.
_createSubProduct($user, $idProduct, $entrepot_id, $qty, $type, $price=0, $label='', $inventorycode='', $datem='')
Create movement in database for all subproducts.
initAsSpecimen()
Initialise an instance with random values.
getTypeMovement($withlabel=0)
Return html string with picto for type of movement.
calculateBalanceForProductBefore($productidselected, $datebefore)
Count number of product in stock before a specific date.
getLibStatut($mode=0)
Return label statut.
reverseMouvement()
reverse mouvement for object by updating infos
Class to manage products or services.
const TYPE_SERVICE
Service.
Manage record for batch number management.
Class with list of lots and properties.
Class to manage projects.
Class to manage receptions.
Class to manage Dolibarr users.
dol_mktime($hour, $minute, $second, $month, $day, $year, $gm='auto', $check=1)
Return a timestamp date built from detailed informations (by default a local PHP server timestamp) Re...
price2num($amount, $rounding='', $option=0)
Function that return a number with universal decimal format (decimal separator is '.
dol_print_error($db='', $error='', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
dol_strlen($string, $stringencoding='UTF-8')
Make a strlen call.
price($amount, $form=0, $outlangs='', $trunc=1, $rounding=-1, $forcerounding=-1, $currency_code='')
Function to format a value into an amount for visual output Function used into PDF and HTML pages.
dol_print_date($time, $format='', $tzoutput='auto', $outputlangs='', $encodetooutput=false)
Output date in a string format according to outputlangs (or langs if not defined).
dol_now($mode='auto')
Return date for now.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
img_picto($titlealt, $picto, $moreatt='', $pictoisfullpath=false, $srconly=0, $notitle=0, $alt='', $morecss='', $marginleftonlyshort=2)
Show picto whatever it's its name (generic function)
if(!function_exists( 'dol_getprefix')) dol_include_once($relpath, $classname='')
Make an include_once using default root and alternate root if it fails.
dol_clone($object, $native=0)
Create a clone of instance of object (new instance with same value for each properties) With native =...
getDolGlobalString($key, $default='')
Return dolibarr global constant string value.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
dol_getdate($timestamp, $fast=false, $forcetimezone='')
Return an array with locale date info.
if(preg_match('/crypted:/i', $dolibarr_main_db_pass)||!empty($dolibarr_main_db_encrypted_pass)) $conf db type
Definition repair.php:121