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