| Total Complexity | 194 |
| Total Lines | 1289 |
| Duplicated Lines | 0 % |
| Changes | 0 | ||
Complex classes like MouvementStock often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use MouvementStock, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 39 | class MouvementStock extends CommonObject |
||
| 40 | { |
||
| 41 | /** |
||
| 42 | * @var string Id to identify managed objects |
||
| 43 | */ |
||
| 44 | public $element = 'stockmouvement'; |
||
| 45 | |||
| 46 | /** |
||
| 47 | * @var string Name of table without prefix where object is stored |
||
| 48 | */ |
||
| 49 | public $table_element = 'stock_mouvement'; |
||
| 50 | |||
| 51 | |||
| 52 | /** |
||
| 53 | * @var int ID product |
||
| 54 | */ |
||
| 55 | public $product_id; |
||
| 56 | |||
| 57 | /** |
||
| 58 | * @var int ID warehouse |
||
| 59 | * @deprecated |
||
| 60 | * @see $warehouse_id |
||
| 61 | */ |
||
| 62 | public $entrepot_id; |
||
| 63 | |||
| 64 | /** |
||
| 65 | * @var int ID warehouse |
||
| 66 | */ |
||
| 67 | public $warehouse_id; |
||
| 68 | |||
| 69 | /** |
||
| 70 | * @var float Quantity |
||
| 71 | */ |
||
| 72 | public $qty; |
||
| 73 | |||
| 74 | /** |
||
| 75 | * @var int Type of movement |
||
| 76 | * 0=input (stock increase by a manual/direct stock transfer, correction or inventory), |
||
| 77 | * 1=output (stock decrease after by a manual/direct stock transfer, correction or inventory), |
||
| 78 | * 2=output (stock decrease after a business event like sale, shipment or manufacturing, ...), |
||
| 79 | * 3=input (stock increase after a business event like purchase, reception or manufacturing, ...) |
||
| 80 | * Note that qty should be > 0 with 0 or 3, < 0 with 1 or 2. |
||
| 81 | */ |
||
| 82 | public $type; |
||
| 83 | |||
| 84 | public $datem = ''; |
||
| 85 | public $price; |
||
| 86 | |||
| 87 | /** |
||
| 88 | * @var int ID user author |
||
| 89 | */ |
||
| 90 | public $fk_user_author; |
||
| 91 | |||
| 92 | /** |
||
| 93 | * @var string stock movements label |
||
| 94 | */ |
||
| 95 | public $label; |
||
| 96 | |||
| 97 | /** |
||
| 98 | * @var int ID |
||
| 99 | * @deprecated |
||
| 100 | * @see $origin_id |
||
| 101 | */ |
||
| 102 | public $fk_origin; |
||
| 103 | |||
| 104 | /** |
||
| 105 | * @var int Origin id |
||
| 106 | */ |
||
| 107 | public $origin_id; |
||
| 108 | |||
| 109 | /** |
||
| 110 | * @var string origintype |
||
| 111 | * @deprecated |
||
| 112 | * see $origin_type |
||
| 113 | */ |
||
| 114 | public $origintype; |
||
| 115 | |||
| 116 | /** |
||
| 117 | * @var string Origin type ('project', ...) |
||
| 118 | */ |
||
| 119 | public $origin_type; |
||
| 120 | public $line_id_oject_src; |
||
| 121 | public $line_id_oject_origin; |
||
| 122 | |||
| 123 | |||
| 124 | public $inventorycode; |
||
| 125 | public $batch; |
||
| 126 | |||
| 127 | public $line_id_object_src; |
||
| 128 | public $line_id_object_origin; |
||
| 129 | |||
| 130 | public $eatby; |
||
| 131 | public $sellby; |
||
| 132 | |||
| 133 | |||
| 134 | |||
| 135 | public $fields = array( |
||
| 136 | 'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 10, 'showoncombobox' => 1), |
||
| 137 | 'tms' => array('type' => 'timestamp', 'label' => 'DateModification', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 15), |
||
| 138 | 'datem' => array('type' => 'datetime', 'label' => 'Datem', 'enabled' => 1, 'visible' => -1, 'position' => 20), |
||
| 139 | 'fk_product' => array('type' => 'integer:Product:product/class/product.class.php:1', 'label' => 'Product', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 25), |
||
| 140 | 'fk_entrepot' => array('type' => 'integer:Entrepot:product/stock/class/entrepot.class.php', 'label' => 'Warehouse', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 30), |
||
| 141 | 'value' => array('type' => 'double', 'label' => 'Value', 'enabled' => 1, 'visible' => -1, 'position' => 35), |
||
| 142 | 'price' => array('type' => 'double(24,8)', 'label' => 'Price', 'enabled' => 1, 'visible' => -1, 'position' => 40), |
||
| 143 | 'type_mouvement' => array('type' => 'smallint(6)', 'label' => 'Type mouvement', 'enabled' => 1, 'visible' => -1, 'position' => 45), |
||
| 144 | 'fk_user_author' => array('type' => 'integer:User:user/class/user.class.php', 'label' => 'Fk user author', 'enabled' => 1, 'visible' => -1, 'position' => 50), |
||
| 145 | 'label' => array('type' => 'varchar(255)', 'label' => 'Label', 'enabled' => 1, 'visible' => -1, 'position' => 55), |
||
| 146 | 'fk_origin' => array('type' => 'integer', 'label' => 'Fk origin', 'enabled' => 1, 'visible' => -1, 'position' => 60), |
||
| 147 | 'origintype' => array('type' => 'varchar(32)', 'label' => 'Origintype', 'enabled' => 1, 'visible' => -1, 'position' => 65), |
||
| 148 | 'model_pdf' => array('type' => 'varchar(255)', 'label' => 'Model pdf', 'enabled' => 1, 'visible' => 0, 'position' => 70), |
||
| 149 | 'fk_projet' => array('type' => 'integer:Project:projet/class/project.class.php:1:(fk_statut:=:1)', 'label' => 'Project', 'enabled' => '$conf->project->enabled', 'visible' => -1, 'notnull' => 1, 'position' => 75), |
||
| 150 | 'inventorycode' => array('type' => 'varchar(128)', 'label' => 'InventoryCode', 'enabled' => 1, 'visible' => -1, 'position' => 80), |
||
| 151 | 'batch' => array('type' => 'varchar(30)', 'label' => 'Batch', 'enabled' => 1, 'visible' => -1, 'position' => 85), |
||
| 152 | 'eatby' => array('type' => 'date', 'label' => 'Eatby', 'enabled' => 1, 'visible' => -1, 'position' => 90), |
||
| 153 | 'sellby' => array('type' => 'date', 'label' => 'Sellby', 'enabled' => 1, 'visible' => -1, 'position' => 95), |
||
| 154 | 'fk_project' => array('type' => 'integer:Project:projet/class/project.class.php:1:(fk_statut:=:1)', 'label' => 'Fk project', 'enabled' => 1, 'visible' => -1, 'position' => 100), |
||
| 155 | ); |
||
| 156 | |||
| 157 | |||
| 158 | |||
| 159 | /** |
||
| 160 | * Constructor |
||
| 161 | * |
||
| 162 | * @param DoliDB $db Database handler |
||
|
|
|||
| 163 | */ |
||
| 164 | public function __construct($db) |
||
| 165 | { |
||
| 166 | $this->db = $db; |
||
| 167 | } |
||
| 168 | |||
| 169 | // phpcs:disable PEAR.NamingConventions.ValidFunctionName.PublicUnderscore |
||
| 170 | /** |
||
| 171 | * Add a movement of stock (in one direction only). |
||
| 172 | * This is the lowest level method to record a stock change. There is no control if warehouse is open or not. |
||
| 173 | * $this->origin_type and $this->origin_id can be also be set to save the source object of movement. |
||
| 174 | * |
||
| 175 | * @param User $user User object |
||
| 176 | * @param int $fk_product Id of product |
||
| 177 | * @param int $entrepot_id Id of warehouse |
||
| 178 | * @param float $qty Qty of movement (can be <0 or >0 depending on parameter type) |
||
| 179 | * @param int $type Direction of movement: |
||
| 180 | * 0=input (stock increase by a stock transfer), 1=output (stock decrease by a stock transfer), |
||
| 181 | * 2=output (stock decrease), 3=input (stock increase) |
||
| 182 | * Note that qty should be > 0 with 0 or 3, < 0 with 1 or 2. |
||
| 183 | * @param int $price Unit price HT of product, used to calculate average weighted price (AWP or PMP in french). If 0, average weighted price is not changed. |
||
| 184 | * @param string $label Label of stock movement |
||
| 185 | * @param string $inventorycode Inventory code |
||
| 186 | * @param integer|string $datem Force date of movement |
||
| 187 | * @param integer|string $eatby eat-by date. Will be used if lot does not exists yet and will be created. |
||
| 188 | * @param integer|string $sellby sell-by date. Will be used if lot does not exists yet and will be created. |
||
| 189 | * @param string $batch batch number |
||
| 190 | * @param boolean $skip_batch If set to true, stock movement is done without impacting batch record |
||
| 191 | * @param int $id_product_batch Id product_batch (when skip_batch is false and we already know which record of product_batch to use) |
||
| 192 | * @param int $disablestockchangeforsubproduct Disable stock change for sub-products of kit (useful only if product is a subproduct) |
||
| 193 | * @param int $donotcleanemptylines Do not clean lines in stock table with qty=0 (because we want to have this done by the caller) |
||
| 194 | * @param boolean $force_update_batch Allows to add batch stock movement even if $product doesn't use batch anymore |
||
| 195 | * @return int|string Return integer <0 if KO, 0 if fk_product is null or product id does not exists, >0 if OK, or printabl result of hook |
||
| 196 | */ |
||
| 197 | public function _create($user, $fk_product, $entrepot_id, $qty, $type, $price = 0, $label = '', $inventorycode = '', $datem = '', $eatby = '', $sellby = '', $batch = '', $skip_batch = false, $id_product_batch = 0, $disablestockchangeforsubproduct = 0, $donotcleanemptylines = 0, $force_update_batch = false) |
||
| 198 | { |
||
| 199 | // phpcs:enable |
||
| 200 | global $conf, $langs; |
||
| 201 | |||
| 202 | require_once constant('DOL_DOCUMENT_ROOT') . '/product/class/product.class.php'; |
||
| 203 | require_once constant('DOL_DOCUMENT_ROOT') . '/product/stock/class/productlot.class.php'; |
||
| 204 | |||
| 205 | $error = 0; |
||
| 206 | dol_syslog(get_class($this) . "::_create start userid=$user->id, fk_product=$fk_product, warehouse_id=$entrepot_id, qty=$qty, type=$type, price=$price, label=$label, inventorycode=$inventorycode, datem=" . $datem . ", eatby=" . $eatby . ", sellby=" . $sellby . ", batch=" . $batch . ", skip_batch=" . json_encode($skip_batch)); |
||
| 207 | |||
| 208 | // Call hook at beginning |
||
| 209 | global $action, $hookmanager; |
||
| 210 | $hookmanager->initHooks(array('mouvementstock')); |
||
| 211 | |||
| 212 | if (is_object($hookmanager)) { |
||
| 213 | $parameters = array( |
||
| 214 | 'currentcontext' => 'mouvementstock', |
||
| 215 | 'user' => &$user, |
||
| 216 | 'fk_product' => &$fk_product, |
||
| 217 | 'entrepot_id' => &$entrepot_id, |
||
| 218 | 'qty' => &$qty, |
||
| 219 | 'type' => &$type, |
||
| 220 | 'price' => &$price, |
||
| 221 | 'label' => &$label, |
||
| 222 | 'inventorycode' => &$inventorycode, |
||
| 223 | 'datem' => &$datem, |
||
| 224 | 'eatby' => &$eatby, |
||
| 225 | 'sellby' => &$sellby, |
||
| 226 | 'batch' => &$batch, |
||
| 227 | 'skip_batch' => &$skip_batch, |
||
| 228 | 'id_product_batch' => &$id_product_batch |
||
| 229 | ); |
||
| 230 | $reshook = $hookmanager->executeHooks('stockMovementCreate', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks |
||
| 231 | |||
| 232 | if ($reshook < 0) { |
||
| 233 | if (!empty($hookmanager->resPrint)) { |
||
| 234 | dol_print_error(null, $hookmanager->resPrint); |
||
| 235 | } |
||
| 236 | return $reshook; |
||
| 237 | } elseif ($reshook > 0) { |
||
| 238 | return $hookmanager->resPrint; |
||
| 239 | } |
||
| 240 | } |
||
| 241 | // end hook at beginning |
||
| 242 | |||
| 243 | // Clean parameters |
||
| 244 | $price = price2num($price, 'MU'); // Clean value for the casse we receive a float zero value, to have it a real zero value. |
||
| 245 | if (empty($price)) { |
||
| 246 | $price = 0; |
||
| 247 | } |
||
| 248 | $now = (!empty($datem) ? $datem : dol_now()); |
||
| 249 | |||
| 250 | // Check parameters |
||
| 251 | if (!($fk_product > 0)) { |
||
| 252 | return 0; |
||
| 253 | } |
||
| 254 | if (!($entrepot_id > 0)) { |
||
| 255 | return 0; |
||
| 256 | } |
||
| 257 | |||
| 258 | if (is_numeric($eatby) && $eatby < 0) { |
||
| 259 | dol_syslog(get_class($this) . "::_create start ErrorBadValueForParameterEatBy eatby = " . $eatby); |
||
| 260 | $this->errors[] = 'ErrorBadValueForParameterEatBy'; |
||
| 261 | return -1; |
||
| 262 | } |
||
| 263 | if (is_numeric($sellby) && $sellby < 0) { |
||
| 264 | dol_syslog(get_class($this) . "::_create start ErrorBadValueForParameterSellBy sellby = " . $sellby); |
||
| 265 | $this->errors[] = 'ErrorBadValueForParameterSellBy'; |
||
| 266 | return -1; |
||
| 267 | } |
||
| 268 | |||
| 269 | // Set properties of movement |
||
| 270 | $this->product_id = $fk_product; |
||
| 271 | $this->entrepot_id = $entrepot_id; // deprecated |
||
| 272 | $this->warehouse_id = $entrepot_id; |
||
| 273 | $this->qty = $qty; |
||
| 274 | $this->type = $type; |
||
| 275 | $this->price = price2num($price); |
||
| 276 | $this->label = $label; |
||
| 277 | $this->inventorycode = $inventorycode; |
||
| 278 | $this->datem = $now; |
||
| 279 | $this->batch = $batch; |
||
| 280 | |||
| 281 | $mvid = 0; |
||
| 282 | |||
| 283 | $product = new Product($this->db); |
||
| 284 | |||
| 285 | $result = $product->fetch($fk_product); |
||
| 286 | if ($result < 0) { |
||
| 287 | $this->error = $product->error; |
||
| 288 | $this->errors = $product->errors; |
||
| 289 | dol_print_error(null, "Failed to fetch product"); |
||
| 290 | return -1; |
||
| 291 | } |
||
| 292 | if ($product->id <= 0) { // Can happen if database is corrupted (a product id exist in stock with product that has been removed) |
||
| 293 | return 0; |
||
| 294 | } |
||
| 295 | |||
| 296 | // Define if we must make the stock change (If product type is a service or if stock is used also for services) |
||
| 297 | // Only record into stock tables will be disabled by this (the rest like writing into lot table or movement of subproucts are done) |
||
| 298 | $movestock = 0; |
||
| 299 | if ($product->type != Product::TYPE_SERVICE || getDolGlobalString('STOCK_SUPPORTS_SERVICES')) { |
||
| 300 | $movestock = 1; |
||
| 301 | } |
||
| 302 | |||
| 303 | $this->db->begin(); |
||
| 304 | |||
| 305 | // Set value $product->stock_reel and detail per warehouse into $product->stock_warehouse array |
||
| 306 | if ($movestock) { |
||
| 307 | $product->load_stock('novirtual'); |
||
| 308 | } |
||
| 309 | |||
| 310 | // Test if product require batch data. If yes, and there is not or values are not correct, we throw an error. |
||
| 311 | if (isModEnabled('productbatch') && $product->hasbatch() && !$skip_batch) { |
||
| 312 | if (empty($batch)) { |
||
| 313 | $langs->load("errors"); |
||
| 314 | $this->errors[] = $langs->transnoentitiesnoconv("ErrorTryToMakeMoveOnProductRequiringBatchData", $product->ref); |
||
| 315 | dol_syslog("Try to make a movement of a product with status_batch on without any batch data", LOG_ERR); |
||
| 316 | |||
| 317 | $this->db->rollback(); |
||
| 318 | return -2; |
||
| 319 | } |
||
| 320 | |||
| 321 | // Check table llx_product_lot from batchnumber for same product |
||
| 322 | // If found and eatby/sellby defined into table and provided and differs, return error |
||
| 323 | // If found and eatby/sellby defined into table and not provided, we take value from table |
||
| 324 | // If found and eatby/sellby not defined into table and provided, we update table |
||
| 325 | // If found and eatby/sellby not defined into table and not provided, we do nothing |
||
| 326 | // If not found, we add record |
||
| 327 | $sql = "SELECT pb.rowid, pb.batch, pb.eatby, pb.sellby FROM " . $this->db->prefix() . "product_lot as pb"; |
||
| 328 | $sql .= " WHERE pb.fk_product = " . ((int) $fk_product) . " AND pb.batch = '" . $this->db->escape($batch) . "'"; |
||
| 329 | |||
| 330 | dol_syslog(get_class($this) . "::_create scan serial for this product to check if eatby and sellby match", LOG_DEBUG); |
||
| 331 | |||
| 332 | $resql = $this->db->query($sql); |
||
| 333 | if ($resql) { |
||
| 334 | $num = $this->db->num_rows($resql); |
||
| 335 | $i = 0; |
||
| 336 | if ($num > 0) { |
||
| 337 | while ($i < $num) { |
||
| 338 | $obj = $this->db->fetch_object($resql); |
||
| 339 | if ($obj->eatby) { |
||
| 340 | if ($eatby) { |
||
| 341 | $tmparray = dol_getdate($eatby, true); |
||
| 342 | $eatbywithouthour = dol_mktime(0, 0, 0, $tmparray['mon'], $tmparray['mday'], $tmparray['year']); |
||
| 343 | if ($this->db->jdate($obj->eatby) != $eatby && $this->db->jdate($obj->eatby) != $eatbywithouthour) { // We test date without hours and with hours for backward compatibility |
||
| 344 | // If found and eatby/sellby defined into table and provided and differs, return error |
||
| 345 | $langs->load("stocks"); |
||
| 346 | $this->errors[] = $langs->transnoentitiesnoconv("ThisSerialAlreadyExistWithDifferentDate", $batch, dol_print_date($this->db->jdate($obj->eatby), 'dayhour'), dol_print_date($eatbywithouthour, 'dayhour')); |
||
| 347 | 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); |
||
| 348 | $this->db->rollback(); |
||
| 349 | return -3; |
||
| 350 | } |
||
| 351 | } else { |
||
| 352 | $eatby = $obj->eatby; // If found and eatby/sellby defined into table and not provided, we take value from table |
||
| 353 | } |
||
| 354 | } else { |
||
| 355 | if ($eatby) { // If found and eatby/sellby not defined into table and provided, we update table |
||
| 356 | $productlot = new Productlot($this->db); |
||
| 357 | $result = $productlot->fetch($obj->rowid); |
||
| 358 | $productlot->eatby = $eatby; |
||
| 359 | $result = $productlot->update($user); |
||
| 360 | if ($result <= 0) { |
||
| 361 | $this->error = $productlot->error; |
||
| 362 | $this->errors = $productlot->errors; |
||
| 363 | $this->db->rollback(); |
||
| 364 | return -5; |
||
| 365 | } |
||
| 366 | } |
||
| 367 | } |
||
| 368 | if ($obj->sellby) { |
||
| 369 | if ($sellby) { |
||
| 370 | $tmparray = dol_getdate($sellby, true); |
||
| 371 | $sellbywithouthour = dol_mktime(0, 0, 0, $tmparray['mon'], $tmparray['mday'], $tmparray['year']); |
||
| 372 | if ($this->db->jdate($obj->sellby) != $sellby && $this->db->jdate($obj->sellby) != $sellbywithouthour) { // We test date without hours and with hours for backward compatibility |
||
| 373 | // If found and eatby/sellby defined into table and provided and differs, return error |
||
| 374 | $this->errors[] = $langs->transnoentitiesnoconv("ThisSerialAlreadyExistWithDifferentDate", $batch, dol_print_date($this->db->jdate($obj->sellby)), dol_print_date($sellby)); |
||
| 375 | dol_syslog($langs->transnoentities("ThisSerialAlreadyExistWithDifferentDate", $batch, dol_print_date($this->db->jdate($obj->sellby)), dol_print_date($sellby)), LOG_ERR); |
||
| 376 | $this->db->rollback(); |
||
| 377 | return -3; |
||
| 378 | } |
||
| 379 | } else { |
||
| 380 | $sellby = $obj->sellby; // If found and eatby/sellby defined into table and not provided, we take value from table |
||
| 381 | } |
||
| 382 | } else { |
||
| 383 | if ($sellby) { // If found and eatby/sellby not defined into table and provided, we update table |
||
| 384 | $productlot = new Productlot($this->db); |
||
| 385 | $result = $productlot->fetch($obj->rowid); |
||
| 386 | $productlot->sellby = $sellby; |
||
| 387 | $result = $productlot->update($user); |
||
| 388 | if ($result <= 0) { |
||
| 389 | $this->error = $productlot->error; |
||
| 390 | $this->errors = $productlot->errors; |
||
| 391 | $this->db->rollback(); |
||
| 392 | return -5; |
||
| 393 | } |
||
| 394 | } |
||
| 395 | } |
||
| 396 | |||
| 397 | $i++; |
||
| 398 | } |
||
| 399 | } else { // If not found, we add record |
||
| 400 | $productlot = new Productlot($this->db); |
||
| 401 | $productlot->origin = !empty($this->origin_type) ? $this->origin_type : ''; |
||
| 402 | $productlot->origin_id = !empty($this->origin_id) ? $this->origin_id : 0; |
||
| 403 | $productlot->entity = $conf->entity; |
||
| 404 | $productlot->fk_product = $fk_product; |
||
| 405 | $productlot->batch = $batch; |
||
| 406 | // If we are here = first time we manage this batch, so we used dates provided by users to create lot |
||
| 407 | $productlot->eatby = $eatby; |
||
| 408 | $productlot->sellby = $sellby; |
||
| 409 | $result = $productlot->create($user); |
||
| 410 | if ($result <= 0) { |
||
| 411 | $this->error = $productlot->error; |
||
| 412 | $this->errors = $productlot->errors; |
||
| 413 | $this->db->rollback(); |
||
| 414 | return -4; |
||
| 415 | } |
||
| 416 | } |
||
| 417 | } else { |
||
| 418 | dol_print_error($this->db); |
||
| 419 | $this->db->rollback(); |
||
| 420 | return -1; |
||
| 421 | } |
||
| 422 | } |
||
| 423 | |||
| 424 | // Check if stock is enough when qty is < 0 |
||
| 425 | // Note that qty should be > 0 with type 0 or 3, < 0 with type 1 or 2. |
||
| 426 | if ($movestock && $qty < 0 && !getDolGlobalInt('STOCK_ALLOW_NEGATIVE_TRANSFER')) { |
||
| 427 | if (isModEnabled('productbatch') && $product->hasbatch() && !$skip_batch) { |
||
| 428 | $foundforbatch = 0; |
||
| 429 | $qtyisnotenough = 0; |
||
| 430 | if (isset($product->stock_warehouse[$entrepot_id])) { |
||
| 431 | foreach ($product->stock_warehouse[$entrepot_id]->detail_batch as $batchcursor => $prodbatch) { |
||
| 432 | if ((string) $batch != (string) $batchcursor) { // Lot '59' must be different than lot '59c' |
||
| 433 | continue; |
||
| 434 | } |
||
| 435 | |||
| 436 | $foundforbatch = 1; |
||
| 437 | if ($prodbatch->qty < abs($qty)) { |
||
| 438 | $qtyisnotenough = $prodbatch->qty; |
||
| 439 | } |
||
| 440 | break; |
||
| 441 | } |
||
| 442 | } |
||
| 443 | if (!$foundforbatch || $qtyisnotenough) { |
||
| 444 | $langs->load("stocks"); |
||
| 445 | include_once DOL_DOCUMENT_ROOT . '/product/stock/class/entrepot.class.php'; |
||
| 446 | $tmpwarehouse = new Entrepot($this->db); |
||
| 447 | $tmpwarehouse->fetch($entrepot_id); |
||
| 448 | |||
| 449 | $this->error = $langs->trans('qtyToTranferLotIsNotEnough', $product->ref, $batch, $qtyisnotenough, $tmpwarehouse->ref); |
||
| 450 | $this->errors[] = $langs->trans('qtyToTranferLotIsNotEnough', $product->ref, $batch, $qtyisnotenough, $tmpwarehouse->ref); |
||
| 451 | $this->db->rollback(); |
||
| 452 | return -8; |
||
| 453 | } |
||
| 454 | } else { |
||
| 455 | if (isset($product->stock_warehouse[$entrepot_id]) && (empty($product->stock_warehouse[$entrepot_id]->real) || $product->stock_warehouse[$entrepot_id]->real < abs($qty))) { |
||
| 456 | $langs->load("stocks"); |
||
| 457 | $this->error = $langs->trans('qtyToTranferIsNotEnough') . ' : ' . $product->ref; |
||
| 458 | $this->errors[] = $langs->trans('qtyToTranferIsNotEnough') . ' : ' . $product->ref; |
||
| 459 | $this->db->rollback(); |
||
| 460 | return -8; |
||
| 461 | } |
||
| 462 | } |
||
| 463 | } |
||
| 464 | |||
| 465 | if ($movestock) { // Change stock for current product, change for subproduct is done after |
||
| 466 | // Set $origin_type, origin_id and fk_project |
||
| 467 | $fk_project = $this->fk_project; |
||
| 468 | if (!empty($this->origin_type)) { // This is set by caller for tracking reason |
||
| 469 | $origin_type = $this->origin_type; |
||
| 470 | $origin_id = $this->origin_id; |
||
| 471 | if (empty($fk_project) && $origin_type == 'project') { |
||
| 472 | $fk_project = $origin_id; |
||
| 473 | $origin_type = ''; |
||
| 474 | $origin_id = 0; |
||
| 475 | } |
||
| 476 | } else { |
||
| 477 | $fk_project = 0; |
||
| 478 | $origin_type = ''; |
||
| 479 | $origin_id = 0; |
||
| 480 | } |
||
| 481 | |||
| 482 | $sql = "INSERT INTO " . $this->db->prefix() . "stock_mouvement("; |
||
| 483 | $sql .= " datem, fk_product, batch, eatby, sellby,"; |
||
| 484 | $sql .= " fk_entrepot, value, type_mouvement, fk_user_author, label, inventorycode, price, fk_origin, origintype, fk_projet"; |
||
| 485 | $sql .= ")"; |
||
| 486 | $sql .= " VALUES ('" . $this->db->idate($this->datem) . "', " . ((int) $this->product_id) . ", "; |
||
| 487 | $sql .= " " . ($batch ? "'" . $this->db->escape($batch) . "'" : "null") . ", "; |
||
| 488 | $sql .= " " . ($eatby ? "'" . $this->db->idate($eatby) . "'" : "null") . ", "; |
||
| 489 | $sql .= " " . ($sellby ? "'" . $this->db->idate($sellby) . "'" : "null") . ", "; |
||
| 490 | $sql .= " " . ((int) $this->entrepot_id) . ", " . ((float) $this->qty) . ", " . ((int) $this->type) . ","; |
||
| 491 | $sql .= " " . ((int) $user->id) . ","; |
||
| 492 | $sql .= " '" . $this->db->escape($label) . "',"; |
||
| 493 | $sql .= " " . ($inventorycode ? "'" . $this->db->escape($inventorycode) . "'" : "null") . ","; |
||
| 494 | $sql .= " " . ((float) price2num($price)) . ","; |
||
| 495 | $sql .= " " . ((int) $origin_id) . ","; |
||
| 496 | $sql .= " '" . $this->db->escape($origin_type) . "',"; |
||
| 497 | $sql .= " " . ((int) $fk_project); |
||
| 498 | $sql .= ")"; |
||
| 499 | |||
| 500 | dol_syslog(get_class($this) . "::_create insert record into stock_mouvement", LOG_DEBUG); |
||
| 501 | $resql = $this->db->query($sql); |
||
| 502 | |||
| 503 | if ($resql) { |
||
| 504 | $mvid = $this->db->last_insert_id($this->db->prefix() . "stock_mouvement"); |
||
| 505 | $this->id = $mvid; |
||
| 506 | } else { |
||
| 507 | $this->error = $this->db->lasterror(); |
||
| 508 | $this->errors[] = $this->error; |
||
| 509 | $error = -1; |
||
| 510 | } |
||
| 511 | |||
| 512 | // Define current values for qty and pmp |
||
| 513 | $oldqty = $product->stock_reel; |
||
| 514 | $oldpmp = $product->pmp; |
||
| 515 | $oldqtywarehouse = 0; |
||
| 516 | |||
| 517 | // Test if there is already a record for couple (warehouse / product), so later we will make an update or create. |
||
| 518 | $alreadyarecord = 0; |
||
| 519 | if (!$error) { |
||
| 520 | $sql = "SELECT rowid, reel FROM " . $this->db->prefix() . "product_stock"; |
||
| 521 | $sql .= " WHERE fk_entrepot = " . ((int) $entrepot_id) . " AND fk_product = " . ((int) $fk_product); // This is a unique key |
||
| 522 | |||
| 523 | dol_syslog(get_class($this) . "::_create check if a record already exists in product_stock", LOG_DEBUG); |
||
| 524 | $resql = $this->db->query($sql); |
||
| 525 | if ($resql) { |
||
| 526 | $obj = $this->db->fetch_object($resql); |
||
| 527 | if ($obj) { |
||
| 528 | $alreadyarecord = 1; |
||
| 529 | $oldqtywarehouse = $obj->reel; |
||
| 530 | $fk_product_stock = $obj->rowid; |
||
| 531 | } |
||
| 532 | $this->db->free($resql); |
||
| 533 | } else { |
||
| 534 | $this->errors[] = $this->db->lasterror(); |
||
| 535 | $error = -2; |
||
| 536 | } |
||
| 537 | } |
||
| 538 | |||
| 539 | // Calculate new AWP (PMP) |
||
| 540 | $newpmp = 0; |
||
| 541 | if (!$error) { |
||
| 542 | if ($type == 0 || $type == 3) { |
||
| 543 | // After a stock increase |
||
| 544 | // Note: PMP is calculated on stock input only (type of movement = 0 or 3). If type == 0 or 3, qty should be > 0. |
||
| 545 | // Note: Price should always be >0 or 0. PMP should be always >0 (calculated on input) |
||
| 546 | if ($price > 0 || (getDolGlobalString('STOCK_UPDATE_AWP_EVEN_WHEN_ENTRY_PRICE_IS_NULL') && $price == 0 && in_array($this->origin_type, array('order_supplier', 'invoice_supplier')))) { |
||
| 547 | $oldqtytouse = ($oldqty >= 0 ? $oldqty : 0); |
||
| 548 | // We make a test on oldpmp>0 to avoid to use normal rule on old data with no pmp field defined |
||
| 549 | if ($oldpmp > 0) { |
||
| 550 | $newpmp = price2num((($oldqtytouse * $oldpmp) + ($qty * $price)) / ($oldqtytouse + $qty), 'MU'); |
||
| 551 | } else { |
||
| 552 | $newpmp = $price; // For this product, PMP was not yet set. We set it to input price. |
||
| 553 | } |
||
| 554 | //print "oldqtytouse=".$oldqtytouse." oldpmp=".$oldpmp." oldqtywarehousetouse=".$oldqtywarehousetouse." "; |
||
| 555 | //print "qty=".$qty." newpmp=".$newpmp; |
||
| 556 | //exit; |
||
| 557 | } else { |
||
| 558 | $newpmp = $oldpmp; |
||
| 559 | } |
||
| 560 | } else { |
||
| 561 | // ($type == 1 || $type == 2) |
||
| 562 | // -> After a stock decrease, we don't change value of the AWP/PMP of a product. |
||
| 563 | // else |
||
| 564 | // Type of movement unknown |
||
| 565 | $newpmp = $oldpmp; |
||
| 566 | } |
||
| 567 | } |
||
| 568 | // Update stock quantity |
||
| 569 | if (!$error) { |
||
| 570 | if ($alreadyarecord > 0) { |
||
| 571 | $sql = "UPDATE " . $this->db->prefix() . "product_stock SET reel = " . ((float) $oldqtywarehouse + (float) $qty); |
||
| 572 | $sql .= " WHERE fk_entrepot = " . ((int) $entrepot_id) . " AND fk_product = " . ((int) $fk_product); |
||
| 573 | } else { |
||
| 574 | $sql = "INSERT INTO " . $this->db->prefix() . "product_stock"; |
||
| 575 | $sql .= " (reel, fk_entrepot, fk_product) VALUES "; |
||
| 576 | $sql .= " (" . ((float) $qty) . ", " . ((int) $entrepot_id) . ", " . ((int) $fk_product) . ")"; |
||
| 577 | } |
||
| 578 | |||
| 579 | dol_syslog(get_class($this) . "::_create update stock value", LOG_DEBUG); |
||
| 580 | $resql = $this->db->query($sql); |
||
| 581 | if (!$resql) { |
||
| 582 | $this->errors[] = $this->db->lasterror(); |
||
| 583 | $error = -3; |
||
| 584 | } elseif (empty($fk_product_stock)) { |
||
| 585 | $fk_product_stock = $this->db->last_insert_id($this->db->prefix() . "product_stock"); |
||
| 586 | } |
||
| 587 | } |
||
| 588 | |||
| 589 | // Update detail of stock for the lot. |
||
| 590 | if (!$error && isModEnabled('productbatch') && (($product->hasbatch() && !$skip_batch) || $force_update_batch)) { |
||
| 591 | if ($id_product_batch > 0) { |
||
| 592 | $result = $this->createBatch($id_product_batch, $qty); |
||
| 593 | if ($result == -2 && $fk_product_stock > 0) { // The entry for this product batch does not exists anymore, bu we already have a llx_product_stock, so we recreate the batch entry in product_batch |
||
| 594 | $param_batch = array('fk_product_stock' => $fk_product_stock, 'batchnumber' => $batch); |
||
| 595 | $result = $this->createBatch($param_batch, $qty); |
||
| 596 | } |
||
| 597 | } else { |
||
| 598 | $param_batch = array('fk_product_stock' => $fk_product_stock, 'batchnumber' => $batch); |
||
| 599 | $result = $this->createBatch($param_batch, $qty); |
||
| 600 | } |
||
| 601 | if ($result < 0) { |
||
| 602 | $error++; |
||
| 603 | } |
||
| 604 | } |
||
| 605 | |||
| 606 | // Update PMP and denormalized value of stock qty at product level |
||
| 607 | if (!$error) { |
||
| 608 | $newpmp = price2num($newpmp, 'MU'); |
||
| 609 | |||
| 610 | // $sql = "UPDATE ".$this->db->prefix()."product SET pmp = ".$newpmp.", stock = ".$this->db->ifsql("stock IS NULL", 0, "stock") . " + ".$qty; |
||
| 611 | // $sql.= " WHERE rowid = ".((int) $fk_product); |
||
| 612 | // Update pmp + denormalized fields because we change content of produt_stock. Warning: Do not use "SET p.stock", does not works with pgsql |
||
| 613 | $sql = "UPDATE " . $this->db->prefix() . "product as p SET pmp = " . ((float) $newpmp) . ","; |
||
| 614 | $sql .= " stock=(SELECT SUM(ps.reel) FROM " . $this->db->prefix() . "product_stock as ps WHERE ps.fk_product = p.rowid)"; |
||
| 615 | $sql .= " WHERE rowid = " . ((int) $fk_product); |
||
| 616 | |||
| 617 | dol_syslog(get_class($this) . "::_create update AWP", LOG_DEBUG); |
||
| 618 | $resql = $this->db->query($sql); |
||
| 619 | if (!$resql) { |
||
| 620 | $this->errors[] = $this->db->lasterror(); |
||
| 621 | $error = -4; |
||
| 622 | } |
||
| 623 | } |
||
| 624 | |||
| 625 | if (empty($donotcleanemptylines)) { |
||
| 626 | // 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 |
||
| 627 | // having a lot1/qty=X and lot2/qty=-X, so 0 but we must not loose repartition of different lot. |
||
| 628 | $sql = "DELETE FROM " . $this->db->prefix() . "product_stock WHERE reel = 0 AND rowid NOT IN (SELECT fk_product_stock FROM " . $this->db->prefix() . "product_batch as pb)"; |
||
| 629 | $resql = $this->db->query($sql); |
||
| 630 | // We do not test error, it can fails if there is child in batch details |
||
| 631 | } |
||
| 632 | } |
||
| 633 | |||
| 634 | // Add movement for sub products (recursive call) |
||
| 635 | if (!$error && getDolGlobalString('PRODUIT_SOUSPRODUITS') && !getDolGlobalString('INDEPENDANT_SUBPRODUCT_STOCK') && empty($disablestockchangeforsubproduct)) { |
||
| 636 | $error = $this->_createSubProduct($user, $fk_product, $entrepot_id, $qty, $type, 0, $label, $inventorycode, $datem); // we use 0 as price, because AWP must not change for subproduct |
||
| 637 | } |
||
| 638 | |||
| 639 | if ($movestock && !$error) { |
||
| 640 | // Call trigger |
||
| 641 | $result = $this->call_trigger('STOCK_MOVEMENT', $user); |
||
| 642 | if ($result < 0) { |
||
| 643 | $error++; |
||
| 644 | } |
||
| 645 | // End call triggers |
||
| 646 | // Check unicity for serial numbered equipment once all movement were done. |
||
| 647 | if (!$error && isModEnabled('productbatch') && $product->hasbatch() && !$skip_batch) { |
||
| 648 | if ($product->status_batch == 2 && $qty > 0) { // We check only if we increased qty |
||
| 649 | if ($this->getBatchCount($fk_product, $batch) > 1) { |
||
| 650 | $error++; |
||
| 651 | $this->errors[] = $langs->trans("TooManyQtyForSerialNumber", $product->ref, $batch); |
||
| 652 | } |
||
| 653 | } |
||
| 654 | } |
||
| 655 | } |
||
| 656 | |||
| 657 | if (!$error) { |
||
| 658 | $this->db->commit(); |
||
| 659 | return $mvid; |
||
| 660 | } else { |
||
| 661 | $this->db->rollback(); |
||
| 662 | dol_syslog(get_class($this) . "::_create error code=" . $error, LOG_ERR); |
||
| 663 | return -6; |
||
| 664 | } |
||
| 665 | } |
||
| 666 | |||
| 667 | |||
| 668 | |||
| 669 | /** |
||
| 670 | * Load object in memory from the database |
||
| 671 | * |
||
| 672 | * @param int $id Id object |
||
| 673 | * |
||
| 674 | * @return int Return integer <0 if KO, 0 if not found, >0 if OK |
||
| 675 | */ |
||
| 676 | public function fetch($id) |
||
| 677 | { |
||
| 678 | dol_syslog(__METHOD__, LOG_DEBUG); |
||
| 679 | |||
| 680 | $sql = "SELECT"; |
||
| 681 | $sql .= " t.rowid,"; |
||
| 682 | $sql .= " t.tms,"; |
||
| 683 | $sql .= " t.datem,"; |
||
| 684 | $sql .= " t.fk_product,"; |
||
| 685 | $sql .= " t.fk_entrepot,"; |
||
| 686 | $sql .= " t.value,"; |
||
| 687 | $sql .= " t.price,"; |
||
| 688 | $sql .= " t.type_mouvement,"; |
||
| 689 | $sql .= " t.fk_user_author,"; |
||
| 690 | $sql .= " t.label,"; |
||
| 691 | $sql .= " t.fk_origin as origin_id,"; |
||
| 692 | $sql .= " t.origintype as origin_type,"; |
||
| 693 | $sql .= " t.inventorycode,"; |
||
| 694 | $sql .= " t.batch,"; |
||
| 695 | $sql .= " t.eatby,"; |
||
| 696 | $sql .= " t.sellby,"; |
||
| 697 | $sql .= " t.fk_projet as fk_project"; |
||
| 698 | $sql .= " FROM " . $this->db->prefix() . $this->table_element . " as t"; |
||
| 699 | $sql .= " WHERE t.rowid = " . ((int) $id); |
||
| 700 | |||
| 701 | $resql = $this->db->query($sql); |
||
| 702 | if ($resql) { |
||
| 703 | $numrows = $this->db->num_rows($resql); |
||
| 704 | if ($numrows) { |
||
| 705 | $obj = $this->db->fetch_object($resql); |
||
| 706 | |||
| 707 | $this->id = $obj->rowid; |
||
| 708 | |||
| 709 | $this->product_id = $obj->fk_product; |
||
| 710 | $this->warehouse_id = $obj->fk_entrepot; |
||
| 711 | $this->qty = $obj->value; |
||
| 712 | $this->type = $obj->type_mouvement; |
||
| 713 | |||
| 714 | $this->tms = $this->db->jdate($obj->tms); |
||
| 715 | $this->datem = $this->db->jdate($obj->datem); |
||
| 716 | $this->price = $obj->price; |
||
| 717 | $this->fk_user_author = $obj->fk_user_author; |
||
| 718 | $this->label = $obj->label; |
||
| 719 | $this->fk_origin = $obj->origin_id; // For backward compatibility |
||
| 720 | $this->origintype = $obj->origin_type; // For backward compatibility |
||
| 721 | $this->origin_id = $obj->origin_id; |
||
| 722 | $this->origin_type = $obj->origin_type; |
||
| 723 | $this->inventorycode = $obj->inventorycode; |
||
| 724 | $this->batch = $obj->batch; |
||
| 725 | $this->eatby = $this->db->jdate($obj->eatby); |
||
| 726 | $this->sellby = $this->db->jdate($obj->sellby); |
||
| 727 | $this->fk_project = $obj->fk_project; |
||
| 728 | } |
||
| 729 | |||
| 730 | // Retrieve all extrafield |
||
| 731 | $this->fetch_optionals(); |
||
| 732 | |||
| 733 | // $this->fetch_lines(); |
||
| 734 | |||
| 735 | $this->db->free($resql); |
||
| 736 | |||
| 737 | if ($numrows) { |
||
| 738 | return 1; |
||
| 739 | } else { |
||
| 740 | return 0; |
||
| 741 | } |
||
| 742 | } else { |
||
| 743 | $this->errors[] = 'Error ' . $this->db->lasterror(); |
||
| 744 | dol_syslog(__METHOD__ . ' ' . implode(',', $this->errors), LOG_ERR); |
||
| 745 | |||
| 746 | return -1; |
||
| 747 | } |
||
| 748 | } |
||
| 749 | |||
| 750 | |||
| 751 | |||
| 752 | |||
| 753 | /** |
||
| 754 | * Create movement in database for all subproducts |
||
| 755 | * |
||
| 756 | * @param User $user Object user |
||
| 757 | * @param int $idProduct Id product |
||
| 758 | * @param int $entrepot_id Warehouse id |
||
| 759 | * @param float $qty Quantity |
||
| 760 | * @param int $type Type |
||
| 761 | * @param int $price Price |
||
| 762 | * @param string $label Label of movement |
||
| 763 | * @param string $inventorycode Inventory code |
||
| 764 | * @param integer|string $datem Force date of movement |
||
| 765 | * @return int Return integer <0 if KO, 0 if OK |
||
| 766 | */ |
||
| 767 | private function _createSubProduct($user, $idProduct, $entrepot_id, $qty, $type, $price = 0, $label = '', $inventorycode = '', $datem = '') |
||
| 768 | { |
||
| 769 | global $langs; |
||
| 770 | |||
| 771 | $error = 0; |
||
| 772 | $pids = array(); |
||
| 773 | $pqtys = array(); |
||
| 774 | |||
| 775 | $sql = "SELECT fk_product_pere, fk_product_fils, qty"; |
||
| 776 | $sql .= " FROM " . $this->db->prefix() . "product_association"; |
||
| 777 | $sql .= " WHERE fk_product_pere = " . ((int) $idProduct); |
||
| 778 | $sql .= " AND incdec = 1"; |
||
| 779 | |||
| 780 | dol_syslog(get_class($this) . "::_createSubProduct for parent product " . $idProduct, LOG_DEBUG); |
||
| 781 | $resql = $this->db->query($sql); |
||
| 782 | if ($resql) { |
||
| 783 | $i = 0; |
||
| 784 | while ($obj = $this->db->fetch_object($resql)) { |
||
| 785 | $pids[$i] = $obj->fk_product_fils; |
||
| 786 | $pqtys[$i] = $obj->qty; |
||
| 787 | $i++; |
||
| 788 | } |
||
| 789 | $this->db->free($resql); |
||
| 790 | } else { |
||
| 791 | $error = -2; |
||
| 792 | } |
||
| 793 | |||
| 794 | // Create movement for each subproduct |
||
| 795 | foreach ($pids as $key => $value) { |
||
| 796 | if (!$error) { |
||
| 797 | $tmpmove = dol_clone($this, 1); |
||
| 798 | |||
| 799 | $result = $tmpmove->_create($user, $pids[$key], $entrepot_id, ($qty * $pqtys[$key]), $type, 0, $label, $inventorycode, $datem); // This will also call _createSubProduct making this recursive |
||
| 800 | if ($result < 0) { |
||
| 801 | $this->error = $tmpmove->error; |
||
| 802 | $this->errors = array_merge($this->errors, $tmpmove->errors); |
||
| 803 | if ($result == -2) { |
||
| 804 | $this->errors[] = $langs->trans("ErrorNoteAlsoThatSubProductCantBeFollowedByLot"); |
||
| 805 | } |
||
| 806 | $error = $result; |
||
| 807 | } |
||
| 808 | unset($tmpmove); |
||
| 809 | } |
||
| 810 | } |
||
| 811 | |||
| 812 | return $error; |
||
| 813 | } |
||
| 814 | |||
| 815 | |||
| 816 | /** |
||
| 817 | * Decrease stock for product and subproducts |
||
| 818 | * |
||
| 819 | * @param User $user Object user |
||
| 820 | * @param int $fk_product Id product |
||
| 821 | * @param int $entrepot_id Warehouse id |
||
| 822 | * @param float $qty Quantity |
||
| 823 | * @param int $price Price |
||
| 824 | * @param string $label Label of stock movement |
||
| 825 | * @param int|string $datem Force date of movement |
||
| 826 | * @param int|string $eatby eat-by date |
||
| 827 | * @param int|string $sellby sell-by date |
||
| 828 | * @param string $batch batch number |
||
| 829 | * @param int $id_product_batch Id product_batch |
||
| 830 | * @param string $inventorycode Inventory code |
||
| 831 | * @param int $donotcleanemptylines Do not clean lines that remains in stock table with qty=0 (because we want to have this done by the caller) |
||
| 832 | * @return int Return integer <0 if KO, >0 if OK |
||
| 833 | */ |
||
| 834 | public function livraison($user, $fk_product, $entrepot_id, $qty, $price = 0, $label = '', $datem = '', $eatby = '', $sellby = '', $batch = '', $id_product_batch = 0, $inventorycode = '', $donotcleanemptylines = 0) |
||
| 835 | { |
||
| 836 | global $conf; |
||
| 837 | |||
| 838 | $skip_batch = empty($conf->productbatch->enabled); |
||
| 839 | |||
| 840 | return $this->_create($user, $fk_product, $entrepot_id, (0 - $qty), 2, $price, $label, $inventorycode, $datem, $eatby, $sellby, $batch, $skip_batch, $id_product_batch, 0, $donotcleanemptylines); |
||
| 841 | } |
||
| 842 | |||
| 843 | /** |
||
| 844 | * Increase stock for product and subproducts |
||
| 845 | * |
||
| 846 | * @param User $user Object user |
||
| 847 | * @param int $fk_product Id product |
||
| 848 | * @param int $entrepot_id Warehouse id |
||
| 849 | * @param float $qty Quantity |
||
| 850 | * @param int $price Price |
||
| 851 | * @param string $label Label of stock movement |
||
| 852 | * @param integer|string $eatby eat-by date |
||
| 853 | * @param integer|string $sellby sell-by date |
||
| 854 | * @param string $batch batch number |
||
| 855 | * @param integer|string $datem Force date of movement |
||
| 856 | * @param int $id_product_batch Id product_batch |
||
| 857 | * @param string $inventorycode Inventory code |
||
| 858 | * @param int $donotcleanemptylines Do not clean lines that remains in stock table with qty=0 (because we want to have this done by the caller) |
||
| 859 | * @return int Return integer <0 if KO, >0 if OK |
||
| 860 | */ |
||
| 861 | public function reception($user, $fk_product, $entrepot_id, $qty, $price = 0, $label = '', $eatby = '', $sellby = '', $batch = '', $datem = '', $id_product_batch = 0, $inventorycode = '', $donotcleanemptylines = 0) |
||
| 862 | { |
||
| 863 | global $conf; |
||
| 864 | |||
| 865 | $skip_batch = empty($conf->productbatch->enabled); |
||
| 866 | |||
| 867 | return $this->_create($user, $fk_product, $entrepot_id, $qty, 3, $price, $label, $inventorycode, $datem, $eatby, $sellby, $batch, $skip_batch, $id_product_batch, 0, $donotcleanemptylines); |
||
| 868 | } |
||
| 869 | |||
| 870 | /** |
||
| 871 | * Count number of product in stock before a specific date |
||
| 872 | * |
||
| 873 | * @param int $productidselected Id of product to count |
||
| 874 | * @param integer $datebefore Date limit |
||
| 875 | * @return int Number |
||
| 876 | */ |
||
| 877 | public function calculateBalanceForProductBefore($productidselected, $datebefore) |
||
| 878 | { |
||
| 879 | $nb = 0; |
||
| 880 | |||
| 881 | $sql = "SELECT SUM(value) as nb from " . $this->db->prefix() . "stock_mouvement"; |
||
| 882 | $sql .= " WHERE fk_product = " . ((int) $productidselected); |
||
| 883 | $sql .= " AND datem < '" . $this->db->idate($datebefore) . "'"; |
||
| 884 | |||
| 885 | dol_syslog(get_class($this) . __METHOD__, LOG_DEBUG); |
||
| 886 | $resql = $this->db->query($sql); |
||
| 887 | if ($resql) { |
||
| 888 | $obj = $this->db->fetch_object($resql); |
||
| 889 | if ($obj) { |
||
| 890 | $nb = $obj->nb; |
||
| 891 | } |
||
| 892 | return (empty($nb) ? 0 : $nb); |
||
| 893 | } else { |
||
| 894 | dol_print_error($this->db); |
||
| 895 | return -1; |
||
| 896 | } |
||
| 897 | } |
||
| 898 | |||
| 899 | /** |
||
| 900 | * Create or update batch record (update table llx_product_batch). No check is done here, done by parent. |
||
| 901 | * |
||
| 902 | * @param array|int $dluo Could be either |
||
| 903 | * - int if row id of product_batch table (for update) |
||
| 904 | * - or complete array('fk_product_stock'=>, 'batchnumber'=>) |
||
| 905 | * @param float $qty Quantity of product with batch number. May be a negative amount. |
||
| 906 | * @return int Return integer <0 if KO, -2 if we try to update a product_batchid that does not exist, else return productbatch id |
||
| 907 | */ |
||
| 908 | private function createBatch($dluo, $qty) |
||
| 909 | { |
||
| 910 | global $user, $langs; |
||
| 911 | |||
| 912 | $langs->load('productbatch'); |
||
| 913 | |||
| 914 | $pdluo = new Productbatch($this->db); |
||
| 915 | |||
| 916 | $result = 0; |
||
| 917 | |||
| 918 | // Try to find an existing record with same batch number or id |
||
| 919 | if (is_numeric($dluo)) { |
||
| 920 | $result = $pdluo->fetch($dluo); |
||
| 921 | if (empty($pdluo->id)) { |
||
| 922 | // We didn't find the line. May be it was deleted before by a previous move in same transaction. |
||
| 923 | $this->error = $langs->trans('CantMoveNonExistantSerial'); |
||
| 924 | $this->errors[] = $this->error; |
||
| 925 | $result = -2; |
||
| 926 | } |
||
| 927 | } elseif (is_array($dluo)) { |
||
| 928 | if (isset($dluo['fk_product_stock'])) { |
||
| 929 | $vfk_product_stock = $dluo['fk_product_stock']; |
||
| 930 | $vbatchnumber = $dluo['batchnumber']; |
||
| 931 | |||
| 932 | $result = $pdluo->find($vfk_product_stock, '', '', $vbatchnumber); // Search on batch number only (eatby and sellby are deprecated here) |
||
| 933 | } else { |
||
| 934 | dol_syslog(get_class($this) . "::createBatch array param dluo must contain at least key fk_product_stock", LOG_ERR); |
||
| 935 | $result = -1; |
||
| 936 | } |
||
| 937 | } else { |
||
| 938 | dol_syslog(get_class($this) . "::createBatch error invalid param dluo", LOG_ERR); |
||
| 939 | $result = -1; |
||
| 940 | } |
||
| 941 | |||
| 942 | if ($result >= 0) { |
||
| 943 | // No error |
||
| 944 | if ($pdluo->id > 0) { // product_batch record found |
||
| 945 | //print "Avant ".$pdluo->qty." Apres ".($pdluo->qty + $qty)."<br>"; |
||
| 946 | $pdluo->qty += $qty; |
||
| 947 | if ($pdluo->qty == 0) { |
||
| 948 | $result = $pdluo->delete($user, 1); |
||
| 949 | } else { |
||
| 950 | $result = $pdluo->update($user, 1); |
||
| 951 | } |
||
| 952 | } else { // product_batch record not found |
||
| 953 | $pdluo->fk_product_stock = $vfk_product_stock; |
||
| 954 | $pdluo->qty = $qty; |
||
| 955 | $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. |
||
| 956 | $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. |
||
| 957 | $pdluo->batch = $vbatchnumber; |
||
| 958 | |||
| 959 | $result = $pdluo->create($user, 1); |
||
| 960 | if ($result < 0) { |
||
| 961 | $this->error = $pdluo->error; |
||
| 962 | $this->errors = $pdluo->errors; |
||
| 963 | } |
||
| 964 | } |
||
| 965 | } |
||
| 966 | |||
| 967 | return $result; |
||
| 968 | } |
||
| 969 | |||
| 970 | // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps |
||
| 971 | /** |
||
| 972 | * Return Url link of origin object |
||
| 973 | * |
||
| 974 | * @param int $origin_id Id origin |
||
| 975 | * @param string $origin_type Type origin ('project', 'xxx@MODULENAME', etc) |
||
| 976 | * @return string |
||
| 977 | */ |
||
| 978 | public function get_origin($origin_id, $origin_type) |
||
| 979 | { |
||
| 980 | // phpcs:enable |
||
| 981 | $origin = ''; |
||
| 982 | |||
| 983 | switch ($origin_type) { |
||
| 984 | case 'commande': |
||
| 985 | require_once constant('DOL_DOCUMENT_ROOT') . '/commande/class/commande.class.php'; |
||
| 986 | $origin = new Commande($this->db); |
||
| 987 | break; |
||
| 988 | case 'shipping': |
||
| 989 | require_once constant('DOL_DOCUMENT_ROOT') . '/expedition/class/expedition.class.php'; |
||
| 990 | $origin = new Expedition($this->db); |
||
| 991 | break; |
||
| 992 | case 'facture': |
||
| 993 | require_once constant('DOL_DOCUMENT_ROOT') . '/compta/facture/class/facture.class.php'; |
||
| 994 | $origin = new Facture($this->db); |
||
| 995 | break; |
||
| 996 | case 'order_supplier': |
||
| 997 | require_once constant('DOL_DOCUMENT_ROOT') . '/fourn/class/fournisseur.commande.class.php'; |
||
| 998 | $origin = new CommandeFournisseur($this->db); |
||
| 999 | break; |
||
| 1000 | case 'invoice_supplier': |
||
| 1001 | require_once constant('DOL_DOCUMENT_ROOT') . '/fourn/class/fournisseur.facture.class.php'; |
||
| 1002 | $origin = new FactureFournisseur($this->db); |
||
| 1003 | break; |
||
| 1004 | case 'project': |
||
| 1005 | require_once constant('DOL_DOCUMENT_ROOT') . '/projet/class/project.class.php'; |
||
| 1006 | $origin = new Project($this->db); |
||
| 1007 | break; |
||
| 1008 | case 'mo': |
||
| 1009 | require_once constant('DOL_DOCUMENT_ROOT') . '/mrp/class/mo.class.php'; |
||
| 1010 | $origin = new Mo($this->db); |
||
| 1011 | break; |
||
| 1012 | case 'user': |
||
| 1013 | require_once constant('DOL_DOCUMENT_ROOT') . '/user/class/user.class.php'; |
||
| 1014 | $origin = new User($this->db); |
||
| 1015 | break; |
||
| 1016 | case 'reception': |
||
| 1017 | require_once constant('DOL_DOCUMENT_ROOT') . '/reception/class/reception.class.php'; |
||
| 1018 | $origin = new Reception($this->db); |
||
| 1019 | break; |
||
| 1020 | case 'inventory': |
||
| 1021 | require_once constant('DOL_DOCUMENT_ROOT') . '/product/inventory/class/inventory.class.php'; |
||
| 1022 | $origin = new Inventory($this->db); |
||
| 1023 | break; |
||
| 1024 | default: |
||
| 1025 | if ($origin_type) { |
||
| 1026 | // Separate origin_type with "@" : left part is class name, right part is module name |
||
| 1027 | $origin_type_array = explode('@', $origin_type); |
||
| 1028 | $classname = $origin_type_array[0]; |
||
| 1029 | $modulename = empty($origin_type_array[1]) ? strtolower($classname) : $origin_type_array[1]; |
||
| 1030 | |||
| 1031 | $result = dol_include_once('/' . $modulename . '/class/' . $classname . '.class.php'); |
||
| 1032 | |||
| 1033 | if ($result) { |
||
| 1034 | $classname = ucfirst($classname); |
||
| 1035 | $origin = new $classname($this->db); |
||
| 1036 | } |
||
| 1037 | } |
||
| 1038 | break; |
||
| 1039 | } |
||
| 1040 | |||
| 1041 | if (empty($origin) || !is_object($origin)) { |
||
| 1042 | return ''; |
||
| 1043 | } |
||
| 1044 | |||
| 1045 | if ($origin->fetch($origin_id) > 0) { |
||
| 1046 | return $origin->getNomUrl(1); |
||
| 1047 | } |
||
| 1048 | |||
| 1049 | return ''; |
||
| 1050 | } |
||
| 1051 | |||
| 1052 | /** |
||
| 1053 | * Set attribute origin_type and fk_origin to object |
||
| 1054 | * |
||
| 1055 | * @param string $origin_element Type of element |
||
| 1056 | * @param int $origin_id Id of element |
||
| 1057 | * @param int $line_id_object_src Id line of element Source |
||
| 1058 | * @param int $line_id_object_origin Id line of element Origin |
||
| 1059 | * |
||
| 1060 | * @return void |
||
| 1061 | */ |
||
| 1062 | public function setOrigin($origin_element, $origin_id, $line_id_object_src = 0, $line_id_object_origin = 0) |
||
| 1063 | { |
||
| 1064 | $this->origin_type = $origin_element; |
||
| 1065 | $this->origin_id = $origin_id; |
||
| 1066 | $this->line_id_object_src = $line_id_object_src; |
||
| 1067 | $this->line_id_object_origin = $line_id_object_origin; |
||
| 1068 | // For backward compatibility |
||
| 1069 | $this->origintype = $origin_element; |
||
| 1070 | $this->fk_origin = $origin_id; |
||
| 1071 | } |
||
| 1072 | |||
| 1073 | |||
| 1074 | /** |
||
| 1075 | * Initialise an instance with random values. |
||
| 1076 | * Used to build previews or test instances. |
||
| 1077 | * id must be 0 if object instance is a specimen. |
||
| 1078 | * |
||
| 1079 | * @return int |
||
| 1080 | */ |
||
| 1081 | public function initAsSpecimen() |
||
| 1089 | } |
||
| 1090 | |||
| 1091 | /** |
||
| 1092 | * Return html string with picto for type of movement |
||
| 1093 | * |
||
| 1094 | * @param int $withlabel With label |
||
| 1095 | * @return string String with URL |
||
| 1096 | */ |
||
| 1097 | public function getTypeMovement($withlabel = 0) |
||
| 1130 | } |
||
| 1131 | |||
| 1132 | /** |
||
| 1133 | * Return a link (with optionally the picto) |
||
| 1134 | * Use this->id,this->lastname, this->firstname |
||
| 1135 | * |
||
| 1136 | * @param int $withpicto Include picto in link (0=No picto, 1=Include picto into link, 2=Only picto) |
||
| 1137 | * @param string $option On what the link point to ('' = Tab of stock movement of warehouse, 'movements' = list of movements) |
||
| 1138 | * @param integer $notooltip 1=Disable tooltip |
||
| 1139 | * @param int $maxlen Max length of visible user name |
||
| 1140 | * @param string $morecss Add more css on link |
||
| 1141 | * @return string String with URL |
||
| 1142 | */ |
||
| 1143 | public function getNomUrl($withpicto = 0, $option = '', $notooltip = 0, $maxlen = 24, $morecss = '') |
||
| 1183 | } |
||
| 1184 | |||
| 1185 | /** |
||
| 1186 | * Return label statut |
||
| 1187 | * |
||
| 1188 | * @param int $mode 0=libelle long, 1=libelle court, 2=Picto + Libelle court, 3=Picto, 4=Picto + Libelle long, 5=Libelle court + Picto |
||
| 1189 | * @return string Label of status |
||
| 1190 | */ |
||
| 1191 | public function getLibStatut($mode = 0) |
||
| 1192 | { |
||
| 1193 | return $this->LibStatut($mode); |
||
| 1194 | } |
||
| 1195 | |||
| 1196 | // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps |
||
| 1197 | /** |
||
| 1198 | * Return the label of the status |
||
| 1199 | * |
||
| 1200 | * @param int $mode 0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=Short label + Picto, 6=Long label + Picto |
||
| 1201 | * @return string Label of status |
||
| 1202 | */ |
||
| 1203 | public function LibStatut($mode = 0) |
||
| 1204 | { |
||
| 1205 | // phpcs:enable |
||
| 1206 | global $langs; |
||
| 1207 | |||
| 1208 | if ($mode == 0 || $mode == 1) { |
||
| 1209 | return $langs->trans('StatusNotApplicable'); |
||
| 1210 | } elseif ($mode == 2) { |
||
| 1211 | return img_picto($langs->trans('StatusNotApplicable'), 'statut9') . ' ' . $langs->trans('StatusNotApplicable'); |
||
| 1212 | } elseif ($mode == 3) { |
||
| 1213 | return img_picto($langs->trans('StatusNotApplicable'), 'statut9'); |
||
| 1214 | } elseif ($mode == 4) { |
||
| 1215 | return img_picto($langs->trans('StatusNotApplicable'), 'statut9') . ' ' . $langs->trans('StatusNotApplicable'); |
||
| 1216 | } elseif ($mode == 5) { |
||
| 1217 | return $langs->trans('StatusNotApplicable') . ' ' . img_picto($langs->trans('StatusNotApplicable'), 'statut9'); |
||
| 1218 | } |
||
| 1219 | |||
| 1220 | return 'Bad value for mode'; |
||
| 1221 | } |
||
| 1222 | |||
| 1223 | /** |
||
| 1224 | * Create object on disk |
||
| 1225 | * |
||
| 1226 | * @param string $modele force le modele a utiliser ('' to not force) |
||
| 1227 | * @param Translate $outputlangs Object langs to use for output |
||
| 1228 | * @param int $hidedetails Hide details of lines |
||
| 1229 | * @param int $hidedesc Hide description |
||
| 1230 | * @param int $hideref Hide ref |
||
| 1231 | * @return int 0 if KO, 1 if OK |
||
| 1232 | */ |
||
| 1233 | public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0) |
||
| 1253 | } |
||
| 1254 | |||
| 1255 | /** |
||
| 1256 | * Delete object in database |
||
| 1257 | * |
||
| 1258 | * @param User $user User that deletes |
||
| 1259 | * @param int $notrigger 0=launch triggers after, 1=disable triggers |
||
| 1260 | * @return int Return integer <0 if KO, >0 if OK |
||
| 1261 | */ |
||
| 1262 | public function delete(User $user, $notrigger = 0) |
||
| 1263 | { |
||
| 1264 | return $this->deleteCommon($user, $notrigger); |
||
| 1265 | //return $this->deleteCommon($user, $notrigger, 1); |
||
| 1266 | } |
||
| 1267 | |||
| 1268 | /** |
||
| 1269 | * Retrieve number of equipment for a product lot/serial |
||
| 1270 | * |
||
| 1271 | * @param int $fk_product Product id |
||
| 1272 | * @param string $batch batch number |
||
| 1273 | * @return int Return integer <0 if KO, number of equipment found if OK |
||
| 1274 | */ |
||
| 1275 | private function getBatchCount($fk_product, $batch) |
||
| 1299 | } |
||
| 1300 | |||
| 1301 | /** |
||
| 1302 | * reverse movement for object by updating infos |
||
| 1303 | * @return int 1 if OK,-1 if KO |
||
| 1304 | */ |
||
| 1305 | public function reverseMouvement() |
||
| 1306 | { |
||
| 1307 | $formattedDate = "REVERTMV" . dol_print_date($this->datem, '%Y%m%d%His'); |
||
| 1308 | if ($this->label == 'Annulation movement ID' . $this->id) { |
||
| 1328 | } |
||
| 1329 | } |
||
| 1330 | } |
||
| 1331 |