dolibarr 21.0.0-alpha
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 * Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
7 * Copyright (C) 2024 Frédéric France <frederic.france@free.fr>
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 3 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 */
22
34{
38 public $element = 'stockmouvement';
39
43 public $table_element = 'stock_mouvement';
44
45
49 public $product_id;
50
56 public $entrepot_id;
57
61 public $warehouse_id;
62
66 public $qty;
67
76 public $type;
77
81 public $datem = '';
82 public $price;
83
87 public $fk_user_author;
88
92 public $label;
93
99 public $fk_origin;
100
104 public $origin_id;
105
111 public $origintype;
112
116 public $origin_type;
117 public $line_id_oject_src;
118 public $line_id_oject_origin;
119
123 public $inventorycode;
124
128 public $batch;
129
130 public $line_id_object_src;
131 public $line_id_object_origin;
132
136 public $eatby;
137
141 public $sellby;
142
143
144
145 public $fields = array(
146 'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 10, 'showoncombobox' => 1),
147 'tms' => array('type' => 'timestamp', 'label' => 'DateModification', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 15),
148 'datem' => array('type' => 'datetime', 'label' => 'Datem', 'enabled' => 1, 'visible' => -1, 'position' => 20),
149 'fk_product' => array('type' => 'integer:Product:product/class/product.class.php:1', 'label' => 'Product', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 25),
150 'fk_entrepot' => array('type' => 'integer:Entrepot:product/stock/class/entrepot.class.php', 'label' => 'Warehouse', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 30),
151 'value' => array('type' => 'double', 'label' => 'Value', 'enabled' => 1, 'visible' => -1, 'position' => 35),
152 'price' => array('type' => 'double(24,8)', 'label' => 'Price', 'enabled' => 1, 'visible' => -1, 'position' => 40),
153 'type_mouvement' => array('type' => 'smallint(6)', 'label' => 'Type mouvement', 'enabled' => 1, 'visible' => -1, 'position' => 45),
154 'fk_user_author' => array('type' => 'integer:User:user/class/user.class.php', 'label' => 'Fk user author', 'enabled' => 1, 'visible' => -1, 'position' => 50),
155 'label' => array('type' => 'varchar(255)', 'label' => 'Label', 'enabled' => 1, 'visible' => -1, 'position' => 55),
156 'fk_origin' => array('type' => 'integer', 'label' => 'Fk origin', 'enabled' => 1, 'visible' => -1, 'position' => 60),
157 'origintype' => array('type' => 'varchar(32)', 'label' => 'Origintype', 'enabled' => 1, 'visible' => -1, 'position' => 65),
158 'model_pdf' => array('type' => 'varchar(255)', 'label' => 'Model pdf', 'enabled' => 1, 'visible' => 0, 'position' => 70),
159 '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),
160 'inventorycode' => array('type' => 'varchar(128)', 'label' => 'InventoryCode', 'enabled' => 1, 'visible' => -1, 'position' => 80),
161 'batch' => array('type' => 'varchar(30)', 'label' => 'Batch', 'enabled' => 1, 'visible' => -1, 'position' => 85),
162 'eatby' => array('type' => 'date', 'label' => 'Eatby', 'enabled' => 1, 'visible' => -1, 'position' => 90),
163 'sellby' => array('type' => 'date', 'label' => 'Sellby', 'enabled' => 1, 'visible' => -1, 'position' => 95),
164 'fk_project' => array('type' => 'integer:Project:projet/class/project.class.php:1:(fk_statut:=:1)', 'label' => 'Fk project', 'enabled' => 1, 'visible' => -1, 'position' => 100),
165 );
166
167
168
174 public function __construct($db)
175 {
176 $this->db = $db;
177 }
178
179 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.PublicUnderscore
207 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)
208 {
209 // phpcs:enable
210 global $conf, $langs;
211
212 require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
213 require_once DOL_DOCUMENT_ROOT.'/product/stock/class/productlot.class.php';
214
215 $error = 0;
216 dol_syslog(get_class($this)."::_create start userid=$user->id, fk_product=$fk_product, warehouse_id=$entrepot_id, qty=$qty, type=$type, price=$price, label=$label, inventorycode=$inventorycode, datem=".$datem.", eatby=".$eatby.", sellby=".$sellby.", batch=".$batch.", skip_batch=".json_encode($skip_batch));
217
218 // Call hook at beginning
219 global $action, $hookmanager;
220 $hookmanager->initHooks(array('mouvementstock'));
221
222 if (is_object($hookmanager)) {
223 $parameters = array(
224 'currentcontext' => 'mouvementstock',
225 'user' => &$user,
226 'fk_product' => &$fk_product,
227 'entrepot_id' => &$entrepot_id,
228 'qty' => &$qty,
229 'type' => &$type,
230 'price' => &$price,
231 'label' => &$label,
232 'inventorycode' => &$inventorycode,
233 'datem' => &$datem,
234 'eatby' => &$eatby,
235 'sellby' => &$sellby,
236 'batch' => &$batch,
237 'skip_batch' => &$skip_batch,
238 'id_product_batch' => &$id_product_batch
239 );
240 $reshook = $hookmanager->executeHooks('stockMovementCreate', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
241
242 if ($reshook < 0) {
243 if (!empty($hookmanager->resPrint)) {
244 dol_print_error(null, $hookmanager->resPrint);
245 }
246 return $reshook;
247 } elseif ($reshook > 0) {
248 return $hookmanager->resPrint;
249 }
250 }
251 // end hook at beginning
252
253 // Clean parameters
254 $price = price2num($price, 'MU'); // Clean value for the casse we receive a float zero value, to have it a real zero value.
255 if (empty($price)) {
256 $price = 0;
257 }
258 $now = (!empty($datem) ? $datem : dol_now());
259
260 // Check parameters
261 if (!($fk_product > 0)) {
262 return 0;
263 }
264 if (!($entrepot_id > 0)) {
265 return 0;
266 }
267
268 if (is_numeric($eatby) && $eatby < 0) {
269 dol_syslog(get_class($this)."::_create start ErrorBadValueForParameterEatBy eatby = ".$eatby);
270 $this->errors[] = 'ErrorBadValueForParameterEatBy';
271 return -1;
272 }
273 if (is_numeric($sellby) && $sellby < 0) {
274 dol_syslog(get_class($this)."::_create start ErrorBadValueForParameterSellBy sellby = ".$sellby);
275 $this->errors[] = 'ErrorBadValueForParameterSellBy';
276 return -1;
277 }
278
279 // Set properties of movement
280 $this->product_id = $fk_product;
281 $this->entrepot_id = $entrepot_id; // deprecated
282 $this->warehouse_id = $entrepot_id;
283 $this->qty = $qty;
284 $this->type = $type;
285 $this->price = price2num($price);
286 $this->label = $label;
287 $this->inventorycode = $inventorycode;
288 $this->datem = $now;
289 $this->batch = $batch;
290
291 $mvid = 0;
292
293 $product = new Product($this->db);
294
295 $result = $product->fetch($fk_product);
296 if ($result < 0) {
297 $this->error = $product->error;
298 $this->errors = $product->errors;
299 dol_print_error(null, "Failed to fetch product");
300 return -1;
301 }
302 if ($product->id <= 0) { // Can happen if database is corrupted (a product id exist in stock with product that has been removed)
303 return 0;
304 }
305
306 // Define if we must make the stock change (If product type is a service or if stock is used also for services)
307 // Only record into stock tables will be disabled by this (the rest like writing into lot table or movement of subproucts are done)
308 $movestock = 0;
309 if ($product->type != Product::TYPE_SERVICE || getDolGlobalString('STOCK_SUPPORTS_SERVICES')) {
310 $movestock = 1;
311 }
312
313 $this->db->begin();
314
315 // Set value $product->stock_reel and detail per warehouse into $product->stock_warehouse array
316 if ($movestock) {
317 $product->load_stock('novirtual');
318 }
319
320 // Test if product require batch data. If yes, and there is not or values are not correct, we throw an error.
321 if (isModEnabled('productbatch') && $product->hasbatch() && !$skip_batch) {
322 if (empty($batch)) {
323 $langs->load("errors");
324 $this->errors[] = $langs->transnoentitiesnoconv("ErrorTryToMakeMoveOnProductRequiringBatchData", $product->ref);
325 dol_syslog("Try to make a movement of a product with status_batch on without any batch data", LOG_ERR);
326
327 $this->db->rollback();
328 return -2;
329 }
330
331 // Check table llx_product_lot from batchnumber for same product
332 // If found and eatby/sellby defined into table and provided and differs, return error
333 // If found and eatby/sellby defined into table and not provided, we take value from table
334 // If found and eatby/sellby not defined into table and provided, we update table
335 // If found and eatby/sellby not defined into table and not provided, we do nothing
336 // If not found, we add record
337 $sql = "SELECT pb.rowid, pb.batch, pb.eatby, pb.sellby FROM ".$this->db->prefix()."product_lot as pb";
338 $sql .= " WHERE pb.fk_product = ".((int) $fk_product)." AND pb.batch = '".$this->db->escape($batch)."'";
339
340 dol_syslog(get_class($this)."::_create scan serial for this product to check if eatby and sellby match", LOG_DEBUG);
341
342 $resql = $this->db->query($sql);
343 if ($resql) {
344 $num = $this->db->num_rows($resql);
345 $i = 0;
346 if ($num > 0) {
347 while ($i < $num) {
348 $obj = $this->db->fetch_object($resql);
349 if ($obj->eatby) {
350 if ($eatby) {
351 $tmparray = dol_getdate($eatby, true);
352 $eatbywithouthour = dol_mktime(0, 0, 0, $tmparray['mon'], $tmparray['mday'], $tmparray['year']);
353 if ($this->db->jdate($obj->eatby) != $eatby && $this->db->jdate($obj->eatby) != $eatbywithouthour) { // We test date without hours and with hours for backward compatibility
354 // If found and eatby/sellby defined into table and provided and differs, return error
355 $langs->load("stocks");
356 $this->errors[] = $langs->transnoentitiesnoconv("ThisSerialAlreadyExistWithDifferentDate", $batch, dol_print_date($this->db->jdate($obj->eatby), 'dayhour'), dol_print_date($eatbywithouthour, 'dayhour'));
357 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);
358 $this->db->rollback();
359 return -3;
360 }
361 } else {
362 $eatby = $obj->eatby; // If found and eatby/sellby defined into table and not provided, we take value from table
363 }
364 } else {
365 if ($eatby) { // If found and eatby/sellby not defined into table and provided, we update table
366 $productlot = new Productlot($this->db);
367 $result = $productlot->fetch($obj->rowid);
368 $productlot->eatby = $eatby;
369 $result = $productlot->update($user);
370 if ($result <= 0) {
371 $this->error = $productlot->error;
372 $this->errors = $productlot->errors;
373 $this->db->rollback();
374 return -5;
375 }
376 }
377 }
378 if ($obj->sellby) {
379 if ($sellby) {
380 $tmparray = dol_getdate($sellby, true);
381 $sellbywithouthour = dol_mktime(0, 0, 0, $tmparray['mon'], $tmparray['mday'], $tmparray['year']);
382 if ($this->db->jdate($obj->sellby) != $sellby && $this->db->jdate($obj->sellby) != $sellbywithouthour) { // We test date without hours and with hours for backward compatibility
383 // If found and eatby/sellby defined into table and provided and differs, return error
384 $this->errors[] = $langs->transnoentitiesnoconv("ThisSerialAlreadyExistWithDifferentDate", $batch, dol_print_date($this->db->jdate($obj->sellby)), dol_print_date($sellby));
385 dol_syslog($langs->transnoentities("ThisSerialAlreadyExistWithDifferentDate", $batch, dol_print_date($this->db->jdate($obj->sellby)), dol_print_date($sellby)), LOG_ERR);
386 $this->db->rollback();
387 return -3;
388 }
389 } else {
390 $sellby = $obj->sellby; // If found and eatby/sellby defined into table and not provided, we take value from table
391 }
392 } else {
393 if ($sellby) { // If found and eatby/sellby not defined into table and provided, we update table
394 $productlot = new Productlot($this->db);
395 $result = $productlot->fetch($obj->rowid);
396 $productlot->sellby = $sellby;
397 $result = $productlot->update($user);
398 if ($result <= 0) {
399 $this->error = $productlot->error;
400 $this->errors = $productlot->errors;
401 $this->db->rollback();
402 return -5;
403 }
404 }
405 }
406
407 $i++;
408 }
409 } else { // If not found, we add record
410 $productlot = new Productlot($this->db);
411 $productlot->origin = !empty($this->origin_type) ? $this->origin_type : '';
412 $productlot->origin_id = !empty($this->origin_id) ? $this->origin_id : 0;
413 $productlot->entity = $conf->entity;
414 $productlot->fk_product = $fk_product;
415 $productlot->batch = $batch;
416 // If we are here = first time we manage this batch, so we used dates provided by users to create lot
417 $productlot->eatby = $eatby;
418 $productlot->sellby = $sellby;
419 $result = $productlot->create($user);
420 if ($result <= 0) {
421 $this->error = $productlot->error;
422 $this->errors = $productlot->errors;
423 $this->db->rollback();
424 return -4;
425 }
426 }
427 } else {
428 dol_print_error($this->db);
429 $this->db->rollback();
430 return -1;
431 }
432 }
433
434 // Check if stock is enough when qty is < 0
435 // Note that qty should be > 0 with type 0 or 3, < 0 with type 1 or 2.
436 if ($movestock && $qty < 0 && !getDolGlobalInt('STOCK_ALLOW_NEGATIVE_TRANSFER')) {
437 if (isModEnabled('productbatch') && $product->hasbatch() && !$skip_batch) {
438 $foundforbatch = 0;
439 $qtyisnotenough = 0;
440 if (isset($product->stock_warehouse[$entrepot_id])) {
441 foreach ($product->stock_warehouse[$entrepot_id]->detail_batch as $batchcursor => $prodbatch) {
442 if ((string) $batch != (string) $batchcursor) { // Lot '59' must be different than lot '59c'
443 continue;
444 }
445
446 $foundforbatch = 1;
447 if ($prodbatch->qty < abs($qty)) {
448 $qtyisnotenough = $prodbatch->qty;
449 }
450 break;
451 }
452 }
453 if (!$foundforbatch || $qtyisnotenough) {
454 $langs->load("stocks");
455 include_once DOL_DOCUMENT_ROOT.'/product/stock/class/entrepot.class.php';
456 $tmpwarehouse = new Entrepot($this->db);
457 $tmpwarehouse->fetch($entrepot_id);
458
459 $this->error = $langs->trans('qtyToTranferLotIsNotEnough', $product->ref, $batch, $qtyisnotenough, $tmpwarehouse->ref);
460 $this->errors[] = $langs->trans('qtyToTranferLotIsNotEnough', $product->ref, $batch, $qtyisnotenough, $tmpwarehouse->ref);
461 $this->db->rollback();
462 return -8;
463 }
464 } else {
465 if (isset($product->stock_warehouse[$entrepot_id]) && (empty($product->stock_warehouse[$entrepot_id]->real) || $product->stock_warehouse[$entrepot_id]->real < abs($qty))) {
466 $langs->load("stocks");
467 $this->error = $langs->trans('qtyToTranferIsNotEnough').' : '.$product->ref;
468 $this->errors[] = $langs->trans('qtyToTranferIsNotEnough').' : '.$product->ref;
469 $this->db->rollback();
470 return -8;
471 }
472 }
473 }
474
475 if ($movestock) { // Change stock for current product, change for subproduct is done after
476 // Set $origin_type, origin_id and fk_project
477 $fk_project = $this->fk_project;
478 if (!empty($this->origin_type)) { // This is set by caller for tracking reason
479 $origin_type = $this->origin_type;
480 $origin_id = $this->origin_id;
481 if (empty($fk_project) && $origin_type == 'project') {
482 $fk_project = $origin_id;
483 $origin_type = '';
484 $origin_id = 0;
485 }
486 } else {
487 $fk_project = 0;
488 $origin_type = '';
489 $origin_id = 0;
490 }
491
492 $sql = "INSERT INTO ".$this->db->prefix()."stock_mouvement(";
493 $sql .= " datem, fk_product, batch, eatby, sellby,";
494 $sql .= " fk_entrepot, value, type_mouvement, fk_user_author, label, inventorycode, price, fk_origin, origintype, fk_projet";
495 $sql .= ")";
496 $sql .= " VALUES ('".$this->db->idate($this->datem)."', ".((int) $this->product_id).", ";
497 $sql .= " ".($batch ? "'".$this->db->escape($batch)."'" : "null").", ";
498 $sql .= " ".($eatby ? "'".$this->db->idate($eatby)."'" : "null").", ";
499 $sql .= " ".($sellby ? "'".$this->db->idate($sellby)."'" : "null").", ";
500 $sql .= " ".((int) $this->entrepot_id).", ".((float) $this->qty).", ".((int) $this->type).",";
501 $sql .= " ".((int) $user->id).",";
502 $sql .= " '".$this->db->escape($label)."',";
503 $sql .= " ".($inventorycode ? "'".$this->db->escape($inventorycode)."'" : "null").",";
504 $sql .= " ".((float) price2num($price)).",";
505 $sql .= " ".((int) $origin_id).",";
506 $sql .= " '".$this->db->escape($origin_type)."',";
507 $sql .= " ".((int) $fk_project);
508 $sql .= ")";
509
510 dol_syslog(get_class($this)."::_create insert record into stock_mouvement", LOG_DEBUG);
511 $resql = $this->db->query($sql);
512
513 if ($resql) {
514 $mvid = $this->db->last_insert_id($this->db->prefix()."stock_mouvement");
515 $this->id = $mvid;
516 } else {
517 $this->error = $this->db->lasterror();
518 $this->errors[] = $this->error;
519 $error = -1;
520 }
521
522 // Define current values for qty and pmp
523 $oldqty = $product->stock_reel;
524 $oldpmp = $product->pmp;
525 $oldqtywarehouse = 0;
526
527 // Test if there is already a record for couple (warehouse / product), so later we will make an update or create.
528 $alreadyarecord = 0;
529 if (!$error) {
530 $sql = "SELECT rowid, reel FROM ".$this->db->prefix()."product_stock";
531 $sql .= " WHERE fk_entrepot = ".((int) $entrepot_id)." AND fk_product = ".((int) $fk_product); // This is a unique key
532
533 dol_syslog(get_class($this)."::_create check if a record already exists in product_stock", LOG_DEBUG);
534 $resql = $this->db->query($sql);
535 if ($resql) {
536 $obj = $this->db->fetch_object($resql);
537 if ($obj) {
538 $alreadyarecord = 1;
539 $oldqtywarehouse = $obj->reel;
540 $fk_product_stock = $obj->rowid;
541 }
542 $this->db->free($resql);
543 } else {
544 $this->errors[] = $this->db->lasterror();
545 $error = -2;
546 }
547 }
548
549 // Calculate new AWP (PMP)
550 $newpmp = 0;
551 if (!$error) {
552 if ($type == 0 || $type == 3) {
553 // After a stock increase
554 // Note: PMP is calculated on stock input only (type of movement = 0 or 3). If type == 0 or 3, qty should be > 0.
555 // Note: Price should always be >0 or 0. PMP should be always >0 (calculated on input)
556 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')))) {
557 $oldqtytouse = ($oldqty >= 0 ? $oldqty : 0);
558 // We make a test on oldpmp>0 to avoid to use normal rule on old data with no pmp field defined
559 if ($oldpmp > 0) {
560 $newpmp = price2num((($oldqtytouse * $oldpmp) + ($qty * $price)) / ($oldqtytouse + $qty), 'MU');
561 } else {
562 $newpmp = $price; // For this product, PMP was not yet set. We set it to input price.
563 }
564 //print "oldqtytouse=".$oldqtytouse." oldpmp=".$oldpmp." oldqtywarehousetouse=".$oldqtywarehousetouse." ";
565 //print "qty=".$qty." newpmp=".$newpmp;
566 //exit;
567 } else {
568 $newpmp = $oldpmp;
569 }
570 } else {
571 // ($type == 1 || $type == 2)
572 // -> After a stock decrease, we don't change value of the AWP/PMP of a product.
573 // else
574 // Type of movement unknown
575 $newpmp = $oldpmp;
576 }
577 }
578 // Update stock quantity
579 if (!$error) {
580 if ($alreadyarecord > 0) {
581 $sql = "UPDATE ".$this->db->prefix()."product_stock SET reel = " . ((float) $oldqtywarehouse + (float) $qty);
582 $sql .= " WHERE fk_entrepot = ".((int) $entrepot_id)." AND fk_product = ".((int) $fk_product);
583 } else {
584 $sql = "INSERT INTO ".$this->db->prefix()."product_stock";
585 $sql .= " (reel, fk_entrepot, fk_product) VALUES ";
586 $sql .= " (".((float) $qty).", ".((int) $entrepot_id).", ".((int) $fk_product).")";
587 }
588
589 dol_syslog(get_class($this)."::_create update stock value", LOG_DEBUG);
590 $resql = $this->db->query($sql);
591 if (!$resql) {
592 $this->errors[] = $this->db->lasterror();
593 $error = -3;
594 } elseif (empty($fk_product_stock)) {
595 $fk_product_stock = $this->db->last_insert_id($this->db->prefix()."product_stock");
596 }
597 }
598
599 // Update detail of stock for the lot.
600 if (!$error && isModEnabled('productbatch') && (($product->hasbatch() && !$skip_batch) || $force_update_batch)) {
601 if ($id_product_batch > 0) {
602 $result = $this->createBatch($id_product_batch, $qty);
603 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
604 $param_batch = array('fk_product_stock' => $fk_product_stock, 'batchnumber' => $batch);
605 $result = $this->createBatch($param_batch, $qty);
606 }
607 } else {
608 $param_batch = array('fk_product_stock' => $fk_product_stock, 'batchnumber' => $batch);
609 $result = $this->createBatch($param_batch, $qty);
610 }
611 if ($result < 0) {
612 $error++;
613 }
614 }
615
616 // Update PMP and denormalized value of stock qty at product level
617 if (!$error) {
618 $newpmp = price2num($newpmp, 'MU');
619
620 // $sql = "UPDATE ".$this->db->prefix()."product SET pmp = ".$newpmp.", stock = ".$this->db->ifsql("stock IS NULL", 0, "stock") . " + ".$qty;
621 // $sql.= " WHERE rowid = ".((int) $fk_product);
622 // Update pmp + denormalized fields because we change content of produt_stock. Warning: Do not use "SET p.stock", does not works with pgsql
623 $sql = "UPDATE ".$this->db->prefix()."product as p SET pmp = ".((float) $newpmp).",";
624 $sql .= " stock=(SELECT SUM(ps.reel) FROM ".$this->db->prefix()."product_stock as ps WHERE ps.fk_product = p.rowid)";
625 $sql .= " WHERE rowid = ".((int) $fk_product);
626
627 dol_syslog(get_class($this)."::_create update AWP", LOG_DEBUG);
628 $resql = $this->db->query($sql);
629 if (!$resql) {
630 $this->errors[] = $this->db->lasterror();
631 $error = -4;
632 }
633 }
634
635 if (empty($donotcleanemptylines)) {
636 // 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
637 // having a lot1/qty=X and lot2/qty=-X, so 0 but we must not loose repartition of different lot.
638 $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)";
639 $resql = $this->db->query($sql);
640 // We do not test error, it can fails if there is child in batch details
641 }
642 }
643
644 // Add movement for sub products (recursive call)
645 if (!$error && getDolGlobalString('PRODUIT_SOUSPRODUITS') && !getDolGlobalString('INDEPENDANT_SUBPRODUCT_STOCK') && empty($disablestockchangeforsubproduct)) {
646 $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
647 }
648
649 if ($movestock && !$error) {
650 // Call trigger
651 $result = $this->call_trigger('STOCK_MOVEMENT', $user);
652 if ($result < 0) {
653 $error++;
654 }
655 // End call triggers
656 // Check unicity for serial numbered equipment once all movement were done.
657 if (!$error && isModEnabled('productbatch') && $product->hasbatch() && !$skip_batch) {
658 if ($product->status_batch == 2 && $qty > 0) { // We check only if we increased qty
659 if ($this->getBatchCount($fk_product, $batch) > 1) {
660 $error++;
661 $this->errors[] = $langs->trans("TooManyQtyForSerialNumber", $product->ref, $batch);
662 }
663 }
664 }
665 }
666
667 if (!$error) {
668 $this->db->commit();
669 return $mvid;
670 } else {
671 $this->db->rollback();
672 dol_syslog(get_class($this)."::_create error code=".$error, LOG_ERR);
673 return -6;
674 }
675 }
676
677
678
686 public function fetch($id)
687 {
688 dol_syslog(__METHOD__, LOG_DEBUG);
689
690 $sql = "SELECT";
691 $sql .= " t.rowid,";
692 $sql .= " t.tms,";
693 $sql .= " t.datem,";
694 $sql .= " t.fk_product,";
695 $sql .= " t.fk_entrepot,";
696 $sql .= " t.value,";
697 $sql .= " t.price,";
698 $sql .= " t.type_mouvement,";
699 $sql .= " t.fk_user_author,";
700 $sql .= " t.label,";
701 $sql .= " t.fk_origin as origin_id,";
702 $sql .= " t.origintype as origin_type,";
703 $sql .= " t.inventorycode,";
704 $sql .= " t.batch,";
705 $sql .= " t.eatby,";
706 $sql .= " t.sellby,";
707 $sql .= " t.fk_projet as fk_project";
708 $sql .= " FROM ".$this->db->prefix().$this->table_element." as t";
709 $sql .= " WHERE t.rowid = ".((int) $id);
710
711 $resql = $this->db->query($sql);
712 if ($resql) {
713 $numrows = $this->db->num_rows($resql);
714 if ($numrows) {
715 $obj = $this->db->fetch_object($resql);
716
717 $this->id = $obj->rowid;
718
719 $this->product_id = $obj->fk_product;
720 $this->warehouse_id = $obj->fk_entrepot;
721 $this->qty = $obj->value;
722 $this->type = $obj->type_mouvement;
723
724 $this->tms = $this->db->jdate($obj->tms);
725 $this->datem = $this->db->jdate($obj->datem);
726 $this->price = $obj->price;
727 $this->fk_user_author = $obj->fk_user_author;
728 $this->label = $obj->label;
729 $this->fk_origin = $obj->origin_id; // For backward compatibility
730 $this->origintype = $obj->origin_type; // For backward compatibility
731 $this->origin_id = $obj->origin_id;
732 $this->origin_type = $obj->origin_type;
733 $this->inventorycode = $obj->inventorycode;
734 $this->batch = $obj->batch;
735 $this->eatby = $this->db->jdate($obj->eatby);
736 $this->sellby = $this->db->jdate($obj->sellby);
737 $this->fk_project = $obj->fk_project;
738 }
739
740 // Retrieve all extrafield
741 $this->fetch_optionals();
742
743 // $this->fetch_lines();
744
745 $this->db->free($resql);
746
747 if ($numrows) {
748 return 1;
749 } else {
750 return 0;
751 }
752 } else {
753 $this->errors[] = 'Error '.$this->db->lasterror();
754 dol_syslog(__METHOD__.' '.implode(',', $this->errors), LOG_ERR);
755
756 return -1;
757 }
758 }
759
760
761
762
777 private function _createSubProduct($user, $idProduct, $entrepot_id, $qty, $type, $price = 0, $label = '', $inventorycode = '', $datem = '')
778 {
779 global $langs;
780
781 $error = 0;
782 $pids = array();
783 $pqtys = array();
784
785 $sql = "SELECT fk_product_pere, fk_product_fils, qty";
786 $sql .= " FROM ".$this->db->prefix()."product_association";
787 $sql .= " WHERE fk_product_pere = ".((int) $idProduct);
788 $sql .= " AND incdec = 1";
789
790 dol_syslog(get_class($this)."::_createSubProduct for parent product ".$idProduct, LOG_DEBUG);
791 $resql = $this->db->query($sql);
792 if ($resql) {
793 $i = 0;
794 while ($obj = $this->db->fetch_object($resql)) {
795 $pids[$i] = $obj->fk_product_fils;
796 $pqtys[$i] = $obj->qty;
797 $i++;
798 }
799 $this->db->free($resql);
800 } else {
801 $error = -2;
802 }
803
804 // Create movement for each subproduct
805 foreach ($pids as $key => $value) {
806 if (!$error) {
807 $tmpmove = dol_clone($this, 1);
808
809 $result = $tmpmove->_create($user, $pids[$key], $entrepot_id, ($qty * $pqtys[$key]), $type, 0, $label, $inventorycode, $datem); // This will also call _createSubProduct making this recursive
810 if ($result < 0) {
811 $this->error = $tmpmove->error;
812 $this->errors = array_merge($this->errors, $tmpmove->errors);
813 if ($result == -2) {
814 $this->errors[] = $langs->trans("ErrorNoteAlsoThatSubProductCantBeFollowedByLot");
815 }
816 $error = $result;
817 }
818 unset($tmpmove);
819 }
820 }
821
822 return $error;
823 }
824
825
844 public function livraison($user, $fk_product, $entrepot_id, $qty, $price = 0, $label = '', $datem = '', $eatby = '', $sellby = '', $batch = '', $id_product_batch = 0, $inventorycode = '', $donotcleanemptylines = 0)
845 {
846 global $conf;
847
848 $skip_batch = empty($conf->productbatch->enabled);
849
850 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);
851 }
852
871 public function reception($user, $fk_product, $entrepot_id, $qty, $price = 0, $label = '', $eatby = '', $sellby = '', $batch = '', $datem = '', $id_product_batch = 0, $inventorycode = '', $donotcleanemptylines = 0)
872 {
873 global $conf;
874
875 $skip_batch = empty($conf->productbatch->enabled);
876
877 return $this->_create($user, $fk_product, $entrepot_id, $qty, 3, $price, $label, $inventorycode, $datem, $eatby, $sellby, $batch, $skip_batch, $id_product_batch, 0, $donotcleanemptylines);
878 }
879
887 public function calculateBalanceForProductBefore($productidselected, $datebefore)
888 {
889 $nb = 0;
890
891 $sql = "SELECT SUM(value) as nb from ".$this->db->prefix()."stock_mouvement";
892 $sql .= " WHERE fk_product = ".((int) $productidselected);
893 $sql .= " AND datem < '".$this->db->idate($datebefore)."'";
894
895 dol_syslog(get_class($this).__METHOD__, LOG_DEBUG);
896 $resql = $this->db->query($sql);
897 if ($resql) {
898 $obj = $this->db->fetch_object($resql);
899 if ($obj) {
900 $nb = $obj->nb;
901 }
902 return (empty($nb) ? 0 : $nb);
903 } else {
904 dol_print_error($this->db);
905 return -1;
906 }
907 }
908
918 private function createBatch($dluo, $qty)
919 {
920 global $user, $langs;
921
922 $langs->load('productbatch');
923
924 $pdluo = new Productbatch($this->db);
925
926 $result = 0;
927
928 // Try to find an existing record with same batch number or id
929 if (is_numeric($dluo)) {
930 $result = $pdluo->fetch($dluo);
931 if (empty($pdluo->id)) {
932 // We didn't find the line. May be it was deleted before by a previous move in same transaction.
933 $this->error = $langs->trans('CantMoveNonExistantSerial');
934 $this->errors[] = $this->error;
935 $result = -2;
936 }
937 } elseif (is_array($dluo)) {
938 if (isset($dluo['fk_product_stock'])) {
939 $vfk_product_stock = $dluo['fk_product_stock'];
940 $vbatchnumber = $dluo['batchnumber'];
941
942 $result = $pdluo->find($vfk_product_stock, '', '', $vbatchnumber); // Search on batch number only (eatby and sellby are deprecated here)
943 } else {
944 dol_syslog(get_class($this)."::createBatch array param dluo must contain at least key fk_product_stock", LOG_ERR);
945 $result = -1;
946 }
947 } else {
948 dol_syslog(get_class($this)."::createBatch error invalid param dluo", LOG_ERR);
949 $result = -1;
950 }
951
952 if ($result >= 0) {
953 // No error
954 if ($pdluo->id > 0) { // product_batch record found
955 //print "Avant ".$pdluo->qty." Apres ".($pdluo->qty + $qty)."<br>";
956 $pdluo->qty += $qty;
957 if ($pdluo->qty == 0) {
958 $result = $pdluo->delete($user, 1);
959 } else {
960 $result = $pdluo->update($user, 1);
961 }
962 } else { // product_batch record not found
963 $pdluo->fk_product_stock = $vfk_product_stock;
964 $pdluo->qty = $qty;
965 $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.
966 $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.
967 $pdluo->batch = $vbatchnumber;
968
969 $result = $pdluo->create($user, 1);
970 if ($result < 0) {
971 $this->error = $pdluo->error;
972 $this->errors = $pdluo->errors;
973 }
974 }
975 }
976
977 return $result;
978 }
979
980 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
988 public function get_origin($origin_id, $origin_type)
989 {
990 // phpcs:enable
991 $origin = '';
992
993 switch ($origin_type) {
994 case 'commande':
995 require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
996 $origin = new Commande($this->db);
997 break;
998 case 'shipping':
999 require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
1000 $origin = new Expedition($this->db);
1001 break;
1002 case 'facture':
1003 require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
1004 $origin = new Facture($this->db);
1005 break;
1006 case 'order_supplier':
1007 require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.commande.class.php';
1008 $origin = new CommandeFournisseur($this->db);
1009 break;
1010 case 'invoice_supplier':
1011 require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
1012 $origin = new FactureFournisseur($this->db);
1013 break;
1014 case 'project':
1015 require_once DOL_DOCUMENT_ROOT.'/projet/class/project.class.php';
1016 $origin = new Project($this->db);
1017 break;
1018 case 'mo':
1019 require_once DOL_DOCUMENT_ROOT.'/mrp/class/mo.class.php';
1020 $origin = new Mo($this->db);
1021 break;
1022 case 'user':
1023 require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';
1024 $origin = new User($this->db);
1025 break;
1026 case 'reception':
1027 require_once DOL_DOCUMENT_ROOT.'/reception/class/reception.class.php';
1028 $origin = new Reception($this->db);
1029 break;
1030 case 'inventory':
1031 require_once DOL_DOCUMENT_ROOT.'/product/inventory/class/inventory.class.php';
1032 $origin = new Inventory($this->db);
1033 break;
1034 default:
1035 if ($origin_type) {
1036 // Separate origin_type with "@" : left part is class name, right part is module name
1037 $origin_type_array = explode('@', $origin_type);
1038 $classname = $origin_type_array[0];
1039 $modulename = empty($origin_type_array[1]) ? strtolower($classname) : $origin_type_array[1];
1040
1041 $result = dol_include_once('/'.$modulename.'/class/'.$classname.'.class.php');
1042
1043 if ($result) {
1044 $classname = ucfirst($classname);
1045 $origin = new $classname($this->db);
1046 }
1047 }
1048 break;
1049 }
1050
1051 if (empty($origin) || !is_object($origin)) {
1052 return '';
1053 }
1054
1055 if ($origin->fetch($origin_id) > 0) {
1056 return $origin->getNomUrl(1);
1057 }
1058
1059 return '';
1060 }
1061
1072 public function setOrigin($origin_element, $origin_id, $line_id_object_src = 0, $line_id_object_origin = 0)
1073 {
1074 $this->origin_type = $origin_element;
1075 $this->origin_id = $origin_id;
1076 $this->line_id_object_src = $line_id_object_src;
1077 $this->line_id_object_origin = $line_id_object_origin;
1078 // For backward compatibility
1079 $this->origintype = $origin_element;
1080 $this->fk_origin = $origin_id;
1081 }
1082
1083
1091 public function initAsSpecimen()
1092 {
1093 // Initialize parameters
1094 $this->id = 0;
1095
1096 // There is no specific properties. All data into insert are provided as method parameter.
1097
1098 return 1;
1099 }
1100
1107 public function getTypeMovement($withlabel = 0)
1108 {
1109 global $langs;
1110
1111 $s = '';
1112 switch ($this->type) {
1113 case "0":
1114 $s = '<span class="fa fa-level-down-alt stockmovemententry stockmovementtransfer" title="'.$langs->trans('StockIncreaseAfterCorrectTransfer').'"></span>';
1115 if ($withlabel) {
1116 $s .= $langs->trans('StockIncreaseAfterCorrectTransfer');
1117 }
1118 break;
1119 case "1":
1120 $s = '<span class="fa fa-level-up-alt stockmovementexit stockmovementtransfer" title="'.$langs->trans('StockDecreaseAfterCorrectTransfer').'"></span>';
1121 if ($withlabel) {
1122 $s .= $langs->trans('StockDecreaseAfterCorrectTransfer');
1123 }
1124 break;
1125 case "2":
1126 $s = '<span class="fa fa-long-arrow-alt-up stockmovementexit stockmovement" title="'.$langs->trans('StockDecrease').'"></span>';
1127 if ($withlabel) {
1128 $s .= $langs->trans('StockDecrease');
1129 }
1130 break;
1131 case "3":
1132 $s = '<span class="fa fa-long-arrow-alt-down stockmovemententry stockmovement" title="'.$langs->trans('StockIncrease').'"></span>';
1133 if ($withlabel) {
1134 $s .= $langs->trans('StockIncrease');
1135 }
1136 break;
1137 }
1138
1139 return $s;
1140 }
1141
1153 public function getNomUrl($withpicto = 0, $option = '', $notooltip = 0, $maxlen = 24, $morecss = '')
1154 {
1155 global $langs, $conf, $db;
1156
1157 $result = '';
1158
1159 $label = img_picto('', 'stock', 'class="pictofixedwidth"').'<u>'.$langs->trans("StockMovement").'</u>';
1160 $label .= '<div width="100%">';
1161 $label .= '<b>'.$langs->trans('Ref').':</b> '.$this->id;
1162 $label .= '<br><b>'.$langs->trans('Label').':</b> '.$this->label;
1163 $qtylabel = (($this->qty > 0) ? '<span class="stockmovemententry">+' : '<span class="stockmovementexit">') . $this->qty . '</span>';
1164 $label .= '<br><b>'.$langs->trans('Qty').':</b> ' . $qtylabel;
1165 if ($this->batch) {
1166 $label .= '<br><b>'.$langs->trans('Batch').':</b> '.$this->batch;
1167 }
1168 /* TODO Get also warehouse label in a property instead of id
1169 if ($this->warehouse_id > 0) {
1170 $label .= '<br><b>'.$langs->trans('Warehouse').':</b> '.$this->warehouse_id;
1171 }*/
1172 $label .= '</div>';
1173
1174 // Link to page of warehouse tab
1175 if ($option == 'movements') {
1176 $url = DOL_URL_ROOT.'/product/stock/movement_list.php?search_ref='.$this->id;
1177 } else {
1178 $url = DOL_URL_ROOT.'/product/stock/movement_list.php?id='.$this->warehouse_id.'&msid='.$this->id;
1179 }
1180
1181 $link = '<a href="'.$url.'"'.($notooltip ? '' : ' title="'.dol_escape_htmltag($label, 1).'" class="classfortooltip'.($morecss ? ' '.$morecss : '').'"');
1182 $link .= '>';
1183 $linkend = '</a>';
1184
1185 if ($withpicto) {
1186 $result .= ($link.img_object(($notooltip ? '' : $label), 'stock', ($notooltip ? '' : 'class="classfortooltip"')).$linkend);
1187 if ($withpicto != 2) {
1188 $result .= ' ';
1189 }
1190 }
1191 $result .= $link.$this->id.$linkend;
1192 return $result;
1193 }
1194
1201 public function getLibStatut($mode = 0)
1202 {
1203 return $this->LibStatut($mode);
1204 }
1205
1206 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1213 public function LibStatut($mode = 0)
1214 {
1215 // phpcs:enable
1216 global $langs;
1217
1218 if ($mode == 0 || $mode == 1) {
1219 return $langs->trans('StatusNotApplicable');
1220 } elseif ($mode == 2) {
1221 return img_picto($langs->trans('StatusNotApplicable'), 'statut9').' '.$langs->trans('StatusNotApplicable');
1222 } elseif ($mode == 3) {
1223 return img_picto($langs->trans('StatusNotApplicable'), 'statut9');
1224 } elseif ($mode == 4) {
1225 return img_picto($langs->trans('StatusNotApplicable'), 'statut9').' '.$langs->trans('StatusNotApplicable');
1226 } elseif ($mode == 5) {
1227 return $langs->trans('StatusNotApplicable').' '.img_picto($langs->trans('StatusNotApplicable'), 'statut9');
1228 }
1229
1230 return 'Bad value for mode';
1231 }
1232
1243 public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0)
1244 {
1245 global $conf, $user, $langs;
1246
1247 $langs->load("stocks");
1248 $outputlangs->load("products");
1249
1250 if (!dol_strlen($modele)) {
1251 $modele = 'stdmovement';
1252
1253 if ($this->model_pdf) {
1254 $modele = $this->model_pdf;
1255 } elseif (getDolGlobalString('MOUVEMENT_ADDON_PDF')) {
1256 $modele = getDolGlobalString('MOUVEMENT_ADDON_PDF');
1257 }
1258 }
1259
1260 $modelpath = "core/modules/stock/doc/";
1261
1262 return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref);
1263 }
1264
1272 public function delete(User $user, $notrigger = 0)
1273 {
1274 return $this->deleteCommon($user, $notrigger);
1275 //return $this->deleteCommon($user, $notrigger, 1);
1276 }
1277
1285 private function getBatchCount($fk_product, $batch)
1286 {
1287 $cpt = 0;
1288
1289 $sql = "SELECT sum(pb.qty) as cpt";
1290 $sql .= " FROM ".$this->db->prefix()."product_batch as pb";
1291 $sql .= " INNER JOIN ".$this->db->prefix()."product_stock as ps ON ps.rowid = pb.fk_product_stock";
1292 $sql .= " WHERE ps.fk_product = " . ((int) $fk_product);
1293 $sql .= " AND pb.batch = '" . $this->db->escape($batch) . "'";
1294
1295 $result = $this->db->query($sql);
1296 if ($result) {
1297 if ($this->db->num_rows($result)) {
1298 $obj = $this->db->fetch_object($result);
1299 $cpt = $obj->cpt;
1300 }
1301
1302 $this->db->free($result);
1303 } else {
1304 dol_print_error($this->db);
1305 return -1;
1306 }
1307
1308 return $cpt;
1309 }
1310
1315 public function reverseMouvement()
1316 {
1317 $formattedDate = "REVERTMV" .dol_print_date($this->datem, '%Y%m%d%His');
1318 if ($this->label == 'Annulation movement ID'.$this->id) {
1319 return -1;
1320 }
1321 if ($this->inventorycode == $formattedDate) {
1322 return -1;
1323 }
1324
1325 $sql = "UPDATE ".$this->db->prefix()."stock_mouvement SET";
1326 $sql .= " label = 'Annulation movement ID ".((int) $this->id)."',";
1327 $sql .= "inventorycode = '".($formattedDate)."'";
1328 $sql .= " WHERE rowid = ".((int) $this->id);
1329
1330 $resql = $this->db->query($sql);
1331
1332 if ($resql) {
1333 $this->db->commit();
1334 return 1;
1335 } else {
1336 $this->db->rollback();
1337 return -1;
1338 }
1339 }
1340}
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=0, $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:36
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 optionally 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 equipment 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 movement 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 information (by default a local PHP server timestamp) Rep...
img_picto($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0, $alt='', $morecss='', $marginleftonlyshort=2)
Show picto whatever it's its name (generic function)
price2num($amount, $rounding='', $option=0)
Function that return a number with universal decimal format (decimal separator is '.
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_now($mode='auto')
Return date for now.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
dol_print_date($time, $format='', $tzoutput='auto', $outputlangs=null, $encodetooutput=false)
Output date in a string format according to outputlangs (or langs if not defined).
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 =...
dol_print_error($db=null, $error='', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
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:139