dolibarr 19.0.3
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 if (!$error) {
509 $sql = "SELECT rowid, reel FROM ".$this->db->prefix()."product_stock";
510 $sql .= " WHERE fk_entrepot = ".((int) $entrepot_id)." AND fk_product = ".((int) $fk_product); // This is a unique key
511
512 dol_syslog(get_class($this)."::_create check if a record already exists in product_stock", LOG_DEBUG);
513 $resql = $this->db->query($sql);
514 if ($resql) {
515 $obj = $this->db->fetch_object($resql);
516 if ($obj) {
517 $alreadyarecord = 1;
518 $oldqtywarehouse = $obj->reel;
519 $fk_product_stock = $obj->rowid;
520 }
521 $this->db->free($resql);
522 } else {
523 $this->errors[] = $this->db->lasterror();
524 $error = -2;
525 }
526 }
527
528 // Calculate new AWP (PMP)
529 $newpmp = 0;
530 if (!$error) {
531 if ($type == 0 || $type == 3) {
532 // After a stock increase
533 // Note: PMP is calculated on stock input only (type of movement = 0 or 3). If type == 0 or 3, qty should be > 0.
534 // Note: Price should always be >0 or 0. PMP should be always >0 (calculated on input)
535 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')))) {
536 $oldqtytouse = ($oldqty >= 0 ? $oldqty : 0);
537 // We make a test on oldpmp>0 to avoid to use normal rule on old data with no pmp field defined
538 if ($oldpmp > 0) {
539 $newpmp = price2num((($oldqtytouse * $oldpmp) + ($qty * $price)) / ($oldqtytouse + $qty), 'MU');
540 } else {
541 $newpmp = $price; // For this product, PMP was not yet set. We set it to input price.
542 }
543 //print "oldqtytouse=".$oldqtytouse." oldpmp=".$oldpmp." oldqtywarehousetouse=".$oldqtywarehousetouse." ";
544 //print "qty=".$qty." newpmp=".$newpmp;
545 //exit;
546 } else {
547 $newpmp = $oldpmp;
548 }
549 } elseif ($type == 1 || $type == 2) {
550 // After a stock decrease, we don't change value of the AWP/PMP of a product.
551 $newpmp = $oldpmp;
552 } else {
553 // Type of movement unknown
554 $newpmp = $oldpmp;
555 }
556 }
557 // Update stock quantity
558 if (!$error) {
559 if ($alreadyarecord > 0) {
560 $sql = "UPDATE ".$this->db->prefix()."product_stock SET reel = " . ((float) $oldqtywarehouse + (float) $qty);
561 $sql .= " WHERE fk_entrepot = ".((int) $entrepot_id)." AND fk_product = ".((int) $fk_product);
562 } else {
563 $sql = "INSERT INTO ".$this->db->prefix()."product_stock";
564 $sql .= " (reel, fk_entrepot, fk_product) VALUES ";
565 $sql .= " (".((float) $qty).", ".((int) $entrepot_id).", ".((int) $fk_product).")";
566 }
567
568 dol_syslog(get_class($this)."::_create update stock value", LOG_DEBUG);
569 $resql = $this->db->query($sql);
570 if (!$resql) {
571 $this->errors[] = $this->db->lasterror();
572 $error = -3;
573 } elseif (empty($fk_product_stock)) {
574 $fk_product_stock = $this->db->last_insert_id($this->db->prefix()."product_stock");
575 }
576 }
577
578 // Update detail of stock for the lot.
579 if (!$error && isModEnabled('productbatch') && (($product->hasbatch() && !$skip_batch) || $force_update_batch)) {
580 if ($id_product_batch > 0) {
581 $result = $this->createBatch($id_product_batch, $qty);
582 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
583 $param_batch = array('fk_product_stock' =>$fk_product_stock, 'batchnumber'=>$batch);
584 $result = $this->createBatch($param_batch, $qty);
585 }
586 } else {
587 $param_batch = array('fk_product_stock' =>$fk_product_stock, 'batchnumber'=>$batch);
588 $result = $this->createBatch($param_batch, $qty);
589 }
590 if ($result < 0) {
591 $error++;
592 }
593 }
594
595 // Update PMP and denormalized value of stock qty at product level
596 if (!$error) {
597 $newpmp = price2num($newpmp, 'MU');
598
599 // $sql = "UPDATE ".$this->db->prefix()."product SET pmp = ".$newpmp.", stock = ".$this->db->ifsql("stock IS NULL", 0, "stock") . " + ".$qty;
600 // $sql.= " WHERE rowid = ".((int) $fk_product);
601 // Update pmp + denormalized fields because we change content of produt_stock. Warning: Do not use "SET p.stock", does not works with pgsql
602 $sql = "UPDATE ".$this->db->prefix()."product as p SET pmp = ".((float) $newpmp).",";
603 $sql .= " stock=(SELECT SUM(ps.reel) FROM ".$this->db->prefix()."product_stock as ps WHERE ps.fk_product = p.rowid)";
604 $sql .= " WHERE rowid = ".((int) $fk_product);
605
606 dol_syslog(get_class($this)."::_create update AWP", LOG_DEBUG);
607 $resql = $this->db->query($sql);
608 if (!$resql) {
609 $this->errors[] = $this->db->lasterror();
610 $error = -4;
611 }
612 }
613
614 if (empty($donotcleanemptylines)) {
615 // 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
616 // having a lot1/qty=X and lot2/qty=-X, so 0 but we must not loose repartition of different lot.
617 $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)";
618 $resql = $this->db->query($sql);
619 // We do not test error, it can fails if there is child in batch details
620 }
621 }
622
623 // Add movement for sub products (recursive call)
624 if (!$error && getDolGlobalString('PRODUIT_SOUSPRODUITS') && !getDolGlobalString('INDEPENDANT_SUBPRODUCT_STOCK') && empty($disablestockchangeforsubproduct)) {
625 $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
626 }
627
628 if ($movestock && !$error) {
629 // Call trigger
630 $result = $this->call_trigger('STOCK_MOVEMENT', $user);
631 if ($result < 0) {
632 $error++;
633 }
634 // End call triggers
635
636 // Check unicity for serial numbered equipments once all movement were done.
637 if (!$error && isModEnabled('productbatch') && $product->hasbatch() && !$skip_batch) {
638 if ($product->status_batch == 2 && $qty > 0) { // We check only if we increased qty
639 if ($this->getBatchCount($fk_product, $batch) > 1) {
640 $error++;
641 $this->errors[] = $langs->trans("TooManyQtyForSerialNumber", $product->ref, $batch);
642 }
643 }
644 }
645 }
646
647 if (!$error) {
648 $this->db->commit();
649 return $mvid;
650 } else {
651 $this->db->rollback();
652 dol_syslog(get_class($this)."::_create error code=".$error, LOG_ERR);
653 return -6;
654 }
655 }
656
657
658
666 public function fetch($id)
667 {
668 dol_syslog(__METHOD__, LOG_DEBUG);
669
670 $sql = "SELECT";
671 $sql .= " t.rowid,";
672 $sql .= " t.tms,";
673 $sql .= " t.datem,";
674 $sql .= " t.fk_product,";
675 $sql .= " t.fk_entrepot,";
676 $sql .= " t.value,";
677 $sql .= " t.price,";
678 $sql .= " t.type_mouvement,";
679 $sql .= " t.fk_user_author,";
680 $sql .= " t.label,";
681 $sql .= " t.fk_origin as origin_id,";
682 $sql .= " t.origintype as origin_type,";
683 $sql .= " t.inventorycode,";
684 $sql .= " t.batch,";
685 $sql .= " t.eatby,";
686 $sql .= " t.sellby,";
687 $sql .= " t.fk_projet as fk_project";
688 $sql .= " FROM ".$this->db->prefix().$this->table_element." as t";
689 $sql .= " WHERE t.rowid = ".((int) $id);
690
691 $resql = $this->db->query($sql);
692 if ($resql) {
693 $numrows = $this->db->num_rows($resql);
694 if ($numrows) {
695 $obj = $this->db->fetch_object($resql);
696
697 $this->id = $obj->rowid;
698
699 $this->product_id = $obj->fk_product;
700 $this->warehouse_id = $obj->fk_entrepot;
701 $this->qty = $obj->value;
702 $this->type = $obj->type_mouvement;
703
704 $this->tms = $this->db->jdate($obj->tms);
705 $this->datem = $this->db->jdate($obj->datem);
706 $this->price = $obj->price;
707 $this->fk_user_author = $obj->fk_user_author;
708 $this->label = $obj->label;
709 $this->fk_origin = $obj->origin_id; // For backward compatibility
710 $this->origintype = $obj->origin_type; // For backward compatibility
711 $this->origin_id = $obj->origin_id;
712 $this->origin_type = $obj->origin_type;
713 $this->inventorycode = $obj->inventorycode;
714 $this->batch = $obj->batch;
715 $this->eatby = $this->db->jdate($obj->eatby);
716 $this->sellby = $this->db->jdate($obj->sellby);
717 $this->fk_project = $obj->fk_project;
718 }
719
720 // Retrieve all extrafield
721 $this->fetch_optionals();
722
723 // $this->fetch_lines();
724
725 $this->db->free($resql);
726
727 if ($numrows) {
728 return 1;
729 } else {
730 return 0;
731 }
732 } else {
733 $this->errors[] = 'Error '.$this->db->lasterror();
734 dol_syslog(__METHOD__.' '.implode(',', $this->errors), LOG_ERR);
735
736 return -1;
737 }
738 }
739
740
741
742
757 private function _createSubProduct($user, $idProduct, $entrepot_id, $qty, $type, $price = 0, $label = '', $inventorycode = '', $datem = '')
758 {
759 global $langs;
760
761 $error = 0;
762 $pids = array();
763 $pqtys = array();
764
765 $sql = "SELECT fk_product_pere, fk_product_fils, qty";
766 $sql .= " FROM ".$this->db->prefix()."product_association";
767 $sql .= " WHERE fk_product_pere = ".((int) $idProduct);
768 $sql .= " AND incdec = 1";
769
770 dol_syslog(get_class($this)."::_createSubProduct for parent product ".$idProduct, LOG_DEBUG);
771 $resql = $this->db->query($sql);
772 if ($resql) {
773 $i = 0;
774 while ($obj = $this->db->fetch_object($resql)) {
775 $pids[$i] = $obj->fk_product_fils;
776 $pqtys[$i] = $obj->qty;
777 $i++;
778 }
779 $this->db->free($resql);
780 } else {
781 $error = -2;
782 }
783
784 // Create movement for each subproduct
785 foreach ($pids as $key => $value) {
786 if (!$error) {
787 $tmpmove = dol_clone($this, 1);
788
789 $result = $tmpmove->_create($user, $pids[$key], $entrepot_id, ($qty * $pqtys[$key]), $type, 0, $label, $inventorycode, $datem); // This will also call _createSubProduct making this recursive
790 if ($result < 0) {
791 $this->error = $tmpmove->error;
792 $this->errors = array_merge($this->errors, $tmpmove->errors);
793 if ($result == -2) {
794 $this->errors[] = $langs->trans("ErrorNoteAlsoThatSubProductCantBeFollowedByLot");
795 }
796 $error = $result;
797 }
798 unset($tmpmove);
799 }
800 }
801
802 return $error;
803 }
804
805
824 public function livraison($user, $fk_product, $entrepot_id, $qty, $price = 0, $label = '', $datem = '', $eatby = '', $sellby = '', $batch = '', $id_product_batch = 0, $inventorycode = '', $donotcleanemptylines = 0)
825 {
826 global $conf;
827
828 $skip_batch = empty($conf->productbatch->enabled);
829
830 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);
831 }
832
851 public function reception($user, $fk_product, $entrepot_id, $qty, $price = 0, $label = '', $eatby = '', $sellby = '', $batch = '', $datem = '', $id_product_batch = 0, $inventorycode = '', $donotcleanemptylines = 0)
852 {
853 global $conf;
854
855 $skip_batch = empty($conf->productbatch->enabled);
856
857 return $this->_create($user, $fk_product, $entrepot_id, $qty, 3, $price, $label, $inventorycode, $datem, $eatby, $sellby, $batch, $skip_batch, $id_product_batch, 0, $donotcleanemptylines);
858 }
859
867 public function calculateBalanceForProductBefore($productidselected, $datebefore)
868 {
869 $nb = 0;
870
871 $sql = "SELECT SUM(value) as nb from ".$this->db->prefix()."stock_mouvement";
872 $sql .= " WHERE fk_product = ".((int) $productidselected);
873 $sql .= " AND datem < '".$this->db->idate($datebefore)."'";
874
875 dol_syslog(get_class($this).__METHOD__, LOG_DEBUG);
876 $resql = $this->db->query($sql);
877 if ($resql) {
878 $obj = $this->db->fetch_object($resql);
879 if ($obj) {
880 $nb = $obj->nb;
881 }
882 return (empty($nb) ? 0 : $nb);
883 } else {
884 dol_print_error($this->db);
885 return -1;
886 }
887 }
888
898 private function createBatch($dluo, $qty)
899 {
900 global $user, $langs;
901
902 $langs->load('productbatch');
903
904 $pdluo = new Productbatch($this->db);
905
906 $result = 0;
907
908 // Try to find an existing record with same batch number or id
909 if (is_numeric($dluo)) {
910 $result = $pdluo->fetch($dluo);
911 if (empty($pdluo->id)) {
912 // We didn't find the line. May be it was deleted before by a previous move in same transaction.
913 $this->error = $langs->trans('CantMoveNonExistantSerial');
914 $this->errors[] = $this->error;
915 $result = -2;
916 }
917 } elseif (is_array($dluo)) {
918 if (isset($dluo['fk_product_stock'])) {
919 $vfk_product_stock = $dluo['fk_product_stock'];
920 $vbatchnumber = $dluo['batchnumber'];
921
922 $result = $pdluo->find($vfk_product_stock, '', '', $vbatchnumber); // Search on batch number only (eatby and sellby are deprecated here)
923 } else {
924 dol_syslog(get_class($this)."::createBatch array param dluo must contain at least key fk_product_stock", LOG_ERR);
925 $result = -1;
926 }
927 } else {
928 dol_syslog(get_class($this)."::createBatch error invalid param dluo", LOG_ERR);
929 $result = -1;
930 }
931
932 if ($result >= 0) {
933 // No error
934 if ($pdluo->id > 0) { // product_batch record found
935 //print "Avant ".$pdluo->qty." Apres ".($pdluo->qty + $qty)."<br>";
936 $pdluo->qty += $qty;
937 if ($pdluo->qty == 0) {
938 $result = $pdluo->delete($user, 1);
939 } else {
940 $result = $pdluo->update($user, 1);
941 }
942 } else { // product_batch record not found
943 $pdluo->fk_product_stock = $vfk_product_stock;
944 $pdluo->qty = $qty;
945 $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.
946 $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.
947 $pdluo->batch = $vbatchnumber;
948
949 $result = $pdluo->create($user, 1);
950 if ($result < 0) {
951 $this->error = $pdluo->error;
952 $this->errors = $pdluo->errors;
953 }
954 }
955 }
956
957 return $result;
958 }
959
960 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
968 public function get_origin($origin_id, $origin_type)
969 {
970 // phpcs:enable
971 $origin = '';
972
973 switch ($origin_type) {
974 case 'commande':
975 require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
976 $origin = new Commande($this->db);
977 break;
978 case 'shipping':
979 require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
980 $origin = new Expedition($this->db);
981 break;
982 case 'facture':
983 require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
984 $origin = new Facture($this->db);
985 break;
986 case 'order_supplier':
987 require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.commande.class.php';
988 $origin = new CommandeFournisseur($this->db);
989 break;
990 case 'invoice_supplier':
991 require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
992 $origin = new FactureFournisseur($this->db);
993 break;
994 case 'project':
995 require_once DOL_DOCUMENT_ROOT.'/projet/class/project.class.php';
996 $origin = new Project($this->db);
997 break;
998 case 'mo':
999 require_once DOL_DOCUMENT_ROOT.'/mrp/class/mo.class.php';
1000 $origin = new Mo($this->db);
1001 break;
1002 case 'user':
1003 require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';
1004 $origin = new User($this->db);
1005 break;
1006 case 'reception':
1007 require_once DOL_DOCUMENT_ROOT.'/reception/class/reception.class.php';
1008 $origin = new Reception($this->db);
1009 break;
1010 case 'inventory':
1011 require_once DOL_DOCUMENT_ROOT.'/product/inventory/class/inventory.class.php';
1012 $origin = new Inventory($this->db);
1013 break;
1014 default:
1015 if ($origin_type) {
1016 // Separate originetype with "@" : left part is class name, right part is module name
1017 $origin_type_array = explode('@', $origin_type);
1018 $classname = $origin_type_array[0];
1019 $modulename = empty($origin_type_array[1]) ? strtolower($classname) : $origin_type_array[1];
1020
1021 $result = dol_include_once('/'.$modulename.'/class/'.$classname.'.class.php');
1022
1023 if ($result) {
1024 $classname = ucfirst($classname);
1025 $origin = new $classname($this->db);
1026 }
1027 }
1028 break;
1029 }
1030
1031 if (empty($origin) || !is_object($origin)) {
1032 return '';
1033 }
1034
1035 if ($origin->fetch($origin_id) > 0) {
1036 return $origin->getNomUrl(1);
1037 }
1038
1039 return '';
1040 }
1041
1052 public function setOrigin($origin_element, $origin_id, $line_id_object_src = 0, $line_id_object_origin = 0)
1053 {
1054 $this->origin_type = $origin_element;
1055 $this->origin_id = $origin_id;
1056 $this->line_id_object_src = $line_id_object_src;
1057 $this->line_id_object_origin = $line_id_object_origin;
1058 // For backward compatibility
1059 $this->origintype = $origin_element;
1060 $this->fk_origin = $origin_id;
1061 }
1062
1063
1071 public function initAsSpecimen()
1072 {
1073 global $user, $langs, $conf, $mysoc;
1074
1075 // Initialize parameters
1076 $this->id = 0;
1077
1078 // There is no specific properties. All data into insert are provided as method parameter.
1079 }
1080
1087 public function getTypeMovement($withlabel = 0)
1088 {
1089 global $langs;
1090
1091 $s = '';
1092 switch ($this->type) {
1093 case "0":
1094 $s = '<span class="fa fa-level-down-alt stockmovemententry stockmovementtransfer" title="'.$langs->trans('StockIncreaseAfterCorrectTransfer').'"></span>';
1095 if ($withlabel) {
1096 $s .= $langs->trans('StockIncreaseAfterCorrectTransfer');
1097 }
1098 break;
1099 case "1":
1100 $s = '<span class="fa fa-level-up-alt stockmovementexit stockmovementtransfer" title="'.$langs->trans('StockDecreaseAfterCorrectTransfer').'"></span>';
1101 if ($withlabel) {
1102 $s .= $langs->trans('StockDecreaseAfterCorrectTransfer');
1103 }
1104 break;
1105 case "2":
1106 $s = '<span class="fa fa-long-arrow-alt-up stockmovementexit stockmovement" title="'.$langs->trans('StockDecrease').'"></span>';
1107 if ($withlabel) {
1108 $s .= $langs->trans('StockDecrease');
1109 }
1110 break;
1111 case "3":
1112 $s = '<span class="fa fa-long-arrow-alt-down stockmovemententry stockmovement" title="'.$langs->trans('StockIncrease').'"></span>';
1113 if ($withlabel) {
1114 $s .= $langs->trans('StockIncrease');
1115 }
1116 break;
1117 }
1118
1119 return $s;
1120 }
1121
1133 public function getNomUrl($withpicto = 0, $option = '', $notooltip = 0, $maxlen = 24, $morecss = '')
1134 {
1135 global $langs, $conf, $db;
1136
1137 $result = '';
1138
1139 $label = img_picto('', 'stock', 'class="pictofixedwidth"').'<u>'.$langs->trans("Movement").' '.$this->id.'</u>';
1140 $label .= '<div width="100%">';
1141 $label .= '<b>'.$langs->trans('Label').':</b> '.$this->label;
1142 $label .= '<br><b>'.$langs->trans('Qty').':</b> '.($this->qty > 0 ? '+' : '').$this->qty;
1143 if ($this->batch) {
1144 $label .= '<br><b>'.$langs->trans('Batch').':</b> '.$this->batch;
1145 }
1146 /* TODO Get also warehouse label in a property instead of id
1147 if ($this->warehouse_id > 0) {
1148 $label .= '<br><b>'.$langs->trans('Warehouse').':</b> '.$this->warehouse_id;
1149 }*/
1150 $label .= '</div>';
1151
1152 // Link to page of warehouse tab
1153 if ($option == 'movements') {
1154 $url = DOL_URL_ROOT.'/product/stock/movement_list.php?search_ref='.$this->id;
1155 } else {
1156 $url = DOL_URL_ROOT.'/product/stock/movement_list.php?id='.$this->warehouse_id.'&msid='.$this->id;
1157 }
1158
1159 $link = '<a href="'.$url.'"'.($notooltip ? '' : ' title="'.dol_escape_htmltag($label, 1).'" class="classfortooltip'.($morecss ? ' '.$morecss : '').'"');
1160 $link .= '>';
1161 $linkend = '</a>';
1162
1163 if ($withpicto) {
1164 $result .= ($link.img_object(($notooltip ? '' : $label), 'stock', ($notooltip ? '' : 'class="classfortooltip"')).$linkend);
1165 if ($withpicto != 2) {
1166 $result .= ' ';
1167 }
1168 }
1169 $result .= $link.$this->id.$linkend;
1170 return $result;
1171 }
1172
1179 public function getLibStatut($mode = 0)
1180 {
1181 return $this->LibStatut($mode);
1182 }
1183
1184 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1191 public function LibStatut($mode = 0)
1192 {
1193 // phpcs:enable
1194 global $langs;
1195
1196 if ($mode == 0 || $mode == 1) {
1197 return $langs->trans('StatusNotApplicable');
1198 } elseif ($mode == 2) {
1199 return img_picto($langs->trans('StatusNotApplicable'), 'statut9').' '.$langs->trans('StatusNotApplicable');
1200 } elseif ($mode == 3) {
1201 return img_picto($langs->trans('StatusNotApplicable'), 'statut9');
1202 } elseif ($mode == 4) {
1203 return img_picto($langs->trans('StatusNotApplicable'), 'statut9').' '.$langs->trans('StatusNotApplicable');
1204 } elseif ($mode == 5) {
1205 return $langs->trans('StatusNotApplicable').' '.img_picto($langs->trans('StatusNotApplicable'), 'statut9');
1206 }
1207
1208 return 'Bad value for mode';
1209 }
1210
1221 public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0)
1222 {
1223 global $conf, $user, $langs;
1224
1225 $langs->load("stocks");
1226 $outputlangs->load("products");
1227
1228 if (!dol_strlen($modele)) {
1229 $modele = 'stdmovement';
1230
1231 if ($this->model_pdf) {
1232 $modele = $this->model_pdf;
1233 } elseif (getDolGlobalString('MOUVEMENT_ADDON_PDF')) {
1234 $modele = $conf->global->MOUVEMENT_ADDON_PDF;
1235 }
1236 }
1237
1238 $modelpath = "core/modules/stock/doc/";
1239
1240 return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref);
1241 }
1242
1250 public function delete(User $user, $notrigger = false)
1251 {
1252 return $this->deleteCommon($user, $notrigger);
1253 //return $this->deleteCommon($user, $notrigger, 1);
1254 }
1255
1263 private function getBatchCount($fk_product, $batch)
1264 {
1265 $cpt = 0;
1266
1267 $sql = "SELECT sum(pb.qty) as cpt";
1268 $sql .= " FROM ".$this->db->prefix()."product_batch as pb";
1269 $sql .= " INNER JOIN ".$this->db->prefix()."product_stock as ps ON ps.rowid = pb.fk_product_stock";
1270 $sql .= " WHERE ps.fk_product = " . ((int) $fk_product);
1271 $sql .= " AND pb.batch = '" . $this->db->escape($batch) . "'";
1272
1273 $result = $this->db->query($sql);
1274 if ($result) {
1275 if ($this->db->num_rows($result)) {
1276 $obj = $this->db->fetch_object($result);
1277 $cpt = $obj->cpt;
1278 }
1279
1280 $this->db->free($result);
1281 } else {
1282 dol_print_error($this->db);
1283 return -1;
1284 }
1285
1286 return $cpt;
1287 }
1288
1293 public function reverseMouvement()
1294 {
1295 $formattedDate = "REVERTMV" .dol_print_date($this->datem, '%Y%m%d%His');
1296 if ($this->label == 'Annulation mouvement ID'.$this->id) {
1297 return -1;
1298 }
1299 if ($this->inventorycode == $formattedDate) {
1300 return -1;
1301 }
1302
1303 $sql = "UPDATE ".$this->db->prefix()."stock_mouvement SET";
1304 $sql .= " label = 'Annulation mouvement ID ".((int) $this->id)."',";
1305 $sql .= "inventorycode = '".($formattedDate)."'";
1306 $sql .= " WHERE rowid = ".((int) $this->id);
1307
1308 $resql = $this->db->query($sql);
1309
1310 if ($resql) {
1311 $this->db->commit();
1312 return 1;
1313 } else {
1314 $this->db->rollback();
1315 return -1;
1316 }
1317 }
1318}
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