mouvementstock.class.php 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796
  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 <http://www.gnu.org/licenses/>.
  19. */
  20. /**
  21. * \file htdocs/product/stock/class/mouvementstock.class.php
  22. * \ingroup stock
  23. * \brief File of class to manage stock movement (input or output)
  24. */
  25. /**
  26. * Class to manage stock movements
  27. */
  28. class MouvementStock extends CommonObject
  29. {
  30. var $product_id;
  31. var $entrepot_id;
  32. var $qty;
  33. var $type;
  34. /**
  35. * Constructor
  36. *
  37. * @param DoliDB $db Database handler
  38. */
  39. function __construct($db)
  40. {
  41. $this->db = $db;
  42. }
  43. /**
  44. * Add a movement of stock (in one direction only)
  45. *
  46. * @param User $user User object
  47. * @param int $fk_product Id of product
  48. * @param int $entrepot_id Id of warehouse
  49. * @param int $qty Qty of movement (can be <0 or >0 depending on parameter type)
  50. * @param int $type Direction of movement:
  51. * 0=input (stock increase after stock transfert), 1=output (stock decrease after stock transfer),
  52. * 2=output (stock decrease), 3=input (stock increase)
  53. * Note that qty should be > 0 with 0 or 3, < 0 with 1 or 2.
  54. * @param int $price Unit price HT of product, used to calculate average weighted price (PMP in french). If 0, average weighted price is not changed.
  55. * @param string $label Label of stock movement
  56. * @param string $inventorycode Inventory code
  57. * @param string $datem Force date of movement
  58. * @param date $eatby eat-by date
  59. * @param date $sellby sell-by date
  60. * @param string $batch batch number
  61. * @param boolean $skip_batch If set to true, stock movement is done without impacting batch record
  62. * @param int $id_product_batch Id product_batch (when skip_batch is flase and we already know which record of product_batch to use)
  63. * @return int <0 if KO, 0 if fk_product is null, >0 if OK
  64. */
  65. function _create($user, $fk_product, $entrepot_id, $qty, $type, $price=0, $label='', $inventorycode='', $datem='',$eatby='',$sellby='',$batch='',$skip_batch=false, $id_product_batch=0)
  66. {
  67. global $conf, $langs;
  68. require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
  69. require_once DOL_DOCUMENT_ROOT.'/product/stock/class/productlot.class.php';
  70. $error = 0;
  71. dol_syslog(get_class($this)."::_create start userid=$user->id, fk_product=$fk_product, warehouse=$entrepot_id, qty=$qty, type=$type, price=$price, label=$label, inventorycode=$inventorycode, datem=".$datem.", eatby=".$eatby.", sellby=".$sellby.", batch=".$batch.", skip_batch=".$skip_batch);
  72. // Clean parameters
  73. if (empty($price)) $price=0;
  74. $now=(! empty($datem) ? $datem : dol_now());
  75. // Check parameters
  76. if (empty($fk_product)) return 0;
  77. if ($eatby < 0)
  78. {
  79. $this->errors[]='ErrorBadValueForParameterEatBy';
  80. return -1;
  81. }
  82. if ($sellby < 0)
  83. {
  84. $this->errors[]='ErrorBadValueForParameterEatBy';
  85. return -1;
  86. }
  87. // Set properties of movement
  88. $this->product_id = $fk_product;
  89. $this->entrepot_id = $entrepot_id;
  90. $this->qty = $qty;
  91. $this->type = $type;
  92. $mvid = 0;
  93. $product = new Product($this->db);
  94. $result=$product->fetch($fk_product);
  95. if ($result < 0)
  96. {
  97. dol_print_error('',"Failed to fetch product");
  98. return -1;
  99. }
  100. $this->db->begin();
  101. $product->load_stock();
  102. // Test if product require batch data. If yes, and there is not, we throw an error.
  103. if (! empty($conf->productbatch->enabled) && $product->hasbatch() && ! $skip_batch)
  104. {
  105. if (empty($batch))
  106. {
  107. $this->errors[]=$langs->trans("ErrorTryToMakeMoveOnProductRequiringBatchData", $product->name);
  108. dol_syslog("Try to make a movement of a product with status_batch on without any batch data");
  109. $this->db->rollback();
  110. return -2;
  111. }
  112. // Check table llx_product_lot from batchnumber for same product
  113. // If found and eatby/sellby defined into table and provided and differs, return error
  114. // If found and eatby/sellby defined into table and not provided, we take value from table
  115. // If found and eatby/sellby not defined into table and provided, we update table
  116. // If found and eatby/sellby not defined into table and not provided, we do nothing
  117. // If not found, we add record
  118. $sql = "SELECT pb.rowid, pb.batch, pb.eatby, pb.sellby FROM ".MAIN_DB_PREFIX."product_lot as pb";
  119. $sql.= " WHERE pb.fk_product = ".$fk_product." AND pb.batch = '".$this->db->escape($batch)."'";
  120. dol_syslog(get_class($this)."::_create scan serial for this product to check if eatby and sellby match", LOG_DEBUG);
  121. $resql = $this->db->query($sql);
  122. if ($resql)
  123. {
  124. $num = $this->db->num_rows($resql);
  125. $i=0;
  126. if ($num > 0)
  127. {
  128. while ($i < $num)
  129. {
  130. $obj = $this->db->fetch_object($resql);
  131. if ($obj->eatby)
  132. {
  133. if ($eatby)
  134. {
  135. $eatbywithouthour=$eatby;
  136. $tmparray=dol_getdate($eatby, true);
  137. $eatbywithouthour=dol_mktime(0, 0, 0, $tmparray['mon'], $tmparray['mday'], $tmparray['year']);
  138. if ($this->db->jdate($obj->eatby) != $eatby && $this->db->jdate($obj->eatby) != $eatbywithouthour) // We test date without hours and with hours for backward compatibility
  139. {
  140. // If found and eatby/sellby defined into table and provided and differs, return error
  141. $this->errors[]=$langs->trans("ThisSerialAlreadyExistWithDifferentDate", $batch, dol_print_date($this->db->jdate($obj->eatby)), dol_print_date($eatby));
  142. dol_syslog($langs->transnoentities("ThisSerialAlreadyExistWithDifferentDate", $batch, dol_print_date($this->db->jdate($obj->eatby)), dol_print_date($eatby)), LOG_ERR);
  143. $this->db->rollback();
  144. return -3;
  145. }
  146. }
  147. else
  148. {
  149. $eatby = $obj->eatby; // If found and eatby/sellby defined into table and not provided, we take value from table
  150. }
  151. }
  152. else
  153. {
  154. if ($eatby) // If found and eatby/sellby not defined into table and provided, we update table
  155. {
  156. $productlot = new Productlot($this->db);
  157. $result = $productlot->fetch($obj->rowid);
  158. $productlot->eatby = $eatby;
  159. $result = $productlot->update($user);
  160. if ($result <= 0)
  161. {
  162. $this->error = $productlot->error;
  163. $this->errors = $productlot->errors;
  164. $this->db->rollback();
  165. return -5;
  166. }
  167. }
  168. }
  169. if ($obj->sellby)
  170. {
  171. if ($sellby)
  172. {
  173. $sellbywithouthour=$sellby;
  174. $tmparray=dol_getdate($eatby, true);
  175. $eatbywithouthour=dol_mktime(0, 0, 0, $tmparray['mon'], $tmparray['mday'], $tmparray['year']);
  176. if ($this->db->jdate($obj->sellby) != $sellby && $this->db->jdate($obj->sellby) != $sellbywithouthour) // We test date without hours and with hours for backward compatibility
  177. {
  178. // If found and eatby/sellby defined into table and provided and differs, return error
  179. $this->errors[]=$langs->trans("ThisSerialAlreadyExistWithDifferentDate", $batch, dol_print_date($this->db->jdate($obj->sellby)), dol_print_date($sellby));
  180. dol_syslog($langs->transnoentities("ThisSerialAlreadyExistWithDifferentDate", $batch, dol_print_date($this->db->jdate($obj->sellby)), dol_print_date($sellby)), LOG_ERR);
  181. $this->db->rollback();
  182. return -3;
  183. }
  184. }
  185. else
  186. {
  187. $sellby = $obj->sellby; // If found and eatby/sellby defined into table and not provided, we take value from table
  188. }
  189. }
  190. else
  191. {
  192. if ($sellby) // If found and eatby/sellby not defined into table and provided, we update table
  193. {
  194. $productlot = new Productlot($this->db);
  195. $result = $productlot->fetch($obj->rowid);
  196. $productlot->sellby = $sellby;
  197. $result = $productlot->update($user);
  198. if ($result <= 0)
  199. {
  200. $this->error = $productlot->error;
  201. $this->errors = $productlot->errors;
  202. $this->db->rollback();
  203. return -5;
  204. }
  205. }
  206. }
  207. $i++;
  208. }
  209. }
  210. else // If not found, we add record
  211. {
  212. $productlot = new Productlot($this->db);
  213. $productlot->fk_product = $fk_product;
  214. $productlot->batch = $batch;
  215. // If we are here = first time we manage this batch, so we used dates provided by users to create lot
  216. $productlot->eatby = $eatby;
  217. $productlot->sellby = $sellby;
  218. $result = $productlot->create($user);
  219. if ($result <= 0)
  220. {
  221. $this->error = $productlot->error;
  222. $this->errors = $productlot->errors;
  223. $this->db->rollback();
  224. return -4;
  225. }
  226. }
  227. }
  228. else
  229. {
  230. dol_print_error($this->db);
  231. $this->db->rollback();
  232. return -1;
  233. }
  234. }
  235. // Define if we must make the stock change (If product type is a service or if stock is used also for services)
  236. $movestock=0;
  237. if ($product->type != Product::TYPE_SERVICE || ! empty($conf->global->STOCK_SUPPORTS_SERVICES)) $movestock=1;
  238. // Check if stock is enough when qty is < 0
  239. // Note that qty should be > 0 with type 0 or 3, < 0 with type 1 or 2.
  240. if ($qty < 0 && empty($conf->global->STOCK_ALLOW_NEGATIVE_TRANSFER))
  241. {
  242. if (! empty($conf->productbatch->enabled) && $product->hasbatch() && ! $skip_batch)
  243. {
  244. $foundforbatch=0;
  245. $qtyisnotenough=0;
  246. foreach($product->stock_warehouse[$entrepot_id]->detail_batch as $batchcursor => $prodbatch)
  247. {
  248. if ($batch != $batchcursor) continue;
  249. $foundforbatch=1;
  250. if ($prodbatch->qty < abs($qty)) $qtyisnotenough=1;
  251. break;
  252. }
  253. if (! $foundforbatch || $qtyisnotenough)
  254. {
  255. $this->error = $langs->trans('qtyToTranferLotIsNotEnough');
  256. $this->errors[] = $langs->trans('qtyToTranferLotIsNotEnough');
  257. $this->db->rollback();
  258. return -8;
  259. }
  260. }
  261. else
  262. {
  263. if (empty($product->stock_warehouse[$entrepot_id]->real) || $product->stock_warehouse[$entrepot_id]->real < abs($qty))
  264. {
  265. $this->error = $langs->trans('qtyToTranferIsNotEnough');
  266. $this->errors[] = $langs->trans('qtyToTranferIsNotEnough');
  267. $this->db->rollback();
  268. return -8;
  269. }
  270. }
  271. }
  272. if ($movestock && $entrepot_id > 0) // Change stock for current product, change for subproduct is done after
  273. {
  274. if(!empty($this->origin)) { // This is set by caller for tracking reason
  275. $origintype = $this->origin->element;
  276. $fk_origin = $this->origin->id;
  277. } else {
  278. $origintype = '';
  279. $fk_origin = 0;
  280. }
  281. $sql = "INSERT INTO ".MAIN_DB_PREFIX."stock_mouvement(";
  282. $sql.= " datem, fk_product, batch, eatby, sellby,";
  283. $sql.= " fk_entrepot, value, type_mouvement, fk_user_author, label, inventorycode, price, fk_origin, origintype";
  284. $sql.= ")";
  285. $sql.= " VALUES ('".$this->db->idate($now)."', ".$this->product_id.", ";
  286. $sql.= " ".($batch?"'".$batch."'":"null").", ";
  287. $sql.= " ".($eatby?"'".$this->db->idate($eatby)."'":"null").", ";
  288. $sql.= " ".($sellby?"'".$this->db->idate($sellby)."'":"null").", ";
  289. $sql.= " ".$this->entrepot_id.", ".$this->qty.", ".$this->type.",";
  290. $sql.= " ".$user->id.",";
  291. $sql.= " '".$this->db->escape($label)."',";
  292. $sql.= " ".($inventorycode?"'".$this->db->escape($inventorycode)."'":"null").",";
  293. $sql.= " '".price2num($price)."',";
  294. $sql.= " '".$fk_origin."',";
  295. $sql.= " '".$origintype."'";
  296. $sql.= ")";
  297. dol_syslog(get_class($this)."::_create", LOG_DEBUG);
  298. $resql = $this->db->query($sql);
  299. if ($resql)
  300. {
  301. $mvid = $this->db->last_insert_id(MAIN_DB_PREFIX."stock_mouvement");
  302. $this->id = $mvid;
  303. }
  304. else
  305. {
  306. $this->errors[]=$this->db->lasterror();
  307. $error = -1;
  308. }
  309. // Define current values for qty and pmp
  310. $oldqty=$product->stock_reel;
  311. $oldpmp=$product->pmp;
  312. $oldqtywarehouse=0;
  313. // Test if there is already a record for couple (warehouse / product)
  314. $alreadyarecord = 0;
  315. if (! $error)
  316. {
  317. $sql = "SELECT rowid, reel FROM ".MAIN_DB_PREFIX."product_stock";
  318. $sql.= " WHERE fk_entrepot = ".$entrepot_id." AND fk_product = ".$fk_product; // This is a unique key
  319. dol_syslog(get_class($this)."::_create", LOG_DEBUG);
  320. $resql=$this->db->query($sql);
  321. if ($resql)
  322. {
  323. $obj = $this->db->fetch_object($resql);
  324. if ($obj)
  325. {
  326. $alreadyarecord = 1;
  327. $oldqtywarehouse = $obj->reel;
  328. $fk_product_stock = $obj->rowid;
  329. }
  330. $this->db->free($resql);
  331. }
  332. else
  333. {
  334. $this->errors[]=$this->db->lasterror();
  335. $error = -2;
  336. }
  337. }
  338. // Calculate new PMP.
  339. $newpmp=0;
  340. if (! $error)
  341. {
  342. // Note: PMP is calculated on stock input only (type of movement = 0 or 3). If type == 0 or 3, qty should be > 0.
  343. // Note: Price should always be >0 or 0. PMP should be always >0 (calculated on input)
  344. if (($type == 0 || $type == 3) && $price > 0)
  345. {
  346. $oldqtytouse=($oldqty >= 0?$oldqty:0);
  347. // We make a test on oldpmp>0 to avoid to use normal rule on old data with no pmp field defined
  348. if ($oldpmp > 0) $newpmp=price2num((($oldqtytouse * $oldpmp) + ($qty * $price)) / ($oldqtytouse + $qty), 'MU');
  349. else
  350. {
  351. $newpmp=$price; // For this product, PMP was not yet set. We set it to input price.
  352. }
  353. //print "oldqtytouse=".$oldqtytouse." oldpmp=".$oldpmp." oldqtywarehousetouse=".$oldqtywarehousetouse." ";
  354. //print "qty=".$qty." newpmp=".$newpmp;
  355. //exit;
  356. }
  357. else if ($type == 1 || $type == 2)
  358. {
  359. // After a stock decrease, we don't change value of PMP for product.
  360. $newpmp = $oldpmp;
  361. }
  362. else
  363. {
  364. $newpmp = $oldpmp;
  365. }
  366. }
  367. // Update stock quantity
  368. if (! $error)
  369. {
  370. if ($alreadyarecord > 0)
  371. {
  372. $sql = "UPDATE ".MAIN_DB_PREFIX."product_stock SET reel = reel + ".$qty;
  373. $sql.= " WHERE fk_entrepot = ".$entrepot_id." AND fk_product = ".$fk_product;
  374. }
  375. else
  376. {
  377. $sql = "INSERT INTO ".MAIN_DB_PREFIX."product_stock";
  378. $sql.= " (reel, fk_entrepot, fk_product) VALUES ";
  379. $sql.= " (".$qty.", ".$entrepot_id.", ".$fk_product.")";
  380. }
  381. dol_syslog(get_class($this)."::_create", LOG_DEBUG);
  382. $resql=$this->db->query($sql);
  383. if (! $resql)
  384. {
  385. $this->errors[]=$this->db->lasterror();
  386. $error = -3;
  387. }
  388. else if (empty($fk_product_stock))
  389. {
  390. $fk_product_stock = $this->db->last_insert_id(MAIN_DB_PREFIX."product_stock");
  391. }
  392. }
  393. // Update detail stock for batch product
  394. if (! $error && ! empty($conf->productbatch->enabled) && $product->hasbatch() && ! $skip_batch)
  395. {
  396. if ($id_product_batch > 0)
  397. {
  398. $result=$this->createBatch($id_product_batch, $qty);
  399. }
  400. else
  401. {
  402. $param_batch=array('fk_product_stock' =>$fk_product_stock, 'batchnumber'=>$batch);
  403. $result=$this->createBatch($param_batch, $qty);
  404. }
  405. if ($result<0) $error++;
  406. }
  407. // Update PMP and denormalized value of stock qty at product level
  408. if (! $error)
  409. {
  410. // $sql = "UPDATE ".MAIN_DB_PREFIX."product SET pmp = ".$newpmp.", stock = ".$this->db->ifsql("stock IS NULL", 0, "stock") . " + ".$qty;
  411. // $sql.= " WHERE rowid = ".$fk_product;
  412. // Update pmp + denormalized fields because we change content of produt_stock. Warning: Do not use "SET p.stock", does not works with pgsql
  413. $sql = "UPDATE ".MAIN_DB_PREFIX."product as p SET p.pmp = ".$newpmp.", ";
  414. $sql.= " stock=(SELECT SUM(ps.reel) FROM ".MAIN_DB_PREFIX."product_stock ps WHERE ps.fk_product = p.rowid)";
  415. $sql.= " WHERE rowid = ".$fk_product;
  416. dol_syslog(get_class($this)."::_create", LOG_DEBUG);
  417. $resql=$this->db->query($sql);
  418. if (! $resql)
  419. {
  420. $this->errors[]=$this->db->lasterror();
  421. $error = -4;
  422. }
  423. }
  424. // 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
  425. // having a lot1/qty=X and lot2/qty=-X, so 0 but we must not loose repartition of different lot.
  426. $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)";
  427. $resql=$this->db->query($sql);
  428. // We do not test error, it can fails if there is child in batch details
  429. }
  430. // Add movement for sub products (recursive call)
  431. if (! $error && ! empty($conf->global->PRODUIT_SOUSPRODUITS) && empty($conf->global->INDEPENDANT_SUBPRODUCT_STOCK))
  432. {
  433. $error = $this->_createSubProduct($user, $fk_product, $entrepot_id, $qty, $type, 0, $label, $inventorycode); // we use 0 as price, because pmp is not changed for subproduct
  434. }
  435. if ($movestock && ! $error)
  436. {
  437. // Call trigger
  438. $result=$this->call_trigger('STOCK_MOVEMENT',$user);
  439. if ($result < 0) $error++;
  440. // End call triggers
  441. }
  442. if (! $error)
  443. {
  444. $this->db->commit();
  445. return $mvid;
  446. }
  447. else
  448. {
  449. $this->db->rollback();
  450. dol_syslog(get_class($this)."::_create error code=".$error, LOG_ERR);
  451. return -6;
  452. }
  453. }
  454. /**
  455. * Create movement in database for all subproducts
  456. *
  457. * @param User $user Object user
  458. * @param int $idProduct Id product
  459. * @param int $entrepot_id Warehouse id
  460. * @param int $qty Quantity
  461. * @param int $type Type
  462. * @param int $price Price
  463. * @param string $label Label of movement
  464. * @param string $inventorycode Inventory code
  465. * @return int <0 if KO, 0 if OK
  466. */
  467. function _createSubProduct($user, $idProduct, $entrepot_id, $qty, $type, $price=0, $label='', $inventorycode='')
  468. {
  469. $error = 0;
  470. $pids = array();
  471. $pqtys = array();
  472. $sql = "SELECT fk_product_pere, fk_product_fils, qty";
  473. $sql.= " FROM ".MAIN_DB_PREFIX."product_association";
  474. $sql.= " WHERE fk_product_pere = ".$idProduct;
  475. $sql.= " AND incdec = 1";
  476. dol_syslog(get_class($this)."::_createSubProduct", LOG_DEBUG);
  477. $resql=$this->db->query($sql);
  478. if ($resql)
  479. {
  480. $i=0;
  481. while ($obj=$this->db->fetch_object($resql))
  482. {
  483. $pids[$i]=$obj->fk_product_fils;
  484. $pqtys[$i]=$obj->qty;
  485. $i++;
  486. }
  487. $this->db->free($resql);
  488. }
  489. else
  490. {
  491. $error = -2;
  492. }
  493. // Create movement for each subproduct
  494. foreach($pids as $key => $value)
  495. {
  496. $tmpmove = clone $this;
  497. $tmpmove->_create($user, $pids[$key], $entrepot_id, ($qty * $pqtys[$key]), $type, 0, $label, $inventorycode); // This will also call _createSubProduct making this recursive
  498. unset($tmpmove);
  499. }
  500. return $error;
  501. }
  502. /**
  503. * Decrease stock for product and subproducts
  504. *
  505. * @param User $user Object user
  506. * @param int $fk_product Id product
  507. * @param int $entrepot_id Warehouse id
  508. * @param int $qty Quantity
  509. * @param int $price Price
  510. * @param string $label Label of stock movement
  511. * @param string $datem Force date of movement
  512. * @param date $eatby eat-by date
  513. * @param date $sellby sell-by date
  514. * @param string $batch batch number
  515. * @param int $id_product_batch Id product_batch
  516. * @return int <0 if KO, >0 if OK
  517. */
  518. function livraison($user, $fk_product, $entrepot_id, $qty, $price=0, $label='', $datem='', $eatby='', $sellby='', $batch='', $id_product_batch=0)
  519. {
  520. $skip_batch = empty($conf->productbatch->enabled);
  521. return $this->_create($user, $fk_product, $entrepot_id, (0 - $qty), 2, $price, $label, '', $datem, $eatby, $sellby, $batch, $skip_batch, $id_product_batch);
  522. }
  523. /**
  524. * Increase stock for product and subproducts
  525. *
  526. * @param User $user Object user
  527. * @param int $fk_product Id product
  528. * @param int $entrepot_id Warehouse id
  529. * @param int $qty Quantity
  530. * @param int $price Price
  531. * @param string $label Label of stock movement
  532. * @param date $eatby eat-by date
  533. * @param date $sellby sell-by date
  534. * @param string $batch batch number
  535. * @return int <0 if KO, >0 if OK
  536. */
  537. function reception($user, $fk_product, $entrepot_id, $qty, $price=0, $label='', $eatby='', $sellby='', $batch='')
  538. {
  539. return $this->_create($user, $fk_product, $entrepot_id, $qty, 3, $price, $label, '', '', $eatby, $sellby, $batch);
  540. }
  541. /**
  542. * Return nb of subproducts lines for a product
  543. *
  544. * @param int $id Id of product
  545. * @return int <0 if KO, nb of subproducts if OK
  546. */
  547. function nbOfSubProdcuts($id)
  548. {
  549. $nbSP=0;
  550. $resql = "SELECT count(*) as nb FROM ".MAIN_DB_PREFIX."product_association";
  551. $resql.= " WHERE fk_product_pere = ".$id;
  552. if ($this->db->query($resql))
  553. {
  554. $obj=$this->db->fetch_object($resql);
  555. $nbSP=$obj->nb;
  556. }
  557. return $nbSP;
  558. }
  559. /**
  560. * Count number of product in stock before a specific date
  561. *
  562. * @param int $productidselected Id of product to count
  563. * @param timestamp $datebefore Date limit
  564. * @return int Number
  565. */
  566. function calculateBalanceForProductBefore($productidselected, $datebefore)
  567. {
  568. $nb=0;
  569. $sql = 'SELECT SUM(value) as nb from '.MAIN_DB_PREFIX.'stock_mouvement';
  570. $sql.= ' WHERE fk_product = '.$productidselected;
  571. $sql.= " AND datem < '".$this->db->idate($datebefore)."'";
  572. dol_syslog(get_class($this).__METHOD__.'', LOG_DEBUG);
  573. $resql=$this->db->query($sql);
  574. if ($resql)
  575. {
  576. $obj=$this->db->fetch_object($resql);
  577. if ($obj) $nb = $obj->nb;
  578. return (empty($nb)?0:$nb);
  579. }
  580. else
  581. {
  582. dol_print_error($this->db);
  583. return -1;
  584. }
  585. }
  586. /**
  587. * Create or update batch record (update table llx_product_batch). No check is done here, done by parent.
  588. *
  589. * @param array|int $dluo Could be either
  590. * - int if row id of product_batch table
  591. * - or complete array('fk_product_stock'=>, 'batchnumber'=>)
  592. * @param int $qty Quantity of product with batch number. May be a negative amount.
  593. * @return int <0 if KO, else return productbatch id
  594. */
  595. private function createBatch($dluo, $qty)
  596. {
  597. global $user;
  598. $pdluo=new Productbatch($this->db);
  599. $result=0;
  600. // Try to find an existing record with same batch number or id
  601. if (is_numeric($dluo))
  602. {
  603. $result=$pdluo->fetch($dluo);
  604. if (empty($pdluo->id))
  605. {
  606. // We didn't find the line. May be it was deleted before by a previous move in same transaction.
  607. $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.';
  608. $this->errors[] = $this->error;
  609. $result = -2;
  610. }
  611. }
  612. else if (is_array($dluo))
  613. {
  614. if (isset($dluo['fk_product_stock']))
  615. {
  616. $vfk_product_stock=$dluo['fk_product_stock'];
  617. $vbatchnumber = $dluo['batchnumber'];
  618. $result = $pdluo->find($vfk_product_stock,'','',$vbatchnumber); // Search on batch number only (eatby and sellby are deprecated here)
  619. }
  620. else
  621. {
  622. dol_syslog(get_class($this)."::createBatch array param dluo must contain at least key fk_product_stock".$error, LOG_ERR);
  623. $result = -1;
  624. }
  625. }
  626. else
  627. {
  628. dol_syslog(get_class($this)."::createBatch error invalid param dluo".$error, LOG_ERR);
  629. $result = -1;
  630. }
  631. if ($result >= 0)
  632. {
  633. // No error
  634. if ($pdluo->id > 0) // product_batch record found
  635. {
  636. //print "Avant ".$pdluo->qty." Apres ".($pdluo->qty + $qty)."<br>";
  637. $pdluo->qty += $qty;
  638. if ($pdluo->qty == 0)
  639. {
  640. $result=$pdluo->delete($user,1);
  641. } else {
  642. $result=$pdluo->update($user,1);
  643. }
  644. }
  645. else // product_batch record not found
  646. {
  647. $pdluo->fk_product_stock=$vfk_product_stock;
  648. $pdluo->qty = $qty;
  649. $pdluo->eatby = $veatby;
  650. $pdluo->sellby = $vsellby;
  651. $pdluo->batch = $vbatchnumber;
  652. $result=$pdluo->create($user,1);
  653. if ($result < 0)
  654. {
  655. $this->error=$pdluo->error;
  656. $this->errors=$pdluo->errors;
  657. }
  658. }
  659. }
  660. return $result;
  661. }
  662. /**
  663. * Return Url link of origin object
  664. *
  665. * @param int $fk_origin Id origin
  666. * @param int $origintype Type origin
  667. * @return string
  668. */
  669. function get_origin($fk_origin, $origintype)
  670. {
  671. $origin='';
  672. switch ($origintype) {
  673. case 'commande':
  674. require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
  675. $origin = new Commande($this->db);
  676. break;
  677. case 'shipping':
  678. require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
  679. $origin = new Expedition($this->db);
  680. break;
  681. case 'facture':
  682. require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
  683. $origin = new Facture($this->db);
  684. break;
  685. case 'order_supplier':
  686. require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.commande.class.php';
  687. $origin = new CommandeFournisseur($this->db);
  688. break;
  689. case 'invoice_supplier':
  690. require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
  691. $origin = new FactureFournisseur($this->db);
  692. break;
  693. default:
  694. if ($origintype)
  695. {
  696. $result=dol_include_once('/'.$origintype.'/class/'.$origintype.'.class.php');
  697. if ($result)
  698. {
  699. $classname = ucfirst($origintype);
  700. $origin = new $classname($this->db);
  701. }
  702. }
  703. break;
  704. }
  705. if (empty($origin) || ! is_object($origin)) return '';
  706. if ($origin->fetch($fk_origin) > 0) {
  707. return $origin->getNomUrl(1);
  708. }
  709. return '';
  710. }
  711. /**
  712. * Initialise an instance with random values.
  713. * Used to build previews or test instances.
  714. * id must be 0 if object instance is a specimen.
  715. *
  716. * @return void
  717. */
  718. function initAsSpecimen()
  719. {
  720. global $user,$langs,$conf,$mysoc;
  721. // Initialize parameters
  722. $this->id=0;
  723. // There is no specific properties. All data into insert are provided as method parameter.
  724. }
  725. }