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