dolibarr  13.0.2
mouvementstock.class.php
Go to the documentation of this file.
1 <?php
2 /* Copyright (C) 2003-2006 Rodolphe Quiedeville <rodolphe@quiedeville.org>
3  * Copyright (C) 2005-2015 Laurent Destailleur <eldy@users.sourceforge.net>
4  * Copyright (C) 2011 Jean Heimburger <jean@tiaris.info>
5  * Copyright (C) 2014 Cedric GROSS <c.gross@kreiz-it.fr>
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 3 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program. If not, see <https://www.gnu.org/licenses/>.
19  */
20 
32 {
36  public $element = 'stockmouvement';
37 
41  public $table_element = 'stock_mouvement';
42 
43 
47  public $product_id;
48 
52  public $warehouse_id;
53  public $qty;
54 
61  public $type;
62 
63  public $tms = '';
64  public $datem = '';
65  public $price;
66 
70  public $fk_user_author;
71 
75  public $label;
76 
80  public $fk_origin;
81 
82  public $origintype;
83 
84  public $inventorycode;
85  public $batch;
86 
90  public $origin;
91 
92  public $fields = array(
93  'rowid' =>array('type'=>'integer', 'label'=>'TechnicalID', 'enabled'=>1, 'visible'=>-1, 'notnull'=>1, 'position'=>10, 'showoncombobox'=>1),
94  'tms' =>array('type'=>'timestamp', 'label'=>'DateModification', 'enabled'=>1, 'visible'=>-1, 'notnull'=>1, 'position'=>15),
95  'datem' =>array('type'=>'datetime', 'label'=>'Datem', 'enabled'=>1, 'visible'=>-1, 'position'=>20),
96  'fk_product' =>array('type'=>'integer:Product:product/class/product.class.php:1', 'label'=>'Product', 'enabled'=>1, 'visible'=>-1, 'notnull'=>1, 'position'=>25),
97  'fk_entrepot' =>array('type'=>'integer:Entrepot:product/stock/class/entrepot.class.php', 'label'=>'Warehouse', 'enabled'=>1, 'visible'=>-1, 'notnull'=>1, 'position'=>30),
98  'value' =>array('type'=>'double', 'label'=>'Value', 'enabled'=>1, 'visible'=>-1, 'position'=>35),
99  'price' =>array('type'=>'double(24,8)', 'label'=>'Price', 'enabled'=>1, 'visible'=>-1, 'position'=>40),
100  'type_mouvement' =>array('type'=>'smallint(6)', 'label'=>'Type mouvement', 'enabled'=>1, 'visible'=>-1, 'position'=>45),
101  'fk_user_author' =>array('type'=>'integer:User:user/class/user.class.php', 'label'=>'Fk user author', 'enabled'=>1, 'visible'=>-1, 'position'=>50),
102  'label' =>array('type'=>'varchar(255)', 'label'=>'Label', 'enabled'=>1, 'visible'=>-1, 'position'=>55),
103  'fk_origin' =>array('type'=>'integer', 'label'=>'Fk origin', 'enabled'=>1, 'visible'=>-1, 'position'=>60),
104  'origintype' =>array('type'=>'varchar(32)', 'label'=>'Origintype', 'enabled'=>1, 'visible'=>-1, 'position'=>65),
105  'model_pdf' =>array('type'=>'varchar(255)', 'label'=>'Model pdf', 'enabled'=>1, 'visible'=>0, 'position'=>70),
106  'fk_projet' =>array('type'=>'integer:Project:projet/class/project.class.php:1:fk_statut=1', 'label'=>'Project', 'enabled'=>1, 'visible'=>-1, 'notnull'=>1, 'position'=>75),
107  'inventorycode' =>array('type'=>'varchar(128)', 'label'=>'InventoryCode', 'enabled'=>1, 'visible'=>-1, 'position'=>80),
108  'batch' =>array('type'=>'varchar(30)', 'label'=>'Batch', 'enabled'=>1, 'visible'=>-1, 'position'=>85),
109  'eatby' =>array('type'=>'date', 'label'=>'Eatby', 'enabled'=>1, 'visible'=>-1, 'position'=>90),
110  'sellby' =>array('type'=>'date', 'label'=>'Sellby', 'enabled'=>1, 'visible'=>-1, 'position'=>95),
111  'fk_project' =>array('type'=>'integer:Project:projet/class/project.class.php:1:fk_statut=1', 'label'=>'Fk project', 'enabled'=>1, 'visible'=>-1, 'position'=>100),
112  );
113 
114 
115 
121  public function __construct($db)
122  {
123  $this->db = $db;
124  }
125 
126  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.PublicUnderscore
151  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)
152  {
153  // phpcs:disable
154  global $conf, $langs;
155 
156  require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
157  require_once DOL_DOCUMENT_ROOT.'/product/stock/class/productlot.class.php';
158 
159  $error = 0;
160  dol_syslog(get_class($this)."::_create start userid=$user->id, fk_product=$fk_product, warehouse_id=$entrepot_id, qty=$qty, type=$type, price=$price, label=$label, inventorycode=$inventorycode, datem=".$datem.", eatby=".$eatby.", sellby=".$sellby.", batch=".$batch.", skip_batch=".$skip_batch);
161 
162  // Clean parameters
163  $price = price2num($price, 'MU'); // Clean value for the casse we receive a float zero value, to have it a real zero value.
164  if (empty($price)) $price = 0;
165  $now = (!empty($datem) ? $datem : dol_now());
166 
167  // Check parameters
168  if (empty($fk_product)) return 0;
169 
170  if (is_numeric($eatby) && $eatby < 0) {
171  dol_syslog(get_class($this)."::_create start ErrorBadValueForParameterEatBy eatby = ".$eatby);
172  $this->errors[] = 'ErrorBadValueForParameterEatBy';
173  return -1;
174  }
175  if (is_numeric($sellby) && $sellby < 0) {
176  dol_syslog(get_class($this)."::_create start ErrorBadValueForParameterSellBy sellby = ".$sellby);
177  $this->errors[] = 'ErrorBadValueForParameterSellBy';
178  return -1;
179  }
180 
181  // Set properties of movement
182  $this->product_id = $fk_product;
183  $this->entrepot_id = $entrepot_id; // deprecated
184  $this->warehouse_id = $entrepot_id;
185  $this->qty = $qty;
186  $this->type = $type;
187  $this->price = price2num($price);
188  $this->label = $label;
189  $this->inventorycode = $inventorycode;
190  $this->datem = $now;
191  $this->batch = $batch;
192 
193  $mvid = 0;
194 
195  $product = new Product($this->db);
196 
197  $result = $product->fetch($fk_product);
198  if ($result < 0) {
199  $this->error = $product->error;
200  $this->errors = $product->errors;
201  dol_print_error('', "Failed to fetch product");
202  return -1;
203  }
204  if ($product->id <= 0) { // Can happen if database is corrupted
205  return 0;
206  }
207 
208  $this->db->begin();
209 
210  $product->load_stock('novirtual');
211 
212  // Test if product require batch data. If yes, and there is not, we throw an error.
213  if (!empty($conf->productbatch->enabled) && $product->hasbatch() && !$skip_batch)
214  {
215  if (empty($batch))
216  {
217  $langs->load("errors");
218  $this->errors[] = $langs->transnoentitiesnoconv("ErrorTryToMakeMoveOnProductRequiringBatchData", $product->ref);
219  dol_syslog("Try to make a movement of a product with status_batch on without any batch data");
220 
221  $this->db->rollback();
222  return -2;
223  }
224 
225  // Check table llx_product_lot from batchnumber for same product
226  // If found and eatby/sellby defined into table and provided and differs, return error
227  // If found and eatby/sellby defined into table and not provided, we take value from table
228  // If found and eatby/sellby not defined into table and provided, we update table
229  // If found and eatby/sellby not defined into table and not provided, we do nothing
230  // If not found, we add record
231  $sql = "SELECT pb.rowid, pb.batch, pb.eatby, pb.sellby FROM ".MAIN_DB_PREFIX."product_lot as pb";
232  $sql .= " WHERE pb.fk_product = ".$fk_product." AND pb.batch = '".$this->db->escape($batch)."'";
233  dol_syslog(get_class($this)."::_create scan serial for this product to check if eatby and sellby match", LOG_DEBUG);
234  $resql = $this->db->query($sql);
235  if ($resql)
236  {
237  $num = $this->db->num_rows($resql);
238  $i = 0;
239  if ($num > 0)
240  {
241  while ($i < $num)
242  {
243  $obj = $this->db->fetch_object($resql);
244  if ($obj->eatby)
245  {
246  if ($eatby)
247  {
248  $tmparray = dol_getdate($eatby, true);
249  $eatbywithouthour = dol_mktime(0, 0, 0, $tmparray['mon'], $tmparray['mday'], $tmparray['year']);
250  if ($this->db->jdate($obj->eatby) != $eatby && $this->db->jdate($obj->eatby) != $eatbywithouthour) // We test date without hours and with hours for backward compatibility
251  {
252  // If found and eatby/sellby defined into table and provided and differs, return error
253  $langs->load("stocks");
254  $this->errors[] = $langs->transnoentitiesnoconv("ThisSerialAlreadyExistWithDifferentDate", $batch, dol_print_date($this->db->jdate($obj->eatby), 'dayhour'), dol_print_date($eatbywithouthour, 'dayhour'));
255  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);
256  $this->db->rollback();
257  return -3;
258  }
259  } else {
260  $eatby = $obj->eatby; // If found and eatby/sellby defined into table and not provided, we take value from table
261  }
262  } else {
263  if ($eatby) // If found and eatby/sellby not defined into table and provided, we update table
264  {
265  $productlot = new Productlot($this->db);
266  $result = $productlot->fetch($obj->rowid);
267  $productlot->eatby = $eatby;
268  $result = $productlot->update($user);
269  if ($result <= 0)
270  {
271  $this->error = $productlot->error;
272  $this->errors = $productlot->errors;
273  $this->db->rollback();
274  return -5;
275  }
276  }
277  }
278  if ($obj->sellby)
279  {
280  if ($sellby)
281  {
282  $tmparray = dol_getdate($sellby, true);
283  $sellbywithouthour = dol_mktime(0, 0, 0, $tmparray['mon'], $tmparray['mday'], $tmparray['year']);
284  if ($this->db->jdate($obj->sellby) != $sellby && $this->db->jdate($obj->sellby) != $sellbywithouthour) // We test date without hours and with hours for backward compatibility
285  {
286  // If found and eatby/sellby defined into table and provided and differs, return error
287  $this->errors[] = $langs->transnoentitiesnoconv("ThisSerialAlreadyExistWithDifferentDate", $batch, dol_print_date($this->db->jdate($obj->sellby)), dol_print_date($sellby));
288  dol_syslog($langs->transnoentities("ThisSerialAlreadyExistWithDifferentDate", $batch, dol_print_date($this->db->jdate($obj->sellby)), dol_print_date($sellby)), LOG_ERR);
289  $this->db->rollback();
290  return -3;
291  }
292  } else {
293  $sellby = $obj->sellby; // If found and eatby/sellby defined into table and not provided, we take value from table
294  }
295  }
296  else
297  {
298  if ($sellby) // If found and eatby/sellby not defined into table and provided, we update table
299  {
300  $productlot = new Productlot($this->db);
301  $result = $productlot->fetch($obj->rowid);
302  $productlot->sellby = $sellby;
303  $result = $productlot->update($user);
304  if ($result <= 0)
305  {
306  $this->error = $productlot->error;
307  $this->errors = $productlot->errors;
308  $this->db->rollback();
309  return -5;
310  }
311  }
312  }
313 
314  $i++;
315  }
316  }
317  else // If not found, we add record
318  {
319  $productlot = new Productlot($this->db);
320  $productlot->entity = $conf->entity;
321  $productlot->fk_product = $fk_product;
322  $productlot->batch = $batch;
323  // If we are here = first time we manage this batch, so we used dates provided by users to create lot
324  $productlot->eatby = $eatby;
325  $productlot->sellby = $sellby;
326  $result = $productlot->create($user);
327  if ($result <= 0)
328  {
329  $this->error = $productlot->error;
330  $this->errors = $productlot->errors;
331  $this->db->rollback();
332  return -4;
333  }
334  }
335  }
336  else
337  {
338  dol_print_error($this->db);
339  $this->db->rollback();
340  return -1;
341  }
342  }
343 
344  // Define if we must make the stock change (If product type is a service or if stock is used also for services)
345  $movestock = 0;
346  if ($product->type != Product::TYPE_SERVICE || !empty($conf->global->STOCK_SUPPORTS_SERVICES)) $movestock = 1;
347 
348  // Check if stock is enough when qty is < 0
349  // Note that qty should be > 0 with type 0 or 3, < 0 with type 1 or 2.
350  if ($movestock && $qty < 0 && empty($conf->global->STOCK_ALLOW_NEGATIVE_TRANSFER))
351  {
352  if (!empty($conf->productbatch->enabled) && $product->hasbatch() && !$skip_batch)
353  {
354  $foundforbatch = 0;
355  $qtyisnotenough = 0;
356  foreach ($product->stock_warehouse[$entrepot_id]->detail_batch as $batchcursor => $prodbatch)
357  {
358  if ($batch != $batchcursor) continue;
359  $foundforbatch = 1;
360  if ($prodbatch->qty < abs($qty)) $qtyisnotenough = $prodbatch->qty;
361  break;
362  }
363  if (!$foundforbatch || $qtyisnotenough)
364  {
365  $langs->load("stocks");
366  include_once DOL_DOCUMENT_ROOT.'/product/stock/class/entrepot.class.php';
367  $tmpwarehouse = new Entrepot($this->db);
368  $tmpwarehouse->fetch($entrepot_id);
369 
370  $this->error = $langs->trans('qtyToTranferLotIsNotEnough', $product->ref, $batch, $qtyisnotenough, $tmpwarehouse->ref);
371  $this->errors[] = $langs->trans('qtyToTranferLotIsNotEnough', $product->ref, $batch, $qtyisnotenough, $tmpwarehouse->ref);
372  $this->db->rollback();
373  return -8;
374  }
375  }
376  else
377  {
378  if (empty($product->stock_warehouse[$entrepot_id]->real) || $product->stock_warehouse[$entrepot_id]->real < abs($qty))
379  {
380  $langs->load("stocks");
381  $this->error = $langs->trans('qtyToTranferIsNotEnough').' : '.$product->ref;
382  $this->errors[] = $langs->trans('qtyToTranferIsNotEnough').' : '.$product->ref;
383  $this->db->rollback();
384  return -8;
385  }
386  }
387  }
388 
389  if ($movestock && $entrepot_id > 0) // Change stock for current product, change for subproduct is done after
390  {
391  // Set $origintype, fk_origin, fk_project
392  $fk_project = 0;
393  if (!empty($this->origin)) { // This is set by caller for tracking reason
394  $origintype = empty($this->origin->origin_type) ? $this->origin->element : $this->origin->origin_type;
395  $fk_origin = $this->origin->id;
396  if ($origintype == 'project') {
397  $fk_project = $fk_origin;
398  } else {
399  $res = $this->origin->fetch($fk_origin);
400  if ($res > 0)
401  {
402  if (!empty($this->origin->fk_project))
403  {
404  $fk_project = $this->origin->fk_project;
405  }
406  }
407  }
408  } else {
409  $origintype = '';
410  $fk_origin = 0;
411  $fk_project = 0;
412  }
413 
414  $sql = "INSERT INTO ".MAIN_DB_PREFIX."stock_mouvement(";
415  $sql .= " datem, fk_product, batch, eatby, sellby,";
416  $sql .= " fk_entrepot, value, type_mouvement, fk_user_author, label, inventorycode, price, fk_origin, origintype, fk_projet";
417  $sql .= ")";
418  $sql .= " VALUES ('".$this->db->idate($now)."', ".$this->product_id.", ";
419  $sql .= " ".($batch ? "'".$this->db->escape($batch)."'" : "null").", ";
420  $sql .= " ".($eatby ? "'".$this->db->idate($eatby)."'" : "null").", ";
421  $sql .= " ".($sellby ? "'".$this->db->idate($sellby)."'" : "null").", ";
422  $sql .= " ".$this->entrepot_id.", ".$this->qty.", ".((int) $this->type).",";
423  $sql .= " ".$user->id.",";
424  $sql .= " '".$this->db->escape($label)."',";
425  $sql .= " ".($inventorycode ? "'".$this->db->escape($inventorycode)."'" : "null").",";
426  $sql .= " ".price2num($price).",";
427  $sql .= " ".$fk_origin.",";
428  $sql .= " '".$this->db->escape($origintype)."',";
429  $sql .= " ".$fk_project;
430  $sql .= ")";
431 
432  dol_syslog(get_class($this)."::_create insert record into stock_mouvement", LOG_DEBUG);
433  $resql = $this->db->query($sql);
434 
435  if ($resql)
436  {
437  $mvid = $this->db->last_insert_id(MAIN_DB_PREFIX."stock_mouvement");
438  $this->id = $mvid;
439  }
440  else
441  {
442  $this->error = $this->db->lasterror();
443  $this->errors[] = $this->error;
444  $error = -1;
445  }
446 
447  // Define current values for qty and pmp
448  $oldqty = $product->stock_reel;
449  $oldpmp = $product->pmp;
450  $oldqtywarehouse = 0;
451 
452  // Test if there is already a record for couple (warehouse / product), so later we will make an update or create.
453  $alreadyarecord = 0;
454  if (!$error)
455  {
456  $sql = "SELECT rowid, reel FROM ".MAIN_DB_PREFIX."product_stock";
457  $sql .= " WHERE fk_entrepot = ".$entrepot_id." AND fk_product = ".$fk_product; // This is a unique key
458 
459  dol_syslog(get_class($this)."::_create check if a record already exists in product_stock", LOG_DEBUG);
460  $resql = $this->db->query($sql);
461  if ($resql)
462  {
463  $obj = $this->db->fetch_object($resql);
464  if ($obj)
465  {
466  $alreadyarecord = 1;
467  $oldqtywarehouse = $obj->reel;
468  $fk_product_stock = $obj->rowid;
469  }
470  $this->db->free($resql);
471  } else {
472  $this->errors[] = $this->db->lasterror();
473  $error = -2;
474  }
475  }
476 
477  // Calculate new AWP (PMP)
478  $newpmp = 0;
479  if (!$error)
480  {
481  if ($type == 0 || $type == 3)
482  {
483  // After a stock increase
484  // Note: PMP is calculated on stock input only (type of movement = 0 or 3). If type == 0 or 3, qty should be > 0.
485  // Note: Price should always be >0 or 0. PMP should be always >0 (calculated on input)
486  if ($price > 0 || (!empty($conf->global->STOCK_UPDATE_AWP_EVEN_WHEN_ENTRY_PRICE_IS_NULL) && $price == 0)) {
487  $oldqtytouse = ($oldqty >= 0 ? $oldqty : 0);
488  // We make a test on oldpmp>0 to avoid to use normal rule on old data with no pmp field defined
489  if ($oldpmp > 0) {
490  $newpmp = price2num((($oldqtytouse * $oldpmp) + ($qty * $price)) / ($oldqtytouse + $qty), 'MU');
491  } else {
492  $newpmp = $price; // For this product, PMP was not yet set. We set it to input price.
493  }
494  //print "oldqtytouse=".$oldqtytouse." oldpmp=".$oldpmp." oldqtywarehousetouse=".$oldqtywarehousetouse." ";
495  //print "qty=".$qty." newpmp=".$newpmp;
496  //exit;
497  } else {
498  $newpmp = $oldpmp;
499  }
500  } elseif ($type == 1 || $type == 2) {
501  // After a stock decrease, we don't change value of the AWP/PMP of a product.
502  $newpmp = $oldpmp;
503  } else {
504  // Type of movement unknown
505  $newpmp = $oldpmp;
506  }
507  }
508  // Update stock quantity
509  if (!$error)
510  {
511  if ($alreadyarecord > 0)
512  {
513  $sql = "UPDATE ".MAIN_DB_PREFIX."product_stock SET reel = reel + ".$qty;
514  $sql .= " WHERE fk_entrepot = ".$entrepot_id." AND fk_product = ".$fk_product;
515  } else {
516  $sql = "INSERT INTO ".MAIN_DB_PREFIX."product_stock";
517  $sql .= " (reel, fk_entrepot, fk_product) VALUES ";
518  $sql .= " (".$qty.", ".$entrepot_id.", ".$fk_product.")";
519  }
520 
521  dol_syslog(get_class($this)."::_create update stock value", LOG_DEBUG);
522  $resql = $this->db->query($sql);
523  if (!$resql)
524  {
525  $this->errors[] = $this->db->lasterror();
526  $error = -3;
527  }
528  elseif (empty($fk_product_stock))
529  {
530  $fk_product_stock = $this->db->last_insert_id(MAIN_DB_PREFIX."product_stock");
531  }
532  }
533 
534  // Update detail stock for batch product
535  if (!$error && !empty($conf->productbatch->enabled) && $product->hasbatch() && !$skip_batch)
536  {
537  if ($id_product_batch > 0)
538  {
539  $result = $this->createBatch($id_product_batch, $qty);
540  } else {
541  $param_batch = array('fk_product_stock' =>$fk_product_stock, 'batchnumber'=>$batch);
542  $result = $this->createBatch($param_batch, $qty);
543  }
544  if ($result < 0) $error++;
545  }
546 
547  // Update PMP and denormalized value of stock qty at product level
548  if (!$error)
549  {
550  $newpmp = price2num($newpmp, 'MU');
551 
552  // $sql = "UPDATE ".MAIN_DB_PREFIX."product SET pmp = ".$newpmp.", stock = ".$this->db->ifsql("stock IS NULL", 0, "stock") . " + ".$qty;
553  // $sql.= " WHERE rowid = ".$fk_product;
554  // Update pmp + denormalized fields because we change content of produt_stock. Warning: Do not use "SET p.stock", does not works with pgsql
555  $sql = "UPDATE ".MAIN_DB_PREFIX."product as p SET pmp = ".$newpmp.",";
556  $sql .= " stock=(SELECT SUM(ps.reel) FROM ".MAIN_DB_PREFIX."product_stock as ps WHERE ps.fk_product = p.rowid)";
557  $sql .= " WHERE rowid = ".$fk_product;
558 
559  dol_syslog(get_class($this)."::_create update AWP", LOG_DEBUG);
560  $resql = $this->db->query($sql);
561  if (!$resql)
562  {
563  $this->errors[] = $this->db->lasterror();
564  $error = -4;
565  }
566  }
567 
568  // 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
569  // having a lot1/qty=X and lot2/qty=-X, so 0 but we must not loose repartition of different lot.
570  $sql = "DELETE FROM ".MAIN_DB_PREFIX."product_stock WHERE reel = 0 AND rowid NOT IN (SELECT fk_product_stock FROM ".MAIN_DB_PREFIX."product_batch as pb)";
571  $resql = $this->db->query($sql);
572  // We do not test error, it can fails if there is child in batch details
573  }
574 
575  // Add movement for sub products (recursive call)
576  if (!$error && !empty($conf->global->PRODUIT_SOUSPRODUITS) && empty($conf->global->INDEPENDANT_SUBPRODUCT_STOCK) && empty($disablestockchangeforsubproduct))
577  {
578  $error = $this->_createSubProduct($user, $fk_product, $entrepot_id, $qty, $type, 0, $label, $inventorycode); // we use 0 as price, because AWP must not change for subproduct
579  }
580 
581  if ($movestock && !$error)
582  {
583  // Call trigger
584  $result = $this->call_trigger('STOCK_MOVEMENT', $user);
585  if ($result < 0) $error++;
586  // End call triggers
587  }
588 
589  if (!$error)
590  {
591  $this->db->commit();
592  return $mvid;
593  }
594  else
595  {
596  $this->db->rollback();
597  dol_syslog(get_class($this)."::_create error code=".$error, LOG_ERR);
598  return -6;
599  }
600  }
601 
602 
603 
611  public function fetch($id)
612  {
613  dol_syslog(__METHOD__, LOG_DEBUG);
614 
615  $sql = 'SELECT';
616  $sql .= ' t.rowid,';
617  $sql .= " t.tms,";
618  $sql .= " t.datem,";
619  $sql .= " t.fk_product,";
620  $sql .= " t.fk_entrepot,";
621  $sql .= " t.value,";
622  $sql .= " t.price,";
623  $sql .= " t.type_mouvement,";
624  $sql .= " t.fk_user_author,";
625  $sql .= " t.label,";
626  $sql .= " t.fk_origin,";
627  $sql .= " t.origintype,";
628  $sql .= " t.inventorycode,";
629  $sql .= " t.batch,";
630  $sql .= " t.eatby,";
631  $sql .= " t.sellby,";
632  $sql .= " t.fk_projet as fk_project";
633  $sql .= ' FROM '.MAIN_DB_PREFIX.$this->table_element.' as t';
634  $sql .= ' WHERE 1 = 1';
635  //if (null !== $ref) {
636  //$sql .= ' AND t.ref = ' . '\'' . $ref . '\'';
637  //} else {
638  $sql .= ' AND t.rowid = '.$id;
639  //}
640 
641  $resql = $this->db->query($sql);
642  if ($resql) {
643  $numrows = $this->db->num_rows($resql);
644  if ($numrows) {
645  $obj = $this->db->fetch_object($resql);
646 
647  $this->id = $obj->rowid;
648 
649  $this->product_id = $obj->fk_product;
650  $this->warehouse_id = $obj->fk_entrepot;
651  $this->qty = $obj->value;
652  $this->type = $obj->type_mouvement;
653 
654  $this->tms = $this->db->jdate($obj->tms);
655  $this->datem = $this->db->jdate($obj->datem);
656  $this->price = $obj->price;
657  $this->fk_user_author = $obj->fk_user_author;
658  $this->label = $obj->label;
659  $this->fk_origin = $obj->fk_origin;
660  $this->origintype = $obj->origintype;
661  $this->inventorycode = $obj->inventorycode;
662  $this->batch = $obj->batch;
663  $this->eatby = $this->db->jdate($obj->eatby);
664  $this->sellby = $this->db->jdate($obj->sellby);
665  $this->fk_project = $obj->fk_project;
666  }
667 
668  // Retrieve all extrafield
669  // fetch optionals attributes and labels
670  $this->fetch_optionals();
671 
672  // $this->fetch_lines();
673 
674  $this->db->free($resql);
675 
676  if ($numrows) {
677  return 1;
678  } else {
679  return 0;
680  }
681  } else {
682  $this->errors[] = 'Error '.$this->db->lasterror();
683  dol_syslog(__METHOD__.' '.implode(',', $this->errors), LOG_ERR);
684 
685  return -1;
686  }
687  }
688 
689 
690 
691 
705  private function _createSubProduct($user, $idProduct, $entrepot_id, $qty, $type, $price = 0, $label = '', $inventorycode = '')
706  {
707  global $langs;
708 
709  $error = 0;
710  $pids = array();
711  $pqtys = array();
712 
713  $sql = "SELECT fk_product_pere, fk_product_fils, qty";
714  $sql .= " FROM ".MAIN_DB_PREFIX."product_association";
715  $sql .= " WHERE fk_product_pere = ".$idProduct;
716  $sql .= " AND incdec = 1";
717 
718  dol_syslog(get_class($this)."::_createSubProduct for parent product ".$idProduct, LOG_DEBUG);
719  $resql = $this->db->query($sql);
720  if ($resql)
721  {
722  $i = 0;
723  while ($obj = $this->db->fetch_object($resql))
724  {
725  $pids[$i] = $obj->fk_product_fils;
726  $pqtys[$i] = $obj->qty;
727  $i++;
728  }
729  $this->db->free($resql);
730  }
731  else
732  {
733  $error = -2;
734  }
735 
736  // Create movement for each subproduct
737  foreach ($pids as $key => $value)
738  {
739  if (!$error)
740  {
741  $tmpmove = dol_clone($this, 1);
742  $result = $tmpmove->_create($user, $pids[$key], $entrepot_id, ($qty * $pqtys[$key]), $type, 0, $label, $inventorycode); // This will also call _createSubProduct making this recursive
743  if ($result < 0)
744  {
745  $this->error = $tmpmove->error;
746  $this->errors = array_merge($this->errors, $tmpmove->errors);
747  if ($result == -2)
748  {
749  $this->errors[] = $langs->trans("ErrorNoteAlsoThatSubProductCantBeFollowedByLot");
750  }
751  $error = $result;
752  }
753  unset($tmpmove);
754  }
755  }
756 
757  return $error;
758  }
759 
760 
778  public function livraison($user, $fk_product, $entrepot_id, $qty, $price = 0, $label = '', $datem = '', $eatby = '', $sellby = '', $batch = '', $id_product_batch = 0, $inventorycode = '')
779  {
780  global $conf;
781 
782  $skip_batch = empty($conf->productbatch->enabled);
783 
784  return $this->_create($user, $fk_product, $entrepot_id, (0 - $qty), 2, $price, $label, $inventorycode, $datem, $eatby, $sellby, $batch, $skip_batch, $id_product_batch);
785  }
786 
804  public function reception($user, $fk_product, $entrepot_id, $qty, $price = 0, $label = '', $eatby = '', $sellby = '', $batch = '', $datem = '', $id_product_batch = 0, $inventorycode = '')
805  {
806  global $conf;
807 
808  $skip_batch = empty($conf->productbatch->enabled);
809 
810  return $this->_create($user, $fk_product, $entrepot_id, $qty, 3, $price, $label, $inventorycode, $datem, $eatby, $sellby, $batch, $skip_batch, $id_product_batch);
811  }
812 
813 
821  /*
822  public function nbOfSubProducts($id)
823  {
824  $nbSP=0;
825 
826  $resql = "SELECT count(*) as nb FROM ".MAIN_DB_PREFIX."product_association";
827  $resql.= " WHERE fk_product_pere = ".$id;
828  if ($this->db->query($resql))
829  {
830  $obj=$this->db->fetch_object($resql);
831  $nbSP=$obj->nb;
832  }
833  return $nbSP;
834  }*/
835 
843  public function calculateBalanceForProductBefore($productidselected, $datebefore)
844  {
845  $nb = 0;
846 
847  $sql = 'SELECT SUM(value) as nb from '.MAIN_DB_PREFIX.'stock_mouvement';
848  $sql .= ' WHERE fk_product = '.$productidselected;
849  $sql .= " AND datem < '".$this->db->idate($datebefore)."'";
850 
851  dol_syslog(get_class($this).__METHOD__.'', LOG_DEBUG);
852  $resql = $this->db->query($sql);
853  if ($resql)
854  {
855  $obj = $this->db->fetch_object($resql);
856  if ($obj) $nb = $obj->nb;
857  return (empty($nb) ? 0 : $nb);
858  } else {
859  dol_print_error($this->db);
860  return -1;
861  }
862  }
863 
873  private function createBatch($dluo, $qty)
874  {
875  global $user;
876 
877  $pdluo = new Productbatch($this->db);
878 
879  $result = 0;
880 
881  // Try to find an existing record with same batch number or id
882  if (is_numeric($dluo))
883  {
884  $result = $pdluo->fetch($dluo);
885  if (empty($pdluo->id))
886  {
887  // We didn't find the line. May be it was deleted before by a previous move in same transaction.
888  $this->error = 'Error. You ask a move on a record for a serial that does not exists anymore. May be you take the same serial on same warehouse several times in same shipment or it was used by another shipment. Remove this shipment and prepare another one.';
889  $this->errors[] = $this->error;
890  $result = -2;
891  }
892  } elseif (is_array($dluo)) {
893  if (isset($dluo['fk_product_stock']))
894  {
895  $vfk_product_stock = $dluo['fk_product_stock'];
896  $vbatchnumber = $dluo['batchnumber'];
897 
898  $result = $pdluo->find($vfk_product_stock, '', '', $vbatchnumber); // Search on batch number only (eatby and sellby are deprecated here)
899  } else {
900  dol_syslog(get_class($this)."::createBatch array param dluo must contain at least key fk_product_stock", LOG_ERR);
901  $result = -1;
902  }
903  } else {
904  dol_syslog(get_class($this)."::createBatch error invalid param dluo", LOG_ERR);
905  $result = -1;
906  }
907 
908  if ($result >= 0)
909  {
910  // No error
911  if ($pdluo->id > 0) { // product_batch record found
912  //print "Avant ".$pdluo->qty." Apres ".($pdluo->qty + $qty)."<br>";
913  $pdluo->qty += $qty;
914  if ($pdluo->qty == 0) {
915  $result = $pdluo->delete($user, 1);
916  } else {
917  $result = $pdluo->update($user, 1);
918  }
919  } else { // product_batch record not found
920  $pdluo->fk_product_stock = $vfk_product_stock;
921  $pdluo->qty = $qty;
922  $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.
923  $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.
924  $pdluo->batch = $vbatchnumber;
925 
926  $result = $pdluo->create($user, 1);
927  if ($result < 0)
928  {
929  $this->error = $pdluo->error;
930  $this->errors = $pdluo->errors;
931  }
932  }
933  }
934 
935  return $result;
936  }
937 
938  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
946  public function get_origin($fk_origin, $origintype)
947  {
948  // phpcs:enable
949  $origin = '';
950 
951  switch ($origintype) {
952  case 'commande':
953  require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
954  $origin = new Commande($this->db);
955  break;
956  case 'shipping':
957  require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
958  $origin = new Expedition($this->db);
959  break;
960  case 'facture':
961  require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
962  $origin = new Facture($this->db);
963  break;
964  case 'order_supplier':
965  require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.commande.class.php';
966  $origin = new CommandeFournisseur($this->db);
967  break;
968  case 'invoice_supplier':
969  require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
970  $origin = new FactureFournisseur($this->db);
971  break;
972  case 'project':
973  require_once DOL_DOCUMENT_ROOT.'/projet/class/project.class.php';
974  $origin = new Project($this->db);
975  break;
976  case 'mo':
977  require_once DOL_DOCUMENT_ROOT.'/mrp/class/mo.class.php';
978  $origin = new Mo($this->db);
979  break;
980  case 'user':
981  require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';
982  $origin = new User($this->db);
983  break;
984  case 'reception':
985  require_once DOL_DOCUMENT_ROOT.'/reception/class/reception.class.php';
986  $origin = new Reception($this->db);
987  break;
988 
989  default:
990  if ($origintype)
991  {
992  // Separate originetype with "@" : left part is class name, right part is module name
993  $origintype_array = explode('@', $origintype);
994  $classname = ucfirst($origintype_array[0]);
995  $modulename = empty($origintype_array[1]) ? $classname : $origintype_array[1];
996  $result = dol_include_once('/'.$modulename.'/class/'.strtolower($classname).'.class.php');
997  if ($result)
998  {
999  $classname = ucfirst($classname);
1000  $origin = new $classname($this->db);
1001  }
1002  }
1003  break;
1004  }
1005 
1006  if (empty($origin) || !is_object($origin)) return '';
1007 
1008  if ($origin->fetch($fk_origin) > 0) {
1009  return $origin->getNomUrl(1);
1010  }
1011 
1012  return '';
1013  }
1014 
1023  public function setOrigin($origin_element, $origin_id)
1024  {
1025  if (!empty($origin_element) && $origin_id > 0)
1026  {
1027  $origin = '';
1028  if ($origin_element == 'project')
1029  {
1030  if (!class_exists('Project')) require_once DOL_DOCUMENT_ROOT.'/projet/class/project.class.php';
1031  $origin = new Project($this->db);
1032  }
1033 
1034  if (!empty($origin))
1035  {
1036  $this->origin = $origin;
1037  $this->origin->id = $origin_id;
1038  }
1039  }
1040  }
1041 
1042 
1050  public function initAsSpecimen()
1051  {
1052  global $user, $langs, $conf, $mysoc;
1053 
1054  // Initialize parameters
1055  $this->id = 0;
1056 
1057  // There is no specific properties. All data into insert are provided as method parameter.
1058  }
1059 
1071  public function getNomUrl($withpicto = 0, $option = '', $notooltip = 0, $maxlen = 24, $morecss = '')
1072  {
1073  global $langs, $conf, $db;
1074 
1075  $result = '';
1076  $companylink = '';
1077 
1078  $label = '<u>'.$langs->trans("Movement").' '.$this->id.'</u>';
1079  $label .= '<div width="100%">';
1080  $label .= '<b>'.$langs->trans('Label').':</b> '.$this->label;
1081  $label .= '<br><b>'.$langs->trans('Qty').':</b> '.$this->qty;
1082  $label .= '</div>';
1083 
1084  $link = '<a href="'.DOL_URL_ROOT.'/product/stock/movement_list.php?id='.$this->warehouse_id.'&msid='.$this->id.'"';
1085  $link .= ($notooltip ? '' : ' title="'.dol_escape_htmltag($label, 1).'" class="classfortooltip'.($morecss ? ' '.$morecss : '').'"');
1086  $link .= '>';
1087  $linkend = '</a>';
1088 
1089  if ($withpicto)
1090  {
1091  $result .= ($link.img_object(($notooltip ? '' : $label), 'stock', ($notooltip ? '' : 'class="classfortooltip"')).$linkend);
1092  if ($withpicto != 2) $result .= ' ';
1093  }
1094  $result .= $link.$this->id.$linkend;
1095  return $result;
1096  }
1097 
1104  public function getLibStatut($mode = 0)
1105  {
1106  return $this->LibStatut($mode);
1107  }
1108 
1109  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1116  public function LibStatut($mode = 0)
1117  {
1118  // phpcs:enable
1119  global $langs;
1120 
1121  if ($mode == 0 || $mode == 1) {
1122  return $langs->trans('StatusNotApplicable');
1123  } elseif ($mode == 2) {
1124  return img_picto($langs->trans('StatusNotApplicable'), 'statut9').' '.$langs->trans('StatusNotApplicable');
1125  } elseif ($mode == 3) {
1126  return img_picto($langs->trans('StatusNotApplicable'), 'statut9');
1127  } elseif ($mode == 4) {
1128  return img_picto($langs->trans('StatusNotApplicable'), 'statut9').' '.$langs->trans('StatusNotApplicable');
1129  } elseif ($mode == 5) {
1130  return $langs->trans('StatusNotApplicable').' '.img_picto($langs->trans('StatusNotApplicable'), 'statut9');
1131  }
1132  }
1133 
1144  public function generateDocument($modele, $outputlangs = '', $hidedetails = 0, $hidedesc = 0, $hideref = 0)
1145  {
1146  global $conf, $user, $langs;
1147 
1148  $langs->load("stocks");
1149  $outputlangs->load("products");
1150 
1151  if (!dol_strlen($modele)) {
1152  $modele = 'stdmovement';
1153 
1154  if ($this->model_pdf) {
1155  $modele = $this->model_pdf;
1156  } elseif (!empty($conf->global->MOUVEMENT_ADDON_PDF)) {
1157  $modele = $conf->global->MOUVEMENT_ADDON_PDF;
1158  }
1159  }
1160 
1161  $modelpath = "core/modules/stock/doc/";
1162 
1163  return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref);
1164  }
1165 
1173  public function delete(User $user, $notrigger = false)
1174  {
1175  return $this->deleteCommon($user, $notrigger);
1176  //return $this->deleteCommon($user, $notrigger, 1);
1177  }
1178 }
_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)
Add a movement of stock (in one direction only).
if(!function_exists('dol_getprefix')) dol_include_once($relpath, $classname= '')
Make an include_once using default root and alternate root if it fails.
getLibStatut($mode=0)
Return label statut.
deleteCommon(User $user, $notrigger=false, $forcechilddeletion=0)
Delete object in database.
Class to manage stock movements.
__construct($db)
Constructor.
Class with list of lots and properties.
dol_mktime($hour, $minute, $second, $month, $day, $year, $gm= 'auto', $check=1)
Return a timestamp date built from detailed informations (by default a local PHP server timestamp) Re...
reception($user, $fk_product, $entrepot_id, $qty, $price=0, $label= '', $eatby= '', $sellby= '', $batch= '', $datem= '', $id_product_batch=0, $inventorycode= '')
Increase stock for product and subproducts.
Class to manage products or services.
dol_now($mode= 'auto')
Return date for now.
Class to manage Dolibarr users.
Definition: user.class.php:44
_createSubProduct($user, $idProduct, $entrepot_id, $qty, $type, $price=0, $label= '', $inventorycode= '')
Create movement in database for all subproducts.
Class for Mo.
Definition: mo.class.php:34
dol_clone($object, $native=0)
Create a clone of instance of object (new instance with same value for properties) With native = 0: P...
commonGenerateDocument($modelspath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams=null)
Common function for all objects extending CommonObject for generating documents.
const TYPE_SERVICE
Service.
Class to manage suppliers invoices.
calculateBalanceForProductBefore($productidselected, $datebefore)
Return nb of subproducts lines for a product.
$conf db
API class for accounts.
Definition: inc.php:54
LibStatut($mode=0)
Renvoi le libelle d&#39;un status donne.
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...
livraison($user, $fk_product, $entrepot_id, $qty, $price=0, $label= '', $datem= '', $eatby= '', $sellby= '', $batch= '', $id_product_batch=0, $inventorycode= '')
Decrease stock for product and subproducts.
Class to manage projects.
initAsSpecimen()
Initialise an instance with random values.
dol_strlen($string, $stringencoding= 'UTF-8')
Make a strlen call.
price2num($amount, $rounding= '', $option=0)
Function that return a number with universal decimal format (decimal separator is &#39;...
Class to manage shipments.
img_picto($titlealt, $picto, $moreatt= '', $pictoisfullpath=false, $srconly=0, $notitle=0, $alt= '', $morecss= '', $marginleftonlyshort=2)
Show picto whatever it&#39;s its name (generic function)
Class to manage receptions.
Class to manage customers orders.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename= '', $restricttologhandler= '', $logcontext=null)
Write log message into outputs.
getNomUrl($withpicto=0, $option= '', $notooltip=0, $maxlen=24, $morecss= '')
Return a link (with optionaly the picto) Use this-&gt;id,this-&gt;lastname, this-&gt;firstname.
dol_getdate($timestamp, $fast=false, $forcetimezone= '')
Return an array with locale date info.
setOrigin($origin_element, $origin_id)
Set attribute origin to object.
fetch_optionals($rowid=null, $optionsArray=null)
Function to get extra fields of an object into $this-&gt;array_options This method is in most cases call...
Class to manage predefined suppliers products.
fetch($id)
Load object in memory from the database.
Manage record for batch number management.
generateDocument($modele, $outputlangs= '', $hidedetails=0, $hidedesc=0, $hideref=0)
Create object on disk.
dol_print_date($time, $format= '', $tzoutput= 'auto', $outputlangs= '', $encodetooutput=false)
Output date in a string format according to outputlangs (or langs if not defined).
call_trigger($triggerName, $user)
Call trigger based on this instance.
if(!empty($conf->facture->enabled)&&$user->rights->facture->lire) if((!empty($conf->fournisseur->enabled)&&empty($conf->global->MAIN_USE_NEW_SUPPLIERMOD)||!empty($conf->supplier_invoice->enabled))&&$user->rights->fournisseur->facture->lire) if(!empty($conf->don->enabled)&&$user->rights->don->lire) if(!empty($conf->tax->enabled)&&$user->rights->tax->charges->lire) if(!empty($conf->facture->enabled)&&!empty($conf->commande->enabled)&&$user->rights->commande->lire &&empty($conf->global->WORKFLOW_DISABLE_CREATE_INVOICE_FROM_ORDER)) if(!empty($conf->facture->enabled)&&$user->rights->facture->lire) if((!empty($conf->fournisseur->enabled)&&empty($conf->global->MAIN_USE_NEW_SUPPLIERMOD)||!empty($conf->supplier_invoice->enabled))&&$user->rights->fournisseur->facture->lire) $resql
Social contributions to pay.
Definition: index.php:1232
dol_print_error($db= '', $error= '', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
Class to manage invoices.
createBatch($dluo, $qty)
Create or update batch record (update table llx_product_batch).
if(preg_match('/crypted:/i', $dolibarr_main_db_pass)||!empty($dolibarr_main_db_encrypted_pass)) $conf db type
Definition: repair.php:105
Parent class of all other business classes (invoices, contracts, proposals, orders, ...)
get_origin($fk_origin, $origintype)
Return Url link of origin object.
Class to manage warehouses.