mouvementstock.class.php 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771
  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. // TODO Check qty is ok for stock move.
  236. if (! empty($conf->productbatch->enabled) && $product->hasbatch() && ! $skip_batch)
  237. {
  238. }
  239. else
  240. {
  241. }
  242. // Define if we must make the stock change (If product type is a service or if stock is used also for services)
  243. $movestock=0;
  244. if ($product->type != Product::TYPE_SERVICE || ! empty($conf->global->STOCK_SUPPORTS_SERVICES)) $movestock=1;
  245. if ($movestock && $entrepot_id > 0) // Change stock for current product, change for subproduct is done after
  246. {
  247. if(!empty($this->origin)) { // This is set by caller for tracking reason
  248. $origintype = $this->origin->element;
  249. $fk_origin = $this->origin->id;
  250. } else {
  251. $origintype = '';
  252. $fk_origin = 0;
  253. }
  254. $sql = "INSERT INTO ".MAIN_DB_PREFIX."stock_mouvement(";
  255. $sql.= " datem, fk_product, batch, eatby, sellby,";
  256. $sql.= " fk_entrepot, value, type_mouvement, fk_user_author, label, inventorycode, price, fk_origin, origintype";
  257. $sql.= ")";
  258. $sql.= " VALUES ('".$this->db->idate($now)."', ".$this->product_id.", ";
  259. $sql.= " ".($batch?"'".$batch."'":"null").", ";
  260. $sql.= " ".($eatby?"'".$this->db->idate($eatby)."'":"null").", ";
  261. $sql.= " ".($sellby?"'".$this->db->idate($sellby)."'":"null").", ";
  262. $sql.= " ".$this->entrepot_id.", ".$this->qty.", ".$this->type.",";
  263. $sql.= " ".$user->id.",";
  264. $sql.= " '".$this->db->escape($label)."',";
  265. $sql.= " ".($inventorycode?"'".$this->db->escape($inventorycode)."'":"null").",";
  266. $sql.= " '".price2num($price)."',";
  267. $sql.= " '".$fk_origin."',";
  268. $sql.= " '".$origintype."'";
  269. $sql.= ")";
  270. dol_syslog(get_class($this)."::_create", LOG_DEBUG);
  271. $resql = $this->db->query($sql);
  272. if ($resql)
  273. {
  274. $mvid = $this->db->last_insert_id(MAIN_DB_PREFIX."stock_mouvement");
  275. $this->id = $mvid;
  276. }
  277. else
  278. {
  279. $this->errors[]=$this->db->lasterror();
  280. $error = -1;
  281. }
  282. // Define current values for qty and pmp
  283. $oldqty=$product->stock_reel;
  284. $oldpmp=$product->pmp;
  285. $oldqtywarehouse=0;
  286. // Test if there is already a record for couple (warehouse / product)
  287. $alreadyarecord = 0;
  288. if (! $error)
  289. {
  290. $sql = "SELECT rowid, reel FROM ".MAIN_DB_PREFIX."product_stock";
  291. $sql.= " WHERE fk_entrepot = ".$entrepot_id." AND fk_product = ".$fk_product; // This is a unique key
  292. dol_syslog(get_class($this)."::_create", LOG_DEBUG);
  293. $resql=$this->db->query($sql);
  294. if ($resql)
  295. {
  296. $obj = $this->db->fetch_object($resql);
  297. if ($obj)
  298. {
  299. $alreadyarecord = 1;
  300. $oldqtywarehouse = $obj->reel;
  301. $fk_product_stock = $obj->rowid;
  302. }
  303. $this->db->free($resql);
  304. }
  305. else
  306. {
  307. $this->errors[]=$this->db->lasterror();
  308. $error = -2;
  309. }
  310. }
  311. // Calculate new PMP.
  312. $newpmp=0;
  313. if (! $error)
  314. {
  315. // Note: PMP is calculated on stock input only (type of movement = 0 or 3). If type == 0 or 3, qty should be > 0.
  316. // Note: Price should always be >0 or 0. PMP should be always >0 (calculated on input)
  317. if (($type == 0 || $type == 3) && $price > 0)
  318. {
  319. $oldqtytouse=($oldqty >= 0?$oldqty:0);
  320. // We make a test on oldpmp>0 to avoid to use normal rule on old data with no pmp field defined
  321. if ($oldpmp > 0) $newpmp=price2num((($oldqtytouse * $oldpmp) + ($qty * $price)) / ($oldqtytouse + $qty), 'MU');
  322. else
  323. {
  324. $newpmp=$price; // For this product, PMP was not yet set. We set it to input price.
  325. }
  326. //print "oldqtytouse=".$oldqtytouse." oldpmp=".$oldpmp." oldqtywarehousetouse=".$oldqtywarehousetouse." ";
  327. //print "qty=".$qty." newpmp=".$newpmp;
  328. //exit;
  329. }
  330. else if ($type == 1 || $type == 2)
  331. {
  332. // After a stock decrease, we don't change value of PMP for product.
  333. $newpmp = $oldpmp;
  334. }
  335. else
  336. {
  337. $newpmp = $oldpmp;
  338. }
  339. }
  340. // Update stock quantity
  341. if (! $error)
  342. {
  343. if ($alreadyarecord > 0)
  344. {
  345. $sql = "UPDATE ".MAIN_DB_PREFIX."product_stock SET reel = reel + ".$qty;
  346. $sql.= " WHERE fk_entrepot = ".$entrepot_id." AND fk_product = ".$fk_product;
  347. }
  348. else
  349. {
  350. $sql = "INSERT INTO ".MAIN_DB_PREFIX."product_stock";
  351. $sql.= " (reel, fk_entrepot, fk_product) VALUES ";
  352. $sql.= " (".$qty.", ".$entrepot_id.", ".$fk_product.")";
  353. }
  354. dol_syslog(get_class($this)."::_create", LOG_DEBUG);
  355. $resql=$this->db->query($sql);
  356. if (! $resql)
  357. {
  358. $this->errors[]=$this->db->lasterror();
  359. $error = -3;
  360. }
  361. else if (empty($fk_product_stock))
  362. {
  363. $fk_product_stock = $this->db->last_insert_id(MAIN_DB_PREFIX."product_stock");
  364. }
  365. }
  366. // Update detail stock for batch product
  367. if (! $error && ! empty($conf->productbatch->enabled) && $product->hasbatch() && ! $skip_batch)
  368. {
  369. if ($id_product_batch > 0)
  370. {
  371. $result=$this->createBatch($id_product_batch, $qty);
  372. }
  373. else
  374. {
  375. $param_batch=array('fk_product_stock' =>$fk_product_stock, 'batchnumber'=>$batch);
  376. $result=$this->createBatch($param_batch, $qty);
  377. }
  378. if ($result<0) $error++;
  379. }
  380. // Update PMP and denormalized value of stock qty at product level
  381. if (! $error)
  382. {
  383. // $sql = "UPDATE ".MAIN_DB_PREFIX."product SET pmp = ".$newpmp.", stock = ".$this->db->ifsql("stock IS NULL", 0, "stock") . " + ".$qty;
  384. // $sql.= " WHERE rowid = ".$fk_product;
  385. // Update pmp + denormalized fields because we change content of produt_stock. Warning: Do not use "SET p.stock", does not works with pgsql
  386. $sql = "UPDATE ".MAIN_DB_PREFIX."product as p SET p.pmp = ".$newpmp.", ";
  387. $sql.= " stock=(SELECT SUM(ps.reel) FROM ".MAIN_DB_PREFIX."product_stock ps WHERE ps.fk_product = p.rowid)";
  388. $sql.= " WHERE rowid = ".$fk_product;
  389. dol_syslog(get_class($this)."::_create", LOG_DEBUG);
  390. $resql=$this->db->query($sql);
  391. if (! $resql)
  392. {
  393. $this->errors[]=$this->db->lasterror();
  394. $error = -4;
  395. }
  396. }
  397. // 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
  398. // having a lot1/qty=X and lot2/qty=-X, so 0 but we must not loose repartition of different lot.
  399. $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)";
  400. $resql=$this->db->query($sql);
  401. // We do not test error, it can fails if there is child in batch details
  402. }
  403. // Add movement for sub products (recursive call)
  404. if (! $error && ! empty($conf->global->PRODUIT_SOUSPRODUITS) && empty($conf->global->INDEPENDANT_SUBPRODUCT_STOCK))
  405. {
  406. $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
  407. }
  408. if ($movestock && ! $error)
  409. {
  410. // Call trigger
  411. $result=$this->call_trigger('STOCK_MOVEMENT',$user);
  412. if ($result < 0) $error++;
  413. // End call triggers
  414. }
  415. if (! $error)
  416. {
  417. $this->db->commit();
  418. return $mvid;
  419. }
  420. else
  421. {
  422. $this->db->rollback();
  423. dol_syslog(get_class($this)."::_create error code=".$error, LOG_ERR);
  424. return -6;
  425. }
  426. }
  427. /**
  428. * Create movement in database for all subproducts
  429. *
  430. * @param User $user Object user
  431. * @param int $idProduct Id product
  432. * @param int $entrepot_id Warehouse id
  433. * @param int $qty Quantity
  434. * @param int $type Type
  435. * @param int $price Price
  436. * @param string $label Label of movement
  437. * @param string $inventorycode Inventory code
  438. * @return int <0 if KO, 0 if OK
  439. */
  440. function _createSubProduct($user, $idProduct, $entrepot_id, $qty, $type, $price=0, $label='', $inventorycode='')
  441. {
  442. $error = 0;
  443. $pids = array();
  444. $pqtys = array();
  445. $sql = "SELECT fk_product_pere, fk_product_fils, qty";
  446. $sql.= " FROM ".MAIN_DB_PREFIX."product_association";
  447. $sql.= " WHERE fk_product_pere = ".$idProduct;
  448. $sql.= " AND incdec = 1";
  449. dol_syslog(get_class($this)."::_createSubProduct", LOG_DEBUG);
  450. $resql=$this->db->query($sql);
  451. if ($resql)
  452. {
  453. $i=0;
  454. while ($obj=$this->db->fetch_object($resql))
  455. {
  456. $pids[$i]=$obj->fk_product_fils;
  457. $pqtys[$i]=$obj->qty;
  458. $i++;
  459. }
  460. $this->db->free($resql);
  461. }
  462. else
  463. {
  464. $error = -2;
  465. }
  466. // Create movement for each subproduct
  467. foreach($pids as $key => $value)
  468. {
  469. $tmpmove = clone $this;
  470. $tmpmove->_create($user, $pids[$key], $entrepot_id, ($qty * $pqtys[$key]), $type, 0, $label, $inventorycode); // This will also call _createSubProduct making this recursive
  471. unset($tmpmove);
  472. }
  473. return $error;
  474. }
  475. /**
  476. * Decrease stock for product and subproducts
  477. *
  478. * @param User $user Object user
  479. * @param int $fk_product Id product
  480. * @param int $entrepot_id Warehouse id
  481. * @param int $qty Quantity
  482. * @param int $price Price
  483. * @param string $label Label of stock movement
  484. * @param string $datem Force date of movement
  485. * @param date $eatby eat-by date
  486. * @param date $sellby sell-by date
  487. * @param string $batch batch number
  488. * @param int $id_product_batch Id product_batch
  489. * @return int <0 if KO, >0 if OK
  490. */
  491. function livraison($user, $fk_product, $entrepot_id, $qty, $price=0, $label='', $datem='', $eatby='', $sellby='', $batch='', $id_product_batch=0)
  492. {
  493. $skip_batch = empty($conf->productbatch->enabled);
  494. return $this->_create($user, $fk_product, $entrepot_id, (0 - $qty), 2, $price, $label, '', $datem, $eatby, $sellby, $batch, $skip_batch, $id_product_batch);
  495. }
  496. /**
  497. * Increase stock for product and subproducts
  498. *
  499. * @param User $user Object user
  500. * @param int $fk_product Id product
  501. * @param int $entrepot_id Warehouse id
  502. * @param int $qty Quantity
  503. * @param int $price Price
  504. * @param string $label Label of stock movement
  505. * @param date $eatby eat-by date
  506. * @param date $sellby sell-by date
  507. * @param string $batch batch number
  508. * @return int <0 if KO, >0 if OK
  509. */
  510. function reception($user, $fk_product, $entrepot_id, $qty, $price=0, $label='', $eatby='', $sellby='', $batch='')
  511. {
  512. return $this->_create($user, $fk_product, $entrepot_id, $qty, 3, $price, $label, '', '', $eatby, $sellby, $batch);
  513. }
  514. /**
  515. * Return nb of subproducts lines for a product
  516. *
  517. * @param int $id Id of product
  518. * @return int <0 if KO, nb of subproducts if OK
  519. */
  520. function nbOfSubProdcuts($id)
  521. {
  522. $nbSP=0;
  523. $resql = "SELECT count(*) as nb FROM ".MAIN_DB_PREFIX."product_association";
  524. $resql.= " WHERE fk_product_pere = ".$id;
  525. if ($this->db->query($resql))
  526. {
  527. $obj=$this->db->fetch_object($resql);
  528. $nbSP=$obj->nb;
  529. }
  530. return $nbSP;
  531. }
  532. /**
  533. * Count number of product in stock before a specific date
  534. *
  535. * @param int $productidselected Id of product to count
  536. * @param timestamp $datebefore Date limit
  537. * @return int Number
  538. */
  539. function calculateBalanceForProductBefore($productidselected, $datebefore)
  540. {
  541. $nb=0;
  542. $sql = 'SELECT SUM(value) as nb from '.MAIN_DB_PREFIX.'stock_mouvement';
  543. $sql.= ' WHERE fk_product = '.$productidselected;
  544. $sql.= " AND datem < '".$this->db->idate($datebefore)."'";
  545. dol_syslog(get_class($this).__METHOD__.'', LOG_DEBUG);
  546. $resql=$this->db->query($sql);
  547. if ($resql)
  548. {
  549. $obj=$this->db->fetch_object($resql);
  550. if ($obj) $nb = $obj->nb;
  551. return (empty($nb)?0:$nb);
  552. }
  553. else
  554. {
  555. dol_print_error($this->db);
  556. return -1;
  557. }
  558. }
  559. /**
  560. * Create or update batch record (update table llx_product_batch). No check is done here, done by parent.
  561. *
  562. * @param array|int $dluo Could be either
  563. * - int if row id of product_batch table
  564. * - or complete array('fk_product_stock'=>, 'batchnumber'=>)
  565. * @param int $qty Quantity of product with batch number. May be a negative amount.
  566. * @return int <0 if KO, else return productbatch id
  567. */
  568. private function createBatch($dluo, $qty)
  569. {
  570. global $user;
  571. $pdluo=new Productbatch($this->db);
  572. $result=0;
  573. // Try to find an existing record with same batch number or id
  574. if (is_numeric($dluo))
  575. {
  576. $result=$pdluo->fetch($dluo);
  577. if (empty($pdluo->id))
  578. {
  579. // We didn't find the line. May be it was deleted before by a previous move in same transaction.
  580. $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.';
  581. $this->errors[] = $this->error;
  582. $result = -2;
  583. }
  584. }
  585. else if (is_array($dluo))
  586. {
  587. if (isset($dluo['fk_product_stock']))
  588. {
  589. $vfk_product_stock=$dluo['fk_product_stock'];
  590. $vbatchnumber = $dluo['batchnumber'];
  591. $result = $pdluo->find($vfk_product_stock,'','',$vbatchnumber); // Search on batch number only (eatby and sellby are deprecated here)
  592. }
  593. else
  594. {
  595. dol_syslog(get_class($this)."::createBatch array param dluo must contain at least key fk_product_stock".$error, LOG_ERR);
  596. $result = -1;
  597. }
  598. }
  599. else
  600. {
  601. dol_syslog(get_class($this)."::createBatch error invalid param dluo".$error, LOG_ERR);
  602. $result = -1;
  603. }
  604. if ($result >= 0)
  605. {
  606. // No error
  607. if ($pdluo->id > 0) // product_batch record found
  608. {
  609. //print "Avant ".$pdluo->qty." Apres ".($pdluo->qty + $qty)."<br>";
  610. $pdluo->qty += $qty;
  611. if ($pdluo->qty == 0)
  612. {
  613. $result=$pdluo->delete($user,1);
  614. } else {
  615. $result=$pdluo->update($user,1);
  616. }
  617. }
  618. else // product_batch record not found
  619. {
  620. $pdluo->fk_product_stock=$vfk_product_stock;
  621. $pdluo->qty = $qty;
  622. $pdluo->eatby = $veatby;
  623. $pdluo->sellby = $vsellby;
  624. $pdluo->batch = $vbatchnumber;
  625. $result=$pdluo->create($user,1);
  626. if ($result < 0)
  627. {
  628. $this->error=$pdluo->error;
  629. $this->errors=$pdluo->errors;
  630. }
  631. }
  632. }
  633. return $result;
  634. }
  635. /**
  636. * Return Url link of origin object
  637. *
  638. * @param int $fk_origin Id origin
  639. * @param int $origintype Type origin
  640. * @return string
  641. */
  642. function get_origin($fk_origin, $origintype)
  643. {
  644. $origin='';
  645. switch ($origintype) {
  646. case 'commande':
  647. require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
  648. $origin = new Commande($this->db);
  649. break;
  650. case 'shipping':
  651. require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
  652. $origin = new Expedition($this->db);
  653. break;
  654. case 'facture':
  655. require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
  656. $origin = new Facture($this->db);
  657. break;
  658. case 'order_supplier':
  659. require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.commande.class.php';
  660. $origin = new CommandeFournisseur($this->db);
  661. break;
  662. case 'invoice_supplier':
  663. require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
  664. $origin = new FactureFournisseur($this->db);
  665. break;
  666. default:
  667. if ($origintype)
  668. {
  669. $result=dol_include_once('/'.$origintype.'/class/'.$origintype.'.class.php');
  670. if ($result)
  671. {
  672. $classname = ucfirst($origintype);
  673. $origin = new $classname($this->db);
  674. }
  675. }
  676. break;
  677. }
  678. if (empty($origin) || ! is_object($origin)) return '';
  679. if ($origin->fetch($fk_origin) > 0) {
  680. return $origin->getNomUrl(1);
  681. }
  682. return '';
  683. }
  684. /**
  685. * Initialise an instance with random values.
  686. * Used to build previews or test instances.
  687. * id must be 0 if object instance is a specimen.
  688. *
  689. * @return void
  690. */
  691. function initAsSpecimen()
  692. {
  693. global $user,$langs,$conf,$mysoc;
  694. // Initialize parameters
  695. $this->id=0;
  696. // There is no specific properties. All data into insert are provided as method parameter.
  697. }
  698. }