From c8293fc2a407866b02e059a6b253f6c78122ef02 Mon Sep 17 00:00:00 2001 From: VESSILLER Date: Fri, 26 Jan 2024 11:13:28 +0100 Subject: [PATCH] NEW shipment kits with dispatcher v2 --- htdocs/admin/stock.php | 8 +- htdocs/expedition/card.php | 87 +- htdocs/expedition/class/expedition.class.php | 840 ++++++++++++------ .../class/expeditionlinebatch.class.php | 4 +- htdocs/expedition/dispatch.php | 394 ++++---- htdocs/expedition/js/lib_dispatch.js.php | 75 +- .../install/mysql/migration/17.0.0-18.0.0.sql | 12 + .../mysql/tables/llx_expeditiondet.key.sql | 4 + .../mysql/tables/llx_expeditiondet.sql | 2 + .../mysql/tables/llx_expeditiondet_batch.sql | 3 +- htdocs/langs/en_US/products.lang | 1 + htdocs/langs/en_US/sendings.lang | 2 + htdocs/product/class/product.class.php | 16 +- .../stock/class/mouvementstock.class.php | 5 +- 14 files changed, 979 insertions(+), 474 deletions(-) diff --git a/htdocs/admin/stock.php b/htdocs/admin/stock.php index 15a88dea73042..b8f8e76766d60 100644 --- a/htdocs/admin/stock.php +++ b/htdocs/admin/stock.php @@ -180,10 +180,16 @@ $disabled = ''; +if (getDolGlobalInt('PRODUIT_SOUSPRODUITS') || isModEnabled('productbatch')) { + $disabled = ' disabled'; +} +if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) { + $langs->load('products'); + print info_admin($langs->trans('WhenProductVirtualOnOptionAreForced')); +} if (isModEnabled('productbatch')) { // If module lot/serial enabled, we force the inc/dec mode to STOCK_CALCULATE_ON_SHIPMENT_CLOSE and STOCK_CALCULATE_ON_RECEPTION_CLOSE $langs->load("productbatch"); - $disabled = ' disabled'; // STOCK_CALCULATE_ON_SHIPMENT_CLOSE $descmode = $langs->trans('DeStockOnShipmentOnClosing'); diff --git a/htdocs/expedition/card.php b/htdocs/expedition/card.php index 1486420fdbf0d..23d6bc498a840 100644 --- a/htdocs/expedition/card.php +++ b/htdocs/expedition/card.php @@ -424,7 +424,14 @@ } else { // batch mode if ($batch_line[$i]['qty'] > 0 || ($batch_line[$i]['qty'] == 0 && getDolGlobalString('SHIPMENT_GETS_ALL_ORDER_PRODUCTS'))) { - $ret = $object->addline_batch($batch_line[$i], $array_options[$i]); + $origin_line_id = (int) $batch_line[$i]['ix_l']; + $origin_line = new OrderLine($db); + $res = $origin_line->fetch($origin_line_id); + if ($res <= 0) { + $error++; + setEventMessages($origin_line->error, $origin_line->errors, 'errors'); + } + $ret = $object->addline_batch($batch_line[$i], $array_options[$i], $origin_line); if ($ret < 0) { setEventMessages($object->error, $object->errors, 'errors'); $error++; @@ -2262,6 +2269,8 @@ print ''; // Loop on each product to send/sent + $conf->cache['product'] = array(); + $conf->cache['warehouse'] = array(); for ($i = 0; $i < $num_prod; $i++) { $parameters = array('i' => $i, 'line' => $lines[$i], 'line_id' => $line_id, 'num' => $num_prod, 'alreadysent' => $alreadysent, 'editColspan' => !empty($editColspan) ? $editColspan : 0, 'outputlangs' => $outputlangs); $reshook = $hookmanager->executeHooks('printObjectLine', $parameters, $object, $action); @@ -2282,8 +2291,13 @@ if ($lines[$i]->fk_product > 0) { // Define output language if (getDolGlobalInt('MAIN_MULTILANGS') && !empty($conf->global->PRODUIT_TEXTS_IN_THIRDPARTY_LANGUAGE)) { - $prod = new Product($db); - $prod->fetch($lines[$i]->fk_product); + $product_id = $lines[$i]->fk_product; + if (empty($conf->cache['product'][$product_id])) { + $prod = new Product($db); + $prod->fetch($product_id); + } else { + $prod = $conf->cache['product'][$product_id]; + } $label = (!empty($prod->multilangs[$outputlangs->defaultlang]["label"])) ? $prod->multilangs[$outputlangs->defaultlang]["label"] : $lines[$i]->product_label; } else { $label = (!empty($lines[$i]->label) ? $lines[$i]->label : $lines[$i]->product_label); @@ -2473,19 +2487,55 @@ if (isModEnabled('stock')) { print ''; if ($lines[$i]->entrepot_id > 0 && $lines[$i]->product->stockable_product == Product::ENABLED_STOCK) { - $entrepot = new Entrepot($db); - $entrepot->fetch($lines[$i]->entrepot_id); - print $entrepot->getNomUrl(1); + $warehouse_id = $lines[$i]->entrepot_id; + if (empty($conf->cache['warehouse'][$warehouse_id])) { + $warehouse = new Entrepot($db); + $warehouse->fetch($warehouse_id); + } else { + $warehouse = $conf->cache['warehouse'][$warehouse_id]; + } + print $warehouse->getNomUrl(1); } elseif (count($lines[$i]->details_entrepot) > 1) { $detail = ''; foreach ($lines[$i]->details_entrepot as $detail_entrepot) { - if ($detail_entrepot->entrepot_id > 0) { - $entrepot = new Entrepot($db); - $entrepot->fetch($detail_entrepot->entrepot_id); - $detail .= $langs->trans("DetailWarehouseFormat", $entrepot->label, $detail_entrepot->qty_shipped).'
'; + $warehouse_id = $detail_entrepot->entrepot_id; + if ($warehouse_id > 0) { + if (empty($conf->cache['warehouse'][$warehouse_id])) { + $warehouse = new Entrepot($db); + $warehouse->fetch($warehouse_id); + } else { + $warehouse = $conf->cache['warehouse'][$warehouse_id]; + } + $detail .= $langs->trans("DetailWarehouseFormat", $warehouse->label, $detail_entrepot->qty_shipped).'
'; } } print $form->textwithtooltip(img_picto('', 'object_stock').' '.$langs->trans("DetailWarehouseNumber"), $detail); + } elseif (count($lines[$i]->detail_children) > 1) { + $detail = ''; + foreach ($lines[$i]->detail_children as $child_product_id => $child_stock_list) { + foreach ($child_stock_list as $warehouse_id => $total_qty) { + // get product from cache + $child_product_label = ''; + if (empty($conf->cache['product'][$child_product_id])) { + $child_product = new Product($db); + $child_product->fetch($child_product_id); + } else { + $child_product = $conf->cache['product'][$child_product_id]; + } + $child_product_label = $child_product->ref . ' ' . $child_product->label; + + // get warehouse from cache + if (empty($conf->cache['warehouse'][$warehouse_id])) { + $child_warehouse = new Entrepot($db); + $child_warehouse->fetch($warehouse_id); + } else { + $child_warehouse = $conf->cache['warehouse'][$warehouse_id]; + } + + $detail .= $langs->trans('DetailChildrenFormat', $child_product_label, $child_warehouse->label, $total_qty).'
'; + } + } + print $form->textwithtooltip(img_picto('', 'object_stock').' '.$langs->trans('DetailWarehouseNumber'), $detail); } print ''; } @@ -2546,9 +2596,24 @@ print '
'; print ''; } elseif ($object->statut == Expedition::STATUS_DRAFT) { + $edit_url = $_SERVER["PHP_SELF"].'?id='.$object->id.'&action=editline&token='.newToken().'&lineid='.$lines[$i]->id; + if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) { + $product_id = $lines[$i]->fk_product; + if (empty($conf->cache['product'][$product_id])) { + $product = new Product($db); + $product->fetch($product_id); + } else { + $product = $conf->cache['product'][$product_id]; + } + + if ($product->hasFatherOrChild(1)) { + $edit_url = dol_buildpath('/expedition/dispatch.php?id='.$object->id, 1); + } + } + // edit-delete buttons print ''; - print 'id.'">'.img_edit().''; + print ''.img_edit().''; print ''; print ''; print 'id.'">'.img_delete().''; diff --git a/htdocs/expedition/class/expedition.class.php b/htdocs/expedition/class/expedition.class.php index 6f0cda9a39261..07c39bac19b7a 100644 --- a/htdocs/expedition/class/expedition.class.php +++ b/htdocs/expedition/class/expedition.class.php @@ -401,15 +401,143 @@ public function create($user, $notrigger = 0) if ($this->db->query($sql)) { // Insert of lines $num = count($this->lines); + $kits_list = array(); + if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) { + for ($i = 0; $i < $num; $i++) { + if (empty($this->lines[$i]->product_type) || getDolGlobalString('STOCK_SUPPORTS_SERVICES') || getDolGlobalString('SHIPMENT_SUPPORTS_SERVICES')) { + // virtual products + $line = $this->lines[$i]; + if ($line->fk_product > 0) { + if (!isset($kits_list[$line->fk_product])) { + if (!isset($line->product)) { + $line_product = new Product($this->db); + $result = $line_product->fetch($line->fk_product, '', '', '', 1, 1, 1); + if ($result <= 0) { + $error++; + } + } else { + $line_product = $line->product; + } + + // get all children of virtual product + $line_product->get_sousproduits_arbo(); + $prods_arbo = $line_product->get_arbo_each_prod($line->qty); + if (count($prods_arbo) > 0) { + $kits_list[$line->fk_product] = array( + 'arbo' => $prods_arbo, + 'total_qty' => $line->qty, + ); + } + } else { + $kits_list[$line->fk_product]['total_qty'] += $line->qty; + } + } + } + } + } + $kits_id_cached = array(); + $sub_kits_id_cached = array(); for ($i = 0; $i < $num; $i++) { - if (empty($this->lines[$i]->product_type) || !empty($conf->global->STOCK_SUPPORTS_SERVICES)) { - if (!isset($this->lines[$i]->detail_batch)) { // no batch management - if ($this->create_line($this->lines[$i]->entrepot_id, $this->lines[$i]->origin_line_id, $this->lines[$i]->qty, $this->lines[$i]->rang, $this->lines[$i]->array_options) <= 0) { - $error++; + $line = $this->lines[$i]; + if (empty($line->product_type) || getDolGlobalString('STOCK_SUPPORTS_SERVICES') || getDolGlobalString('SHIPMENT_SUPPORTS_SERVICES')) { + $line_id = 0; + if (!isset($kits_id_cached[$line->fk_product])) { + if (!isset($line->detail_batch) || isset($kits_list[$line->fk_product])) { // no batch management or is kit + $qty = isset($kits_list[$line->fk_product]) ? $kits_list[$line->fk_product]['total_qty'] : $line->qty; + $warehouse_id = isset($kits_list[$line->fk_product]) ? 0 : $line->entrepot_id; + $line_id = $this->create_line($warehouse_id, $line->origin_line_id, $qty, $line->rang, $line->array_options, 0, $line->fk_product); + if ($line_id <= 0) { + $error++; + } + if (isset($kits_list[$line->fk_product])) $kits_id_cached[$line->fk_product] = $line_id; + } else { // with batch management + if ($this->create_line_batch($line, $line->array_options) <= 0) { + $error++; + } } - } else { // with batch management - if ($this->create_line_batch($this->lines[$i], $this->lines[$i]->array_options) <= 0) { - $error++; + } else { + $line_id = $kits_id_cached[$line->fk_product]; + } + + // virtual products + if (isset($kits_list[$line->fk_product])) { + $prods_arbo = $kits_list[$line->fk_product]['arbo']; + $total_qty = $kits_list[$line->fk_product]['total_qty']; + + // get all children of virtual product + $parent_line_id = $line_id; // parent line created + $level_last = 1; + $product_child_id = 0; + foreach ($prods_arbo as $index => $product_child_arr) { + // 'id' => Id product + // 'id_parent' => Id parent product + // 'ref' => Ref product + // 'nb' => Nb of units that compose parent product + // 'nb_total' => // Nb of units for all nb of product + // 'stock' => Stock + // 'stock_alert' => Stock alert + // 'label' => Label + // 'fullpath' => // Full path label + // 'type' => + // 'desiredstock' => Desired stock + // 'level' => Level + // 'incdec' => Need to be incremented or decremented + // 'entity' => Entity + $product_child_level = (int) $product_child_arr['level']; + $product_child_incdec = !empty($product_child_arr['incdec']); + + // detect new level + if ($product_child_level != $level_last) { + $parent_line_id = $line_id; // last line id + $parent_product_id = $product_child_id; // last line id + if (isset($kits_id_cached[$parent_product_id])) { + $parent_line_id = $kits_id_cached[$parent_product_id]; + } else { + $kits_id_cached[$parent_product_id] = $parent_line_id; + } + } + + // determine if it's a kit : check next level + $is_kit = false; + $next_level = $product_child_level; + $next_index = $index + 1; + if (isset($prods_arbo[$next_index])) { + $next_level = (int) $prods_arbo[$next_index]['level']; + } + if ($next_level > $product_child_level) { + $is_kit = true; + } + + // determine quantity of sub-product + $product_child_id = (int) $product_child_arr['id']; + $qty = $line->qty; // by default + $warehouse_id = $line->entrepot_id; // by default + if ($is_kit || !$product_child_incdec) { + if ($is_kit) { + $qty = $total_qty; // insert only one line in expeditiondet table and use "total_qty" + } else { + $qty = 0; + } + $warehouse_id = 0; // no warehouse used for a kit or if stock is not managed (empty incdec) + } + $product_child_qty = (float) $product_child_arr['nb'] * $qty; + + // create line for a child of virtual product + if (!isset($sub_kits_id_cached[$product_child_id]) || $warehouse_id > 0) { + $line_id = $this->create_line($warehouse_id, 0, $product_child_qty, $line->rang, $line->array_options, $parent_line_id, $product_child_id); + if ($line_id <= 0) { + $error++; + dol_syslog(__METHOD__ . ' : ' . $this->errorsToString(), LOG_ERR); + break; + } + + // if kit or not manage stock (empty incdec) + if (empty($warehouse_id)) { + $sub_kits_id_cached[$product_child_id] = $line_id; + } + } + + $level_last = $product_child_level; } } } @@ -477,9 +605,11 @@ public function create($user, $notrigger = 0) * @param int $qty Quantity * @param int $rang Rang * @param array $array_options extrafields array + * @param int $parent_line_id Id of parent line for virtual products + * @param int $product_id Id of product (child of virtual product) * @return int <0 if KO, line_id if OK */ - public function create_line($entrepot_id, $origin_line_id, $qty, $rang = 0, $array_options = null) + public function create_line($entrepot_id, $origin_line_id, $qty, $rang = 0, $array_options = null, $parent_line_id = 0, $product_id = 0) { //phpcs:enable global $user; @@ -488,10 +618,18 @@ public function create_line($entrepot_id, $origin_line_id, $qty, $rang = 0, $arr $expeditionline->fk_expedition = $this->id; $expeditionline->entrepot_id = $entrepot_id; $expeditionline->fk_origin_line = $origin_line_id; + $expeditionline->fk_parent = $parent_line_id; + $expeditionline->fk_product = $product_id; $expeditionline->qty = $qty; $expeditionline->rang = $rang; $expeditionline->array_options = $array_options; + if (!($expeditionline->fk_product > 0)) { + $order_line = new OrderLine($this->db); + $order_line->fetch($expeditionline->fk_origin_line); + $expeditionline->fk_product = $order_line->fk_product; + } + if (($lineId = $expeditionline->insert($user)) < 0) { $this->errors[] = $expeditionline->error; } @@ -516,11 +654,11 @@ public function create_line_batch($line_ext, $array_options = 0) $tab = $line_ext->detail_batch; // create stockLocation Qty array foreach ($tab as $detbatch) { - if (!empty($detbatch->entrepot_id)) { - if (empty($stockLocationQty[$detbatch->entrepot_id])) { - $stockLocationQty[$detbatch->entrepot_id] = 0; + if (!empty($detbatch->fk_warehouse)) { + if (empty($stockLocationQty[$detbatch->fk_warehouse])) { + $stockLocationQty[$detbatch->fk_warehouse] = 0; } - $stockLocationQty[$detbatch->entrepot_id] += $detbatch->qty; + $stockLocationQty[$detbatch->fk_warehouse] += $detbatch->qty; } } // create shipment lines @@ -531,7 +669,7 @@ public function create_line_batch($line_ext, $array_options = 0) } else { // create shipment batch lines for stockLocation foreach ($tab as $detbatch) { - if ($detbatch->entrepot_id == $stockLocation) { + if ($detbatch->fk_warehouse == $stockLocation) { if (!($detbatch->create($line_id) > 0)) { // Create an ExpeditionLineBatch $this->errors = $detbatch->errors; $error++; @@ -881,9 +1019,11 @@ public function create_delivery($user) * @param int $id Id of source line (order line) * @param int $qty Quantity * @param array $array_options extrafields array + * @param int $fk_product Id of product + * @param int $fk_parent Id of parent line * @return int <0 if KO, >0 if OK */ - public function addline($entrepot_id, $id, $qty, $array_options = 0) + public function addline($entrepot_id, $id, $qty, $array_options = 0, $fk_product = 0, $fk_parent = 0) { global $conf, $langs; @@ -893,6 +1033,8 @@ public function addline($entrepot_id, $id, $qty, $array_options = 0) $line->entrepot_id = $entrepot_id; $line->origin_line_id = $id; $line->fk_origin_line = $id; + $line->fk_parent = $fk_parent; + $line->fk_product = $fk_product; $line->qty = $qty; $orderline = new OrderLine($this->db); @@ -901,10 +1043,11 @@ public function addline($entrepot_id, $id, $qty, $array_options = 0) // Copy the rang of the order line to the expedition line $line->rang = $orderline->rang; $line->product_type = $orderline->product_type; + if (!($line->fk_product > 0)) { + $line->fk_product = $orderline->fk_product; + } if (isModEnabled('stock') && !empty($orderline->fk_product)) { - $fk_product = $orderline->fk_product; - if (!($entrepot_id > 0) && empty($conf->global->STOCK_WAREHOUSE_NOT_REQUIRED_FOR_SHIPMENTS)) { $langs->load("errors"); $this->error = $langs->trans("ErrorWarehouseRequiredIntoShipmentLine"); @@ -913,7 +1056,7 @@ public function addline($entrepot_id, $id, $qty, $array_options = 0) if (!empty($conf->global->STOCK_MUST_BE_ENOUGH_FOR_SHIPMENT)) { $product = new Product($this->db); - $product->fetch($fk_product); + $product->fetch($line->fk_product); // Check must be done for stock of product into warehouse if $entrepot_id defined if ($entrepot_id > 0) { @@ -943,8 +1086,8 @@ public function addline($entrepot_id, $id, $qty, $array_options = 0) // If product need a batch number, we should not have called this function but addline_batch instead. // If this happen, we may have a bug in card.php page - if (isModEnabled('productbatch') && !empty($orderline->fk_product) && !empty($orderline->product_tobatch)) { - $this->error = 'ADDLINE_WAS_CALLED_INSTEAD_OF_ADDLINEBATCH '.$orderline->id.' '.$orderline->fk_product; // + if (isModEnabled('productbatch') && !empty($line->fk_product) && !empty($orderline->product_tobatch)) { + $this->error = 'ADDLINE_WAS_CALLED_INSTEAD_OF_ADDLINEBATCH '.$orderline->id.' '.$line->fk_product; // return -4; } @@ -964,9 +1107,10 @@ public function addline($entrepot_id, $id, $qty, $array_options = 0) * * @param array $dbatch Array of value (key 'detail' -> Array, key 'qty' total quantity for line, key ix_l : original line index) * @param array $array_options extrafields array + * @param Object $origin_line Origin line (only from OrderLine at this moment) * @return int <0 if KO, >0 if OK */ - public function addline_batch($dbatch, $array_options = 0) + public function addline_batch($dbatch, $array_options = 0, $origin_line = null) { // phpcs:enable global $conf, $langs; @@ -1015,6 +1159,12 @@ public function addline_batch($dbatch, $array_options = 0) $line->fk_origin_line = $dbatch['ix_l']; $line->qty = $dbatch['qty']; $line->detail_batch = $tab; + if (!($line->rang > 0)) { + $line->rang = $origin_line->rang; + } + if (!($line->fk_product > 0)) { + $line->fk_product = $origin_line->fk_product; + } // extrafields if (empty($conf->global->MAIN_EXTRAFIELDS_DISABLED) && is_array($array_options) && count($array_options) > 0) { // For avoid conflicts if trigger used @@ -1198,19 +1348,26 @@ public function cancel($notrigger = 0, $also_update_stock = false) } // Stock control - if (!$error && isModEnabled('stock') && + $can_update_stock = isModEnabled('stock') && (($conf->global->STOCK_CALCULATE_ON_SHIPMENT && $this->statut > self::STATUS_DRAFT) || - ($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE && $this->statut == self::STATUS_CLOSED && $also_update_stock))) { + ($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE && $this->statut == self::STATUS_CLOSED && $also_update_stock)); + if (!$error) { require_once DOL_DOCUMENT_ROOT."/product/stock/class/mouvementstock.class.php"; $langs->load("agenda"); - // Loop on each product line to add a stock movement and delete features - $sql = "SELECT cd.fk_product, cd.subprice, ed.qty, ed.fk_entrepot, ed.rowid as expeditiondet_id"; - $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd,"; - $sql .= " ".MAIN_DB_PREFIX."expeditiondet as ed"; + // Loop on each product line to add a stock movement (contain sub-products) + $sql = "SELECT "; + $sql .= " ed.fk_product"; + $sql .= ", ed.qty, ed.fk_entrepot, ed.rowid as expeditiondet_id"; + $sql .= ", SUM(".$this->db->ifsql("pa.rowid IS NOT NULL", 1, 0).") as iskit"; + $sql .= ", ".$this->db->ifsql("pai.incdec IS NULL", 1, "pai.incdec")." as incdec"; + $sql .= " FROM ".$this->db->prefix()."expeditiondet as ed"; + $sql .= " LEFT JOIN ".$this->db->prefix()."product_association as pa ON pa.fk_product_pere = ed.fk_product"; + $sql .= " LEFT JOIN ".$this->db->prefix()."expeditiondet as edp ON edp.rowid = ed.fk_parent"; + $sql .= " LEFT JOIN ".$this->db->prefix()."product_association as pai ON pai.fk_product_pere = edp.fk_product AND pai.fk_product_fils = ed.fk_product"; $sql .= " WHERE ed.fk_expedition = ".((int) $this->id); - $sql .= " AND cd.rowid = ed.fk_origin_line"; + $sql .= " GROUP BY ed.fk_product, ed.qty, ed.fk_entrepot, ed.rowid, pai.incdec"; dol_syslog(get_class($this)."::delete select details", LOG_DEBUG); $resql = $this->db->query($sql); @@ -1222,130 +1379,136 @@ public function cancel($notrigger = 0, $also_update_stock = false) for ($i = 0; $i < $cpt; $i++) { dol_syslog(get_class($this)."::delete movement index ".$i); $obj = $this->db->fetch_object($resql); + $line_id = (int) $obj->expeditiondet_id; - $mouvS = new MouvementStock($this->db); - // we do not log origin because it will be deleted - $mouvS->origin = null; - // get lot/serial - $lotArray = null; - if (isModEnabled('productbatch')) { - $lotArray = $shipmentlinebatch->fetchAll($obj->expeditiondet_id); - if (!is_array($lotArray)) { - $error++; - $this->errors[] = "Error ".$this->db->lasterror(); + if ($can_update_stock && empty($obj->iskit) && !empty($obj->incdec)) { + $mouvS = new MouvementStock($this->db); + // we do not log origin because it will be deleted + $mouvS->origin = null; + // get lot/serial + $lotArray = null; + if (isModEnabled('productbatch')) { + $lotArray = $shipmentlinebatch->fetchAll($obj->expeditiondet_id); + if (!is_array($lotArray)) { + $error++; + $this->errors[] = "Error " . $this->db->lasterror(); + } } - } - if (empty($lotArray)) { - // no lot/serial - // We increment stock of product (and sub-products) - // We use warehouse selected for each line - $result = $mouvS->reception($user, $obj->fk_product, $obj->fk_entrepot, $obj->qty, 0, $langs->trans("ShipmentCanceledInDolibarr", $this->ref)); // Price is set to 0, because we don't want to see WAP changed - if ($result < 0) { - $error++; - $this->errors = array_merge($this->errors, $mouvS->errors); - break; - } - } else { - // We increment stock of batches - // We use warehouse selected for each line - foreach ($lotArray as $lot) { - $result = $mouvS->reception($user, $obj->fk_product, $obj->fk_entrepot, $lot->qty, 0, $langs->trans("ShipmentCanceledInDolibarr", $this->ref), $lot->eatby, $lot->sellby, $lot->batch); // Price is set to 0, because we don't want to see WAP changed + if (empty($lotArray)) { + // no lot/serial + // We increment stock of product (and sub-products) + // We use warehouse selected for each line + $result = $mouvS->reception($user, $obj->fk_product, $obj->fk_entrepot, $obj->qty, 0, $langs->trans("ShipmentCanceledInDolibarr", $this->ref), '', '', '', '', 0, '', 0, 1); // Price is set to 0, because we don't want to see WAP changed if ($result < 0) { $error++; $this->errors = array_merge($this->errors, $mouvS->errors); break; } + } else { + // We increment stock of batches + // We use warehouse selected for each line + foreach ($lotArray as $lot) { + $result = $mouvS->reception($user, $obj->fk_product, $obj->fk_entrepot, $lot->qty, 0, $langs->trans("ShipmentCanceledInDolibarr", $this->ref), $lot->eatby, $lot->sellby, $lot->batch, '', 0, '', 0, 1); // Price is set to 0, because we don't want to see WAP changed + if ($result < 0) { + $error++; + $this->errors = array_merge($this->errors, $mouvS->errors); + break; + } + } + if ($error) { + break; // break for loop in case of error + } } - if ($error) { - break; // break for loop incase of error + } + + if (!$error) { + // delete all children and batches of this shipment line + $shipment_line = new ExpeditionLigne($this->db); + $res = $shipment_line->fetch($line_id); + if ($res > 0) { + $result = $shipment_line->delete($user); + if ($result < 0) { + $error++; + $this->errors[] = "Error ".$shipment_line->errorsToString(); + } + } else { + $error++; + $this->errors[] = "Error ".$shipment_line->errorsToString(); } } + + if ($error) { + break; + } } } else { - $error++; $this->errors[] = "Error ".$this->db->lasterror(); - } - } - - // delete batch expedition line - if (!$error && isModEnabled('productbatch')) { - $shipmentlinebatch = new ExpeditionLineBatch($this->db); - if ($shipmentlinebatch->deleteFromShipment($this->id) < 0) { - $error++; $this->errors[] = "Error ".$this->db->lasterror(); + $error++; + $this->errors[] = "Error ".$this->db->lasterror(); } } - if (!$error) { - $sql = "DELETE FROM ".MAIN_DB_PREFIX."expeditiondet"; - $sql .= " WHERE fk_expedition = ".((int) $this->id); - - if ($this->db->query($sql)) { - // Delete linked object - $res = $this->deleteObjectLinked(); - if ($res < 0) { - $error++; - } + // Delete linked object + $res = $this->deleteObjectLinked(); + if ($res < 0) { + $error++; + } - // No delete expedition - if (!$error) { - $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."expedition"; - $sql .= " WHERE rowid = ".((int) $this->id); - - if ($this->db->query($sql)) { - if (!empty($this->origin) && $this->origin_id > 0) { - $this->fetch_origin(); - $origin = $this->origin; - if ($this->$origin->statut == Commande::STATUS_SHIPMENTONPROCESS) { // If order source of shipment is "shipment in progress" - // Check if there is no more shipment. If not, we can move back status of order to "validated" instead of "shipment in progress" - $this->$origin->loadExpeditions(); - //var_dump($this->$origin->expeditions);exit; - if (count($this->$origin->expeditions) <= 0) { - $this->$origin->setStatut(Commande::STATUS_VALIDATED); - } + // No delete expedition + if (!$error) { + $sql = "SELECT rowid FROM ".$this->db->prefix()."expedition"; + $sql .= " WHERE rowid = ".((int) $this->id); + + if ($this->db->query($sql)) { + if (!empty($this->origin) && $this->origin_id > 0) { + $this->fetch_origin(); + $origin = $this->origin; + if ($this->$origin->statut == Commande::STATUS_SHIPMENTONPROCESS) { // If order source of shipment is "shipment in progress" + // Check if there is no more shipment. If not, we can move back status of order to "validated" instead of "shipment in progress" + $this->$origin->loadExpeditions(); + //var_dump($this->$origin->expeditions);exit; + if (count($this->$origin->expeditions) <= 0) { + $this->$origin->setStatut(Commande::STATUS_VALIDATED); } } + } - if (!$error) { - $this->db->commit(); - - // We delete PDFs - $ref = dol_sanitizeFileName($this->ref); - if (!empty($conf->expedition->dir_output)) { - $dir = $conf->expedition->dir_output.'/sending/'.$ref; - $file = $dir.'/'.$ref.'.pdf'; - if (file_exists($file)) { - if (!dol_delete_file($file)) { - return 0; - } + if (!$error) { + $this->db->commit(); + + // We delete PDFs + $ref = dol_sanitizeFileName($this->ref); + if (!empty($conf->expedition->dir_output)) { + $dir = $conf->expedition->dir_output.'/sending/'.$ref; + $file = $dir.'/'.$ref.'.pdf'; + if (file_exists($file)) { + if (!dol_delete_file($file)) { + return 0; } - if (file_exists($dir)) { - if (!dol_delete_dir_recursive($dir)) { - $this->error = $langs->trans("ErrorCanNotDeleteDir", $dir); - return 0; - } + } + if (file_exists($dir)) { + if (!dol_delete_dir_recursive($dir)) { + $this->error = $langs->trans("ErrorCanNotDeleteDir", $dir); + return 0; } } - - return 1; - } else { - $this->db->rollback(); - return -1; } + + return 1; } else { - $this->error = $this->db->lasterror()." - sql=$sql"; $this->db->rollback(); - return -3; + return -1; } } else { $this->error = $this->db->lasterror()." - sql=$sql"; $this->db->rollback(); - return -2; - }//*/ + return -3; + } } else { $this->error = $this->db->lasterror()." - sql=$sql"; $this->db->rollback(); - return -1; + return -2; } } else { $this->db->rollback(); @@ -1389,9 +1552,10 @@ public function delete($notrigger = 0, $also_update_stock = false) } // Stock control - if (!$error && isModEnabled('stock') && + $can_update_stock = isModEnabled('stock') && (($conf->global->STOCK_CALCULATE_ON_SHIPMENT && $this->statut > self::STATUS_DRAFT) || - ($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE && $this->statut == self::STATUS_CLOSED && $also_update_stock))) { + ($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE && $this->statut == self::STATUS_CLOSED && $also_update_stock)); + if (!$error) { require_once DOL_DOCUMENT_ROOT."/product/stock/class/mouvementstock.class.php"; $langs->load("agenda"); @@ -1399,12 +1563,18 @@ public function delete($notrigger = 0, $also_update_stock = false) // we try deletion of batch line even if module batch not enabled in case of the module were enabled and disabled previously $shipmentlinebatch = new ExpeditionLineBatch($this->db); - // Loop on each product line to add a stock movement - $sql = "SELECT cd.fk_product, cd.subprice, ed.qty, ed.fk_entrepot, ed.rowid as expeditiondet_id"; - $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd,"; - $sql .= " ".MAIN_DB_PREFIX."expeditiondet as ed"; + // Loop on each product line to add a stock movement (contain sub-products) + $sql = "SELECT "; + $sql .= " ed.fk_product"; + $sql .= ", ed.qty, ed.fk_entrepot, ed.rowid as expeditiondet_id"; + $sql .= ", SUM(".$this->db->ifsql("pa.rowid IS NOT NULL", 1, 0).") as iskit"; + $sql .= ", ".$this->db->ifsql("pai.incdec IS NULL", 1, "pai.incdec")." as incdec"; + $sql .= " FROM ".$this->db->prefix()."expeditiondet as ed"; + $sql .= " LEFT JOIN ".$this->db->prefix()."product_association as pa ON pa.fk_product_pere = ed.fk_product"; + $sql .= " LEFT JOIN ".$this->db->prefix()."expeditiondet as edp ON edp.rowid = ed.fk_parent"; + $sql .= " LEFT JOIN ".$this->db->prefix()."product_association as pai ON pai.fk_product_pere = edp.fk_product AND pai.fk_product_fils = ed.fk_product"; $sql .= " WHERE ed.fk_expedition = ".((int) $this->id); - $sql .= " AND cd.rowid = ed.fk_origin_line"; + $sql .= " GROUP BY ed.fk_product, ed.qty, ed.fk_entrepot, ed.rowid, pai.incdec"; dol_syslog(get_class($this)."::delete select details", LOG_DEBUG); $resql = $this->db->query($sql); @@ -1413,136 +1583,141 @@ public function delete($notrigger = 0, $also_update_stock = false) for ($i = 0; $i < $cpt; $i++) { dol_syslog(get_class($this)."::delete movement index ".$i); $obj = $this->db->fetch_object($resql); + $line_id = (int) $obj->expeditiondet_id; - $mouvS = new MouvementStock($this->db); - // we do not log origin because it will be deleted - $mouvS->origin = null; - // get lot/serial - $lotArray = $shipmentlinebatch->fetchAll($obj->expeditiondet_id); - if (!is_array($lotArray)) { - $error++; $this->errors[] = "Error ".$this->db->lasterror(); - } - if (empty($lotArray)) { - // no lot/serial - // We increment stock of product (and sub-products) - // We use warehouse selected for each line - $result = $mouvS->reception($user, $obj->fk_product, $obj->fk_entrepot, $obj->qty, 0, $langs->trans("ShipmentDeletedInDolibarr", $this->ref)); // Price is set to 0, because we don't want to see WAP changed - if ($result < 0) { + if ($can_update_stock && empty($obj->iskit) && !empty($obj->incdec)) { + $mouvS = new MouvementStock($this->db); + // we do not log origin because it will be deleted + $mouvS->origin = null; + // get lot/serial + $lotArray = $shipmentlinebatch->fetchAll($line_id); + if (!is_array($lotArray)) { $error++; - $this->errors = array_merge($this->errors, $mouvS->errors); - break; + $this->errors[] = "Error " . $this->db->lasterror(); } - } else { - // We increment stock of batches - // We use warehouse selected for each line - foreach ($lotArray as $lot) { - $result = $mouvS->reception($user, $obj->fk_product, $obj->fk_entrepot, $lot->qty, 0, $langs->trans("ShipmentDeletedInDolibarr", $this->ref), $lot->eatby, $lot->sellby, $lot->batch); // Price is set to 0, because we don't want to see WAP changed + if (empty($lotArray)) { + // no lot/serial + // We increment stock of product (disable for sub-products : already in shipment lines) + // We use warehouse selected for each line + $result = $mouvS->reception($user, $obj->fk_product, $obj->fk_entrepot, $obj->qty, 0, $langs->trans("ShipmentDeletedInDolibarr", $this->ref), '', '', '', '', 0, '', 0, 1); // Price is set to 0, because we don't want to see WAP changed if ($result < 0) { $error++; $this->errors = array_merge($this->errors, $mouvS->errors); break; } + } else { + // We increment stock of batches + // We use warehouse selected for each line + foreach ($lotArray as $lot) { + $result = $mouvS->reception($user, $obj->fk_product, $obj->fk_entrepot, $lot->qty, 0, $langs->trans("ShipmentDeletedInDolibarr", $this->ref), $lot->eatby, $lot->sellby, $lot->batch, '', 0, '', 0, 1); // Price is set to 0, because we don't want to see WAP changed + if ($result < 0) { + $error++; + $this->errors = array_merge($this->errors, $mouvS->errors); + break; + } + } + if ($error) { + break; // break for loop in case of error + } } - if ($error) { - break; // break for loop incase of error + } + + if (!$error) { + // delete all children and batches of this shipment line + $shipment_line = new ExpeditionLigne($this->db); + $res = $shipment_line->fetch($line_id); + if ($res > 0) { + $result = $shipment_line->delete($user); + if ($result < 0) { + $error++; + $this->errors[] = "Error ".$shipment_line->errorsToString(); + } + } else { + $error++; + $this->errors[] = "Error ".$shipment_line->errorsToString(); } } + + if ($error) { + break; + } } } else { - $error++; $this->errors[] = "Error ".$this->db->lasterror(); + $error++; + $this->errors[] = "Error ".$this->db->lasterror(); } } - // delete batch expedition line if (!$error) { - $shipmentlinebatch = new ExpeditionLineBatch($this->db); - if ($shipmentlinebatch->deleteFromShipment($this->id) < 0) { - $error++; $this->errors[] = "Error ".$this->db->lasterror(); + // Delete linked object + $res = $this->deleteObjectLinked(); + if ($res < 0) { + $error++; } - } - - if (!$error) { - $main = MAIN_DB_PREFIX.'expeditiondet'; - $ef = $main."_extrafields"; - $sqlef = "DELETE FROM $ef WHERE fk_object IN (SELECT rowid FROM $main WHERE fk_expedition = ".((int) $this->id).")"; - - $sql = "DELETE FROM ".MAIN_DB_PREFIX."expeditiondet"; - $sql .= " WHERE fk_expedition = ".((int) $this->id); - if ($this->db->query($sqlef) && $this->db->query($sql)) { - // Delete linked object - $res = $this->deleteObjectLinked(); - if ($res < 0) { - $error++; - } - - // delete extrafields - $res = $this->deleteExtraFields(); - if ($res < 0) { - $error++; - } + // delete extrafields + $res = $this->deleteExtraFields(); + if ($res < 0) { + $error++; + } - if (!$error) { - $sql = "DELETE FROM ".MAIN_DB_PREFIX."expedition"; - $sql .= " WHERE rowid = ".((int) $this->id); - - if ($this->db->query($sql)) { - if (!empty($this->origin) && $this->origin_id > 0) { - $this->fetch_origin(); - $origin = $this->origin; - if ($this->$origin->statut == Commande::STATUS_SHIPMENTONPROCESS) { // If order source of shipment is "shipment in progress" - // Check if there is no more shipment. If not, we can move back status of order to "validated" instead of "shipment in progress" - $this->$origin->loadExpeditions(); - //var_dump($this->$origin->expeditions);exit; - if (count($this->$origin->expeditions) <= 0) { - $this->$origin->setStatut(Commande::STATUS_VALIDATED); - } + if (!$error) { + $sql = "DELETE FROM ".$this->db->prefix()."expedition"; + $sql .= " WHERE rowid = ".((int) $this->id); + + if ($this->db->query($sql)) { + if (!empty($this->origin) && $this->origin_id > 0) { + $this->fetch_origin(); + $origin = $this->origin; + if ($this->$origin->statut == Commande::STATUS_SHIPMENTONPROCESS) { // If order source of shipment is "shipment in progress" + // Check if there is no more shipment. If not, we can move back status of order to "validated" instead of "shipment in progress" + $this->$origin->loadExpeditions(); + //var_dump($this->$origin->expeditions);exit; + if (count($this->$origin->expeditions) <= 0) { + $this->$origin->setStatut(Commande::STATUS_VALIDATED); } } + } - if (!$error) { - $this->db->commit(); - - // Delete record into ECM index (Note that delete is also done when deleting files with the dol_delete_dir_recursive - $this->deleteEcmFiles(); + if (!$error) { + $this->db->commit(); - // We delete PDFs - $ref = dol_sanitizeFileName($this->ref); - if (!empty($conf->expedition->dir_output)) { - $dir = $conf->expedition->dir_output.'/sending/'.$ref; - $file = $dir.'/'.$ref.'.pdf'; - if (file_exists($file)) { - if (!dol_delete_file($file)) { - return 0; - } + // Delete record into ECM index (Note that delete is also done when deleting files with the dol_delete_dir_recursive + $this->deleteEcmFiles(0); // Deleting files physically is done later with the dol_delete_dir_recursive + $this->deleteEcmFiles(1); // Deleting files physically is done later with the dol_delete_dir_recursive + + // We delete PDFs + $ref = dol_sanitizeFileName($this->ref); + if (!empty($conf->expedition->dir_output)) { + $dir = $conf->expedition->dir_output . '/sending/' . $ref; + $file = $dir . '/' . $ref . '.pdf'; + if (file_exists($file)) { + if (!dol_delete_file($file)) { + return 0; } - if (file_exists($dir)) { - if (!dol_delete_dir_recursive($dir)) { - $this->error = $langs->trans("ErrorCanNotDeleteDir", $dir); - return 0; - } + } + if (file_exists($dir)) { + if (!dol_delete_dir_recursive($dir)) { + $this->error = $langs->trans("ErrorCanNotDeleteDir", $dir); + return 0; } } - - return 1; - } else { - $this->db->rollback(); - return -1; } + + return 1; } else { - $this->error = $this->db->lasterror()." - sql=$sql"; $this->db->rollback(); - return -3; + return -1; } } else { $this->error = $this->db->lasterror()." - sql=$sql"; $this->db->rollback(); - return -2; + return -3; } } else { $this->error = $this->db->lasterror()." - sql=$sql"; $this->db->rollback(); - return -1; + return -2; } } else { $this->db->rollback(); @@ -1714,6 +1889,35 @@ public function fetch_lines() } } + // virtual product : find all children stock (group by product id and warehouse id) + if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) { + $detail_children = array(); // detail by product : array of [warehouse_id => total_qty] + $line_child_list = array(); + $res = $line->findAllChild($line->id, $line_child_list, 1); + if ($res > 0) { + if (!empty($line_child_list)) { + foreach ($line_child_list as $child_line) { + foreach ($child_line as $child_obj) { + $child_product_id = (int) $child_obj->fk_product; + $child_warehouse_id = (int) $child_obj->fk_warehouse; + + if ($child_warehouse_id > 0) { + // child quantities group by warehouses + if (!isset($detail_children[$child_product_id])) { + $detail_children[$child_product_id] = array(); + } + if (!isset($detail_children[$child_product_id][$child_warehouse_id])) { + $detail_children[$child_product_id][$child_warehouse_id] = 0; + } + $detail_children[$child_product_id][$child_warehouse_id] += $child_obj->qty; + } + } + } + } + } + $line->detail_children = $detail_children; + } + $line->fetch_optionals(); if ($originline != $obj->fk_origin_line) { @@ -2227,17 +2431,17 @@ private function manageStockMvtOnEvt($user, $labelmovement = 'ShipmentClassifyCl $langs->load("agenda"); // Loop on each product line to add a stock movement - $sql = "SELECT cd.fk_product, cd.subprice,"; - $sql .= " ed.rowid, ed.qty, ed.fk_entrepot,"; - $sql .= " e.ref,"; - $sql .= " edb.rowid as edbrowid, edb.eatby, edb.sellby, edb.batch, edb.qty as edbqty, edb.fk_origin_stock,"; - $sql .= " cd.rowid as cdid, ed.rowid as edid"; - $sql .= " FROM " . MAIN_DB_PREFIX . "commandedet as cd,"; - $sql .= " " . MAIN_DB_PREFIX . "expeditiondet as ed"; - $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "expeditiondet_batch as edb on edb.fk_expeditiondet = ed.rowid"; - $sql .= " INNER JOIN " . MAIN_DB_PREFIX . "expedition as e ON ed.fk_expedition = e.rowid"; + $sql = "SELECT"; + $sql .= " ed.rowid as edid, ed.fk_product, ed.qty, ed.fk_entrepot"; + $sql .= ", cd.rowid as cdid"; + $sql .= ", cd.subprice"; + $sql .= ", edb.rowid as edbrowid, edb.eatby, edb.sellby, edb.batch, edb.qty as edbqty, edb.fk_origin_stock"; + $sql .= ", e.ref"; + $sql .= " FROM " . $this->db->prefix() . "expeditiondet as ed"; + $sql .= " LEFT JOIN " . $this->db->prefix() . "commandedet as cd ON cd.rowid = ed.fk_origin_line"; + $sql .= " LEFT JOIN " . $this->db->prefix() . "expeditiondet_batch as edb on edb.fk_expeditiondet = ed.rowid"; + $sql .= " INNER JOIN " . $this->db->prefix() . "expedition as e ON ed.fk_expedition = e.rowid"; $sql .= " WHERE ed.fk_expedition = " . ((int) $this->id); - $sql .= " AND cd.rowid = ed.fk_origin_line"; dol_syslog(get_class($this) . "::valid select details", LOG_DEBUG); $resql = $this->db->query($sql); @@ -2253,7 +2457,7 @@ private function manageStockMvtOnEvt($user, $labelmovement = 'ShipmentClassifyCl if ($qty <= 0 || ($qty < 0 && !getDolGlobalInt('SHIPMENT_ALLOW_NEGATIVE_QTY'))) { continue; } - dol_syslog(get_class($this) . "::valid movement index " . $i . " ed.rowid=" . $obj->rowid . " edb.rowid=" . $obj->edbrowid); + dol_syslog(get_class($this) . "::valid movement index " . $i . " ed.rowid=" . $obj->edid . " edb.rowid=" . $obj->edbrowid); $mouvS = new MouvementStock($this->db); $mouvS->origin = &$this; @@ -2285,7 +2489,7 @@ private function manageStockMvtOnEvt($user, $labelmovement = 'ShipmentClassifyCl // If some stock lines are 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 // having a lot1/qty=X and lot2/qty=-X, so 0 but we must not loose repartition of different lot. - $sqldelete = "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)"; + $sqldelete = "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)"; $resqldelete = $this->db->query($sqldelete); // We do not test error, it can fails if there is child in batch details } @@ -2560,6 +2764,11 @@ class ExpeditionLigne extends CommonObjectLine */ public $origin_line_id; + /** + * @var int Id of parent line for children of virtual product + */ + public $fk_parent; + /** * Code of object line that is origin of the shipment line. * @@ -2601,6 +2810,9 @@ class ExpeditionLigne extends CommonObjectLine // We can use this to know warehouse planned to be used for each lot. public $detail_batch; + // virtual products : array of total of quantities group product id and warehouse id + public $detail_children; + // detail of warehouses and qty // We can use this to know warehouse when there is no lot. public $details_entrepot; @@ -2771,7 +2983,10 @@ public function insert($user, $notrigger = 0) $error = 0; // Check parameters - if (empty($this->fk_expedition) || empty($this->fk_origin_line) || !is_numeric($this->qty)) { + if (empty($this->fk_expedition) + || empty($this->fk_product) // product id is mandatory + || (empty($this->fk_origin_line) && empty($this->fk_parent)) // at least origin line id of parent line id is set + || !is_numeric($this->qty)) { $this->error = 'ErrorMandatoryParametersNotProvided'; return -1; } @@ -2793,12 +3008,16 @@ public function insert($user, $notrigger = 0) $sql .= "fk_expedition"; $sql .= ", fk_entrepot"; $sql .= ", fk_origin_line"; + $sql .= ", fk_parent"; + $sql .= ", fk_product"; $sql .= ", qty"; $sql .= ", rang"; $sql .= ") VALUES ("; $sql .= $this->fk_expedition; $sql .= ", ".(empty($this->entrepot_id) ? 'NULL' : $this->entrepot_id); - $sql .= ", ".((int) $this->fk_origin_line); + $sql .= ", ".(empty($this->fk_origin_line) ? 'NULL' : $this->fk_origin_line); + $sql .= ", ".(empty($this->fk_parent) ? 'NULL' : $this->fk_parent); + $sql .= ", ".(empty($this->fk_product) ? 'NULL' : $this->fk_product); $sql .= ", ".price2num($this->qty, 'MS'); $sql .= ", ".((int) $ranktouse); $sql .= ")"; @@ -2826,7 +3045,7 @@ public function insert($user, $notrigger = 0) if ($error) { foreach ($this->errors as $errmsg) { - dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR); + dol_syslog(__METHOD__.' '.$errmsg, LOG_ERR); $this->error .= ($this->error ? ', '.$errmsg : $errmsg); } } @@ -2843,12 +3062,75 @@ public function insert($user, $notrigger = 0) } } + /** + * Find all children + * + * @param int $line_id Line id + * @param array $list List of sub-lines for a virtual product line + * @param int $mode [=0] array of lines ids, 1 array of line object for dispatcher + * @return int Return integer <0 if KO else >0 if OK + */ + public function findAllChild($line_id, &$list = array(), $mode = 0) + { + if ($line_id > 0) { + // find all child + $sql = "SELECT ed.rowid as child_line_id"; + if ($mode == 1) { + $sql .= ", ed.fk_product"; + $sql .= ", ed.fk_parent"; + $sql .= ", " . $this->db->ifsql('eb.rowid IS NULL', 'ed.qty', 'eb.qty') . " as qty"; + $sql .= ", " . $this->db->ifsql('eb.rowid IS NULL', 'ed.fk_entrepot', 'eb.fk_warehouse') . " as fk_warehouse"; + $sql .= ", eb.batch, eb.eatby, eb.sellby"; + } + $sql .= " FROM " . $this->db->prefix() . $this->table_element . " as ed"; + $sql .= " LEFT JOIN " . $this->db->prefix() . "expeditiondet_batch as eb ON eb.fk_expeditiondet = " . ((int) $line_id); + $sql .= " WHERE ed.fk_parent = " . ((int) $line_id); + $sql .= $this->db->order('ed.fk_product,ed.rowid', 'ASC,ASC'); + + $resql = $this->db->query($sql); + if ($resql) { + while ($obj = $this->db->fetch_object($resql)) { + $child_line_id = (int) $obj->child_line_id; + if (!isset($list[$line_id])) { + $list[$line_id] = array(); + } + + if ($mode == 0) { + $list[$line_id][] = $child_line_id; + } elseif ($mode == 1) { + $line_obj = new stdClass(); + $line_obj->rowid = $child_line_id; + $line_obj->fk_product = $obj->fk_product; + $line_obj->fk_parent = $obj->fk_parent; + $line_obj->qty = $obj->qty; + $line_obj->fk_warehouse = $obj->fk_warehouse; + $line_obj->batch = $obj->batch; + $line_obj->eatby = $obj->eatby; + $line_obj->sellby = $obj->sellby; + $line_obj->iskit = $obj->iskit; + $line_obj->incdec = $obj->incdec; + $list[$line_id][] = $line_obj; + } + + $this->findAllChild($child_line_id, $list, $mode); + } + $this->db->free($resql); + } else { + $this->error = $this->db->lasterror(); + $this->errors[] = $this->error; + dol_syslog(__METHOD__.' '.$this->error, LOG_ERR); + } + } + + return 1; + } + /** * Delete shipment line. * * @param User $user User that modify * @param int $notrigger 0=launch triggers after, 1=disable triggers - * @return int >0 if OK, <0 if KO + * @return int Return integer < 0 if KO, > 0 if OK */ public function delete($user = null, $notrigger = 0) { @@ -2856,41 +3138,81 @@ public function delete($user = null, $notrigger = 0) $this->db->begin(); - // delete batch expedition line - if (isModEnabled('productbatch')) { - $sql = "DELETE FROM ".MAIN_DB_PREFIX."expeditiondet_batch"; - $sql .= " WHERE fk_expeditiondet = ".((int) $this->id); + // virtual products : delete all children and batch + if (getDolGlobalInt('PRODUIT_SOUSPRODUITS') && !($this->fk_parent > 0)) { + // find all children + $result = $this->findAllChild($this->id, $line_id_list); + if ($result) { + $child_line_id_list = array_reverse($line_id_list, true); + foreach ($child_line_id_list as $child_line_id_arr) { + foreach ($child_line_id_arr as $child_line_id) { + // delete batch expedition line + if (isModEnabled('productbatch')) { + $sql = "DELETE FROM " . $this->db->prefix() . "expeditiondet_batch"; + $sql .= " WHERE fk_expeditiondet = " . ((int) $child_line_id); + if (!$this->db->query($sql)) { + $error++; + $this->errors[] = $this->db->lasterror() . " - sql=$sql"; + } + } + + $sql = "DELETE FROM " . $this->db->prefix() . "expeditiondet"; + $sql .= " WHERE rowid = " . ((int) $child_line_id); + if (!$this->db->query($sql)) { + $error++; + $this->errors[] = $this->db->lasterror() . " - sql=$sql"; + } - if (!$this->db->query($sql)) { - $this->errors[] = $this->db->lasterror()." - sql=$sql"; + if ($error) { + break; + } + } + if ($error) { + break; + } + } + } else { $error++; } } - $sql = "DELETE FROM ".MAIN_DB_PREFIX."expeditiondet"; - $sql .= " WHERE rowid = ".((int) $this->id); + if (!$error) { + // delete batch expedition line + if (isModEnabled('productbatch')) { + $sql = "DELETE FROM " . $this->db->prefix() . "expeditiondet_batch"; + $sql .= " WHERE fk_expeditiondet = " . ((int) $this->id); - if (!$error && $this->db->query($sql)) { - // Remove extrafields - if (!$error) { - $result = $this->deleteExtraFields(); - if ($result < 0) { - $this->errors[] = $this->error; + if (!$this->db->query($sql)) { + $this->errors[] = $this->db->lasterror() . " - sql=$sql"; $error++; } } - if (!$error && !$notrigger) { - // Call trigger - $result = $this->call_trigger('LINESHIPPING_DELETE', $user); - if ($result < 0) { - $this->errors[] = $this->error; - $error++; + + $sql = "DELETE FROM " . $this->db->prefix() . "expeditiondet"; + $sql .= " WHERE rowid = " . ((int) $this->id); + + if (!$error && $this->db->query($sql)) { + // Remove extrafields + if (!$error) { + $result = $this->deleteExtraFields(); + if ($result < 0) { + $this->errors[] = $this->error; + $error++; + } } - // End call triggers + if (!$error && !$notrigger) { + // Call trigger + $result = $this->call_trigger('LINESHIPPING_DELETE', $user); + if ($result < 0) { + $this->errors[] = $this->error; + $error++; + } + // End call triggers + } + } else { + $this->errors[] = $this->db->lasterror() . " - sql=$sql"; + $error++; } - } else { - $this->errors[] = $this->db->lasterror()." - sql=$sql"; - $error++; } if (!$error) { @@ -2898,8 +3220,8 @@ public function delete($user = null, $notrigger = 0) return 1; } else { foreach ($this->errors as $errmsg) { - dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR); - $this->error .= ($this->error ? ', '.$errmsg : $errmsg); + dol_syslog(get_class($this) . "::delete " . $errmsg, LOG_ERR); + $this->error .= ($this->error ? ', ' . $errmsg : $errmsg); } $this->db->rollback(); return -1 * $error; @@ -3019,7 +3341,7 @@ public function update($user = null, $notrigger = 0) $shipmentLot->batch = $lot->batch; $shipmentLot->eatby = $lot->eatby; $shipmentLot->sellby = $lot->sellby; - $shipmentLot->entrepot_id = $this->detail_batch->entrepot_id; + $shipmentLot->fk_warehouse = $this->detail_batch->entrepot_id; $shipmentLot->qty = $this->detail_batch->qty; $shipmentLot->fk_origin_stock = $batch_id; if ($shipmentLot->create($this->id) < 0) { diff --git a/htdocs/expedition/class/expeditionlinebatch.class.php b/htdocs/expedition/class/expeditionlinebatch.class.php index 73f352ac21a0c..60e36f09fcc15 100644 --- a/htdocs/expedition/class/expeditionlinebatch.class.php +++ b/htdocs/expedition/class/expeditionlinebatch.class.php @@ -43,7 +43,7 @@ class ExpeditionLineBatch extends CommonObject public $batch; public $qty; public $dluo_qty; // deprecated, use qty - public $entrepot_id; + public $entrepot_id; // @deprecated, use fk_warehouse public $fk_origin_stock; // rowid in llx_product_batch table public $fk_warehouse; // for future use in v19 public $fk_expeditiondet; @@ -88,7 +88,7 @@ public function fetchFromStock($id_stockdluo) $this->sellby = $this->db->jdate($obj->sellby); $this->eatby = $this->db->jdate($obj->eatby); $this->batch = $obj->batch; - $this->entrepot_id = $obj->fk_entrepot; + $this->fk_warehouse = $obj->fk_entrepot; $this->fk_origin_stock = (int) $id_stockdluo; } $this->db->free($resql); diff --git a/htdocs/expedition/dispatch.php b/htdocs/expedition/dispatch.php index bf7b23b21d324..874a4d0e72f1a 100644 --- a/htdocs/expedition/dispatch.php +++ b/htdocs/expedition/dispatch.php @@ -130,32 +130,35 @@ foreach ($_POST as $key => $value) { // without batch module enabled $reg = array(); - if (preg_match('/^product_.*([0-9]+)_([0-9]+)$/i', $key, $reg)) { + if (preg_match('/^product([0-9]+)_([0-9]+)_([0-9]+)$/i', $key, $reg)) { $pos++; - if (preg_match('/^product_([0-9]+)_([0-9]+)$/i', $key, $reg)) { + if (preg_match('/^product([0-9]+)_([0-9]+)_([0-9]+)$/i', $key, $reg)) { $modebatch = "barcode"; - } elseif (preg_match('/^product_batch_([0-9]+)_([0-9]+)$/i', $key, $reg)) { // With batchmode enabled + } elseif (preg_match('/^productbatch([0-9]+)_([0-9]+)_([0-9]+)$/i', $key, $reg)) { // With batchmode enabled $modebatch = "batch"; } $numline = $pos; + $dispatch_line_suffix = $reg[1].'_'.$reg[2].'_'.$reg[3]; if ($modebatch == "barcode") { - $prod = "product_".$reg[1].'_'.$reg[2]; + $prod = "product".$dispatch_line_suffix; } else { - $prod = 'product_batch_'.$reg[1].'_'.$reg[2]; + $prod = 'productbatch'.$dispatch_line_suffix; } - $qty = "qty_".$reg[1].'_'.$reg[2]; - $ent = "entrepot_".$reg[1].'_'.$reg[2]; - $fk_commandedet = "fk_commandedet_".$reg[1].'_'.$reg[2]; - $idline = GETPOST("idline_".$reg[1].'_'.$reg[2]); - $pu = "pu_".$reg[1].'_'.$reg[2]; // This is unit price including discount + $qty = "qty".$dispatch_line_suffix; + $ent = "entrepot".$dispatch_line_suffix; + $fk_commandedet = "fk_commandedet".$dispatch_line_suffix; + $idline = GETPOSTINT("idline".$dispatch_line_suffix); + $warehouse_id = GETPOSTINT($ent); + $prod_id = GETPOSTINT($prod); + //$pu = "pu_".$dispatch_line_suffix; // This is unit price including discount $lot = ''; $dDLUO = ''; $dDLC = ''; if ($modebatch == "batch") { //TODO: Make impossible to input non existing batch code - $lot = GETPOST('lot_number_'.$reg[1].'_'.$reg[2]); - $dDLUO = dol_mktime(12, 0, 0, GETPOST('dluo_'.$reg[1].'_'.$reg[2].'month', 'int'), GETPOST('dluo_'.$reg[1].'_'.$reg[2].'day', 'int'), GETPOST('dluo_'.$reg[1].'_'.$reg[2].'year', 'int')); - $dDLC = dol_mktime(12, 0, 0, GETPOST('dlc_'.$reg[1].'_'.$reg[2].'month', 'int'), GETPOST('dlc_'.$reg[1].'_'.$reg[2].'day', 'int'), GETPOST('dlc_'.$reg[1].'_'.$reg[2].'year', 'int')); + $lot = GETPOST('lot_number'.$dispatch_line_suffix); + $dDLUO = dol_mktime(12, 0, 0, GETPOST('dluo'.$dispatch_line_suffix.'month', 'int'), GETPOST('dluo'.$dispatch_line_suffix.'day', 'int'), GETPOST('dluo'.$dispatch_line_suffix.'year', 'int')); + $dDLC = dol_mktime(12, 0, 0, GETPOST('dlc'.$dispatch_line_suffix.'month', 'int'), GETPOST('dlc'.$dispatch_line_suffix.'day', 'int'), GETPOST('dlc'.$dispatch_line_suffix.'year', 'int')); } $newqty = price2num(GETPOST($qty, 'alpha'), 'MS'); @@ -172,8 +175,8 @@ } if (!$error && $modebatch == "batch") { $sql = "SELECT pb.rowid "; - $sql .= " FROM ".MAIN_DB_PREFIX."product_batch as pb"; - $sql .= " JOIN ".MAIN_DB_PREFIX."product_stock as ps"; + $sql .= " FROM ".$db->prefix()."product_batch as pb"; + $sql .= " JOIN ".$db->prefix()."product_stock as ps"; $sql .= " ON ps.rowid = pb.fk_product_stock"; $sql .= " WHERE pb.batch = '".$db->escape($lot)."'"; $sql .= " AND ps.fk_product = ".((int) GETPOST($prod, 'int')) ; @@ -222,11 +225,11 @@ if (!$error && $modebatch == "batch") { if ($newqty > 0) { - $suffixkeyfordate = preg_replace('/^product_batch/', '', $key); + $suffixkeyfordate = preg_replace('/^productbatch/', '', $key); $sellby = dol_mktime(0, 0, 0, GETPOST('dlc'.$suffixkeyfordate.'month'), GETPOST('dlc'.$suffixkeyfordate.'day'), GETPOST('dlc'.$suffixkeyfordate.'year'), ''); $eatby = dol_mktime(0, 0, 0, GETPOST('dluo'.$suffixkeyfordate.'month'), GETPOST('dluo'.$suffixkeyfordate.'day'), GETPOST('dluo'.$suffixkeyfordate.'year')); - $sqlsearchdet = "SELECT rowid FROM ".MAIN_DB_PREFIX.$expeditionlinebatch->table_element; + $sqlsearchdet = "SELECT rowid FROM ".$db->prefix().$expeditionlinebatch->table_element; $sqlsearchdet .= " WHERE fk_expeditiondet = ".((int) $idline); $sqlsearchdet .= " AND batch = '".$db->escape($lot)."'"; $resqlsearchdet = $db->query($sqlsearchdet); @@ -238,21 +241,21 @@ } if ($objsearchdet) { - $sql = "UPDATE ".MAIN_DB_PREFIX.$expeditionlinebatch->table_element." SET"; + $sql = "UPDATE ".$db->prefix().$expeditionlinebatch->table_element." SET"; $sql .= " eatby = ".($eatby ? "'".$db->idate($eatby)."'" : "null"); $sql .= " , sellby = ".($sellby ? "'".$db->idate($sellby)."'" : "null"); $sql .= " , qty = ".((float) $newqty); // TODO Add a column fk_warehouse $sql .= " WHERE rowid = ".((int) $objsearchdet->rowid); } else { - $sql = "INSERT INTO ".MAIN_DB_PREFIX.$expeditionlinebatch->table_element." ("; + $sql = "INSERT INTO ".$db->prefix().$expeditionlinebatch->table_element." ("; $sql .= "fk_expeditiondet, eatby, sellby, batch, qty, fk_origin_stock)"; // TODO Add a column fk_warehouse $sql .= " VALUES (".((int) $idline).", ".($eatby ? "'".$db->idate($eatby)."'" : "null").", ".($sellby ? "'".$db->idate($sellby)."'" : "null").", "; $sql .= " '".$db->escape($lot)."', ".((float) $newqty).", 0)"; } } else { - $sql = " DELETE FROM ".MAIN_DB_PREFIX.$expeditionlinebatch->table_element; + $sql = " DELETE FROM ".$db->prefix().$expeditionlinebatch->table_element; $sql .= " WHERE fk_expeditiondet = ".((int) $idline); $sql .= " AND batch = '".$db->escape($lot)."'"; } @@ -267,7 +270,11 @@ } else { $expeditiondispatch->fk_expedition = $object->id; $expeditiondispatch->entrepot_id = GETPOST($ent, 'int'); - $expeditiondispatch->fk_origin_line = GETPOST($fk_commandedet, 'int'); + $expeditiondispatch->fk_parent = GETPOST('fk_parent'.$dispatch_line_suffix, 'int'); + $expeditiondispatch->fk_product = $prod_id; + if (!($expeditiondispatch->fk_parent > 0)) { + $expeditiondispatch->fk_origin_line = GETPOST($fk_commandedet, 'int'); + } $expeditiondispatch->qty = $newqty; if ($newqty > 0) { @@ -342,7 +349,7 @@ setEventMessages($error, $errors, 'errors'); } else { $db->commit(); - setEventMessages($langs->trans("ReceptionUpdated"), null); + setEventMessages($langs->trans("ShipmentUpdated"), null); header("Location: ".DOL_URL_ROOT.'/expedition/dispatch.php?id='.$object->id); exit; @@ -543,7 +550,7 @@ // Get list of lines of the shipment $products_dispatched, with qty dispatched for each product id $products_dispatched = array(); $sql = "SELECT ed.fk_origin_line as rowid, sum(ed.qty) as qty"; - $sql .= " FROM ".MAIN_DB_PREFIX."expeditiondet as ed"; + $sql .= " FROM ".$db->prefix()."expeditiondet as ed"; $sql .= " WHERE ed.fk_expedition = ".((int) $object->id); $sql .= " GROUP BY ed.fk_origin_line"; @@ -578,8 +585,8 @@ } $sql .= $hookmanager->resPrint; - $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as l"; - $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON l.fk_product=p.rowid"; + $sql .= " FROM ".$db->prefix()."commandedet as l"; + $sql .= " LEFT JOIN ".$db->prefix()."product as p ON l.fk_product=p.rowid"; $sql .= " WHERE l.fk_commande = ".((int) $objectsrc->id); if (empty($conf->global->STOCK_SUPPORTS_SERVICES)) { $sql .= " AND l.product_type = 0"; @@ -766,13 +773,17 @@ print ''; // Warehouse column /*$sql = "SELECT cfd.rowid, cfd.qty, cfd.fk_entrepot, cfd.batch, cfd.eatby, cfd.sellby, cfd.fk_product"; - $sql .= " FROM ".MAIN_DB_PREFIX."commande_fournisseur_dispatch as cfd"; + $sql .= " FROM ".$db->prefix()."commande_fournisseur_dispatch as cfd"; $sql .= " WHERE cfd.fk_commandefourndet = ".(int) $objp->rowid;*/ - $sql = "SELECT ed.rowid, ed.qty, ed.fk_entrepot, eb.batch, eb.eatby, eb.sellby, cd.fk_product"; - $sql .= " FROM ".MAIN_DB_PREFIX."expeditiondet as ed"; - $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."expeditiondet_batch as eb on ed.rowid = eb.fk_expeditiondet"; - $sql .= " JOIN ".MAIN_DB_PREFIX."commandedet as cd on ed.fk_origin_line = cd.rowid"; + $sql = "SELECT ed.rowid"; + $sql .= ", cd.fk_product"; + $sql .= ", ".$db->ifsql('eb.rowid IS NULL', 'ed.qty', 'eb.qty')." as qty"; + $sql .= ", ".$db->ifsql('eb.rowid IS NULL', 'ed.fk_entrepot', 'eb.fk_warehouse')." as fk_warehouse"; + $sql .= ", eb.batch, eb.eatby, eb.sellby"; + $sql .= " FROM ".$db->prefix()."expeditiondet as ed"; + $sql .= " LEFT JOIN ".$db->prefix()."expeditiondet_batch as eb on ed.rowid = eb.fk_expeditiondet"; + $sql .= " JOIN ".$db->prefix()."commandedet as cd on ed.fk_origin_line = cd.rowid"; $sql .= " WHERE ed.fk_origin_line =".(int) $objp->rowid; $sql .= " AND ed.fk_expedition =".(int) $object->id; $sql .= " ORDER BY ed.rowid, ed.fk_origin_line"; @@ -782,76 +793,204 @@ if ($resultsql) { $numd = $db->num_rows($resultsql); - while ($j < $numd) { - $suffix = "_".$j."_".$i; - $objd = $db->fetch_object($resultsql); - - if (isModEnabled('productbatch') && (!empty($objd->batch) || (is_null($objd->batch) && $tmpproduct->status_batch > 0))) { - $type = 'batch'; - - // Enable hooks to append additional columns - $parameters = array( - // allows hook to distinguish between the rows with information and the rows with dispatch form input - 'is_information_row' => true, - 'j' => $j, - 'suffix' => $suffix, - 'objd' => $objd, - ); - $reshook = $hookmanager->executeHooks( - 'printFieldListValue', - $parameters, - $object, - $action - ); - if ($reshook < 0) { - setEventMessages($hookmanager->error, $hookmanager->errors, 'errors'); + while ($obj_exp = $db->fetch_object($resultsql)) { + $suffix = "_" . $j . "_" . $i; + + $expedition_line_child_list = array(); + if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) { + // virtual product : find all children + if ($tmpproduct->hasFatherOrChild(1) > 0) { + $line_id_list = array(); + + // load all child as object line + $expeditionLine = new ExpeditionLigne($db); + $result = $expeditionLine->findAllChild($obj_exp->rowid, $line_id_list, 1); + if ($result > 0) { + $child_level = 1; + foreach ($line_id_list as $line_id_arr) { + foreach ($line_id_arr as $line_obj) { + $child_product_id = (int) $line_obj->fk_product; + if (empty($conf->cache['product'][$child_product_id])) { + $child_product = new Product($db); + $child_product->fetch($child_product_id); + $conf->cache['product'][$child_product_id] = $child_product; + } else { + $child_product = $conf->cache['product'][$child_product_id]; + } + + // determine if line is virtual product and stock is managed + $line_obj->iskit = 0; + $line_obj->incdec = 1; + $sql_child = "SELECT "; + $sql_child .= " SUM(".$db->ifsql("pa.rowid IS NOT NULL", 1, 0).") as iskit"; + $sql_child .= ", ".$db->ifsql("pai.incdec IS NULL", 1, "pai.incdec")." as incdec"; + $sql_child .= " FROM ".$db->prefix()."expeditiondet as ed"; + $sql_child .= " LEFT JOIN ".$db->prefix()."expeditiondet as edp ON edp.rowid = ".((int) $line_obj->fk_parent); + $sql_child .= " LEFT JOIN ".$db->prefix()."product_association as pa ON pa.fk_product_pere = ".((int) $child_product_id); + $sql_child .= " LEFT JOIN ".$db->prefix()."product_association as pai ON pai.fk_product_pere = edp.fk_product AND pai.fk_product_fils = ".((int) $child_product_id); + $sql_child .= " WHERE ed.rowid = ".$line_obj->rowid; + $sql_child .= " GROUP BY pa.rowid, pai.incdec"; + $resql_child = $db->query($sql_child); + if ($resql_child) { + if ($child_obj = $db->fetch_object($resql_child)) { + $line_obj->iskit = (int) $child_obj->iskit; + $line_obj->incdec = (int) $child_obj->incdec; + } + $db->free($resql_child); + } + $line_obj->html_label = str_repeat("    ", $child_level) . "→" . $child_product->getNomUrl(1); + $expedition_line_child_list[] = $line_obj; + } + $child_level++; + } + } } - print $hookmanager->resPrint; - - print ''; + } + if (empty($expedition_line_child_list)) { + $obj_exp->iskit = 0; // is not virtual product + $obj_exp->incdec = 1; // manage stock + $expedition_line_child_list[] = $obj_exp; + } - print ''; - print ''; - print ''; - print ''; - print ''; - print ''; + $child_suffix = $suffix; + foreach ($expedition_line_child_list as $objd) { + $child_line_id = $objd->rowid; + + $can_update_stock = empty($objd->iskit) && !empty($objd->incdec); + $suffix = $child_line_id.$child_suffix; + + if (isModEnabled('productbatch') && (!empty($objd->batch) || (is_null($objd->batch) && $tmpproduct->status_batch > 0))) { + $type = 'batch'; + + // Enable hooks to append additional columns + $parameters = array( + // allows hook to distinguish between the rows with information and the rows with dispatch form input + 'is_information_row' => true, + 'j' => $j, + 'suffix' => $suffix, + 'objd' => $objd, + ); + $reshook = $hookmanager->executeHooks( + 'printFieldListValue', + $parameters, + $object, + $action + ); + if ($reshook < 0) { + setEventMessages($hookmanager->error, $hookmanager->errors, 'errors'); + } + print $hookmanager->resPrint; + + print ''; + + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + + print ''; + print ''; - print ''; - print ''; + print ''; + print ''; + print ''; + //print ''; + print ''; + if (!getDolGlobalString('PRODUCT_DISABLE_SELLBY')) { + print ''; + $dlcdatesuffix = !empty($objd->sellby) ? dol_stringtotime($objd->sellby) : dol_mktime(0, 0, 0, GETPOST('dlc' . $suffix . 'month'), GETPOST('dlc' . $suffix . 'day'), GETPOST('dlc' . $suffix . 'year')); + print $form->selectDate($dlcdatesuffix, 'dlc' . $suffix, '', '', 1, ''); + print ''; + } + if (!getDolGlobalString('PRODUCT_DISABLE_EATBY')) { + print ''; + $dluodatesuffix = !empty($objd->eatby) ? dol_stringtotime($objd->eatby) : dol_mktime(0, 0, 0, GETPOST('dluo' . $suffix . 'month'), GETPOST('dluo' . $suffix . 'day'), GETPOST('dluo' . $suffix . 'year')); + print $form->selectDate($dluodatesuffix, 'dluo' . $suffix, '', '', 1, ''); + print ''; + } + print ' '; // Supplier ref + Qty ordered + qty already dispatched + } else { + $type = 'dispatch'; + $colspan = 6; + $colspan = (getDolGlobalString('PRODUCT_DISABLE_SELLBY')) ? --$colspan : $colspan; + $colspan = (getDolGlobalString('PRODUCT_DISABLE_EATBY')) ? --$colspan : $colspan; + + // Enable hooks to append additional columns + $parameters = array( + // allows hook to distinguish between the rows with information and the rows with dispatch form input + 'is_information_row' => true, + 'j' => $j, + 'suffix' => $suffix, + 'objd' => $objd, + ); + $reshook = $hookmanager->executeHooks( + 'printFieldListValue', + $parameters, + $object, + $action + ); + if ($reshook < 0) { + setEventMessages($hookmanager->error, $hookmanager->errors, 'errors'); + } + print $hookmanager->resPrint; + + print ''; + + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + if (!empty($objd->html_label)) { + print $objd->html_label; + } + print ''; + } + // Qty to dispatch + print ''; + $suggestedvalue = (GETPOSTISSET('qty' . $suffix) ? GETPOST('qty' . $suffix, 'int') : $objd->qty); + //var_dump($suggestedvalue);exit; + if ($can_update_stock) { + print '' . img_picto($langs->trans("Reset"), 'eraser', 'class="pictofixedwidth opacitymedium"') . ''; + print ''; + } else { + print ''; + } print ''; - print ''; - print ''; - //print ''; - print ''; - if (empty($conf->global->PRODUCT_DISABLE_SELLBY)) { - print ''; - $dlcdatesuffix = !empty($objd->sellby) ? dol_stringtotime($objd->sellby) : dol_mktime(0, 0, 0, GETPOST('dlc'.$suffix.'month'), GETPOST('dlc'.$suffix.'day'), GETPOST('dlc'.$suffix.'year')); - print $form->selectDate($dlcdatesuffix, 'dlc'.$suffix, '', '', 1, ''); - print ''; + if ($can_update_stock) { + print img_picto($langs->trans('AddStockLocationLine'), 'split.png', 'class="splitbutton" onClick="addDispatchLine(' . $i . ', \'' . $type . '-' . $child_line_id . '\')"'); } - if (empty($conf->global->PRODUCT_DISABLE_EATBY)) { - print ''; - $dluodatesuffix = !empty($objd->eatby) ? dol_stringtotime($objd->eatby) : dol_mktime(0, 0, 0, GETPOST('dluo'.$suffix.'month'), GETPOST('dluo'.$suffix.'day'), GETPOST('dluo'.$suffix.'year')); - print $form->selectDate($dluodatesuffix, 'dluo'.$suffix, '', '', 1, ''); - print ''; + print ''; + + // Warehouse + print ''; + if ($can_update_stock) { + if (count($listwarehouses) > 1) { + print $formproduct->selectWarehouses(GETPOST("entrepot" . $suffix) ? GETPOST("entrepot" . $suffix) : $objd->fk_warehouse, "entrepot" . $suffix, '', 1, 0, $objp->fk_product, '', 1, 0, null, 'csswarehouse' . $suffix); + } elseif (count($listwarehouses) == 1) { + print $formproduct->selectWarehouses(GETPOST("entrepot" . $suffix) ? GETPOST("entrepot" . $suffix) : $objd->fk_warehouse, "entrepot" . $suffix, '', 0, 0, $objp->fk_product, '', 1, 0, null, 'csswarehouse' . $suffix); + } else { + $langs->load("errors"); + print $langs->trans("ErrorNoWarehouseDefined"); + } } - print ' '; // Supplier ref + Qty ordered + qty already dispatched - } else { - $type = 'dispatch'; - $colspan = 6; - $colspan = (!empty($conf->global->PRODUCT_DISABLE_SELLBY)) ? --$colspan : $colspan; - $colspan = (!empty($conf->global->PRODUCT_DISABLE_EATBY)) ? --$colspan : $colspan; + print "\n"; // Enable hooks to append additional columns $parameters = array( - // allows hook to distinguish between the rows with information and the rows with dispatch form input - 'is_information_row' => true, - 'j' => $j, + 'is_information_row' => false, // this is a dispatch form row + 'i' => $i, 'suffix' => $suffix, - 'objd' => $objd, + 'objp' => $objp, ); $reshook = $hookmanager->executeHooks( 'printFieldListValue', @@ -864,74 +1003,17 @@ } print $hookmanager->resPrint; - print ''; - - print ''; - print ''; - print ''; - print ''; - print ''; - print ''; - print ''; - print ''; - print ''; + print "\n"; } - // Qty to dispatch - print ''; - print ''.img_picto($langs->trans("Reset"), 'eraser', 'class="pictofixedwidth opacitymedium"').''; - $suggestedvalue = (GETPOSTISSET('qty'.$suffix) ? GETPOST('qty'.$suffix, 'int') : $objd->qty); - //var_dump($suggestedvalue);exit; - print ''; - print ''; - print ''; - if (isModEnabled('productbatch') && $objp->tobatch > 0) { - $type = 'batch'; - print img_picto($langs->trans('AddStockLocationLine'), 'split.png', 'class="splitbutton" '.($numd != $j+1 ? 'style="display:none"' : '').' onClick="addDispatchLine('.$i.', \''.$type.'\')"'); - } else { - $type = 'dispatch'; - print img_picto($langs->trans('AddStockLocationLine'), 'split.png', 'class="splitbutton" '.($numd != $j+1 ? 'style="display:none"' : '').' onClick="addDispatchLine('.$i.', \''.$type.'\')"'); - } - - print ''; - // Warehouse - print ''; - if (count($listwarehouses) > 1) { - print $formproduct->selectWarehouses(GETPOST("entrepot".$suffix) ?GETPOST("entrepot".$suffix) : $objd->fk_entrepot, "entrepot".$suffix, '', 1, 0, $objp->fk_product, '', 1, 0, null, 'csswarehouse'.$suffix); - } elseif (count($listwarehouses) == 1) { - print $formproduct->selectWarehouses(GETPOST("entrepot".$suffix) ?GETPOST("entrepot".$suffix) : $objd->fk_entrepot, "entrepot".$suffix, '', 0, 0, $objp->fk_product, '', 1, 0, null, 'csswarehouse'.$suffix); - } else { - $langs->load("errors"); - print $langs->trans("ErrorNoWarehouseDefined"); - } - print "\n"; - - // Enable hooks to append additional columns - $parameters = array( - 'is_information_row' => false, // this is a dispatch form row - 'i' => $i, - 'suffix' => $suffix, - 'objp' => $objp, - ); - $reshook = $hookmanager->executeHooks( - 'printFieldListValue', - $parameters, - $object, - $action - ); - if ($reshook < 0) { - setEventMessages($hookmanager->error, $hookmanager->errors, 'errors'); - } - print $hookmanager->resPrint; - - print "\n"; $j++; - $numline++; } - $suffix = "_".$j."_".$i; + + //$suffix = "_".$j."_".$i; } + /* if ($j == 0) { if (isModEnabled('productbatch') && !empty($objp->tobatch)) { $type = 'batch'; @@ -962,7 +1044,7 @@ print ''; print ''; print ''; - print ''; + print ''; print ''; print ''; @@ -1075,6 +1157,7 @@ print $hookmanager->resPrint; print "\n"; } + */ } } $i++; @@ -1148,7 +1231,7 @@ $("select[name=fk_default_warehouse]").change(function() { console.log("warehouse is modified"); var fk_default_warehouse = $("option:selected", this).val(); - $("select[name^=entrepot_]").val(fk_default_warehouse).change(); + $("select[name^=entrepot]").val(fk_default_warehouse).change(); }); $("#autoreset").click(function() { @@ -1158,7 +1241,12 @@ idtab = id.split("_"); if ($(this).data("remove") == "clear"){ console.log("We clear the object to expected value") - $("#qty_"+idtab[1]+"_"+idtab[2]).val(""); + var idlinetab = idtab[0].split("-"); + var idline = ""; + if (idlinetab.length > 0) { + idline = idlinetab[1]; + } + $("#qty"+idline+"_"+idtab[1]+"_"+idtab[2]).val(""); /* qtyexpected = $("#qty_"+idtab[1]+"_"+idtab[2]).data("expected") console.log(qtyexpected); @@ -1188,7 +1276,7 @@ id = $(this).attr("id"); id = id.split("reset_"); console.log("Reset trigger for id = qty_"+id[1]); - $("#qty_"+id[1]).val(""); + $("#qty"+id[1]).val(""); }); }); '; diff --git a/htdocs/expedition/js/lib_dispatch.js.php b/htdocs/expedition/js/lib_dispatch.js.php index dfcfbc54f833d..e2cd1aae7f120 100644 --- a/htdocs/expedition/js/lib_dispatch.js.php +++ b/htdocs/expedition/js/lib_dispatch.js.php @@ -70,18 +70,23 @@ function addDispatchLine(index, type, mode) { console.log("expedition/js/lib_dispatch.js.php addDispatchLine Split line type="+type+" index="+index+" mode="+mode); + var lineId = ''; + var typeArr = type.split('-'); + if (typeArr.length > 0) { + lineId = typeArr[1]; + } var $row0 = $("tr[name='"+type+'_0_'+index+"']"); var $dpopt = $row0.find('.hasDatepicker').first().datepicker('option', 'all'); // get current datepicker options to apply the same to the cloned datepickers var $row = $row0.clone(true); // clone first batch line to jQuery object var nbrTrs = $("tr[name^='"+type+"_'][name$='_"+index+"']").length; // count nb of tr line with attribute name that starts with 'batch_' or 'dispatch_', and end with _index var qtyOrdered = parseFloat($("#qty_ordered_0_"+index).val()); // Qty ordered is same for all rows - var qty = parseFloat($("#qty_"+(nbrTrs - 1)+"_"+index).val()); + var qty = parseFloat($("#qty"+lineId+"_"+(nbrTrs - 1)+"_"+index).val()); if (isNaN(qty)) { qty = ''; } - console.log("expedition/js/lib_dispatch.js.php addDispatchLine Split line nbrTrs="+nbrTrs+" qtyOrdered="+qtyOrdered+" qty="+qty); + console.log("expedition/js/lib_dispatch.js.php addDispatchLine Split line="+lineId+" nbrTrs="+nbrTrs+" qtyOrdered="+qtyOrdered+" qty="+qty); var qtyDispatched; @@ -106,7 +111,7 @@ function addDispatchLine(index, type, mode) { if (newlineqty <= 0) { newlineqty = qty - 1; oldlineqty = 1; - $("#qty_"+(nbrTrs - 1)+"_"+index).val(oldlineqty); + $("#qty"+lineId+"_"+(nbrTrs - 1)+"_"+index).val(oldlineqty); } //replace tr suffix nbr @@ -124,65 +129,65 @@ function addDispatchLine(index, type, mode) { }, 0); //create new select2 to avoid duplicate id of cloned one - $row.find("select[name='" + 'entrepot_' + nbrTrs + '_' + index + "']").select2(); + $row.find("select[name='"+'entrepot'+lineId+'_'+nbrTrs+'_'+index+"']").select2(); // TODO find solution to copy selected option to new select // TODO find solution to keep new tr's after page refresh //clear value $row.find("input[name^='qty']").val(''); //change name of new row - $row.attr('name', type + '_' + nbrTrs + '_' + index); + $row.attr('name', type+'_'+nbrTrs+'_'+index); //insert new row before last row - $("tr[name^='" + type + "_'][name$='_" + index + "']:last").after($row); + $("tr[name^='"+type+"_'][name$='_"+index+"']:last").after($row); //remove cloned select2 with duplicate id. - $("#s2id_entrepot_" + nbrTrs + '_' + index).detach(); // old way to find duplicated select2 component - $(".csswarehouse_" + nbrTrs + "_" + index + ":first-child").parent("span.selection").parent(".select2").detach(); + $("#s2id_entrepot"+lineId+"_"+nbrTrs+'_'+index).detach(); // old way to find duplicated select2 component + $(".csswarehouse"+lineId+"_"+nbrTrs+"_"+index + ":first-child").parent("span.selection").parent(".select2").detach(); /* Suffix of lines are: _ trs.length _ index */ - $("#qty_"+nbrTrs+"_"+index).focus(); + $("#qty"+lineId+"_"+nbrTrs+"_"+index).focus(); $("#qty_dispatched_0_"+index).val(oldlineqty); //hide all buttons then show only the last one - $("tr[name^='" + type + "_'][name$='_" + index + "'] .splitbutton").hide(); - $("tr[name^='" + type + "_'][name$='_" + index + "']:last .splitbutton").show(); + $("tr[name^='"+type+"_'][name$='_"+index+"'] .splitbutton").hide(); + $("tr[name^='"+type+"_'][name$='_"+index+"']:last .splitbutton").show(); - $("#reset_" + (nbrTrs) + "_" + index).click(function (event) { + $("#reset"+lineId+"_"+(nbrTrs)+"_"+index).click(function (event) { event.preventDefault(); id = $(this).attr("id"); - id = id.split("reset_"); + id = id.split("reset"+lineId+"_"); idrow = id[1]; - idlast = $("tr[name^='" + type + "_'][name$='_" + index + "']:last .qtydispatchinput").attr("id"); - if (idlast == $("#qty_" + idrow).attr("id")) { - console.log("expedition/js/lib_dispatch.js.php Remove trigger for tr name = " + type + "_" + idrow); - $('tr[name="' + type + '_' + idrow + '"').remove(); - $("tr[name^='" + type + "_'][name$='_" + index + "']:last .splitbutton").show(); + idlast = $("tr[name^='"+type+"_'][name$='_"+index+"']:last .qtydispatchinput").attr("id"); + if (idlast == $("#qty"+lineId+"_"+idrow).attr("id")) { + console.log("expedition/js/lib_dispatch.js.php Remove trigger for tr name = "+type+"_"+idrow); + $('tr[name="'+type+'_'+idrow+'"').remove(); + $("tr[name^='"+type+"_'][name$='_"+index+"']:last .splitbutton").show(); } else { - console.log("expedition/js/lib_dispatch.js.php Reset trigger for id = qty_" + idrow); - $("#qty_" + idrow).val(""); + console.log("expedition/js/lib_dispatch.js.php Reset trigger for id = qty_"+idrow); + $("#qty"+lineId+"_"+idrow).val(""); } }); if (mode === 'lessone') { qty = 1; // keep 1 in old line - $("#qty_"+(nbrTrs-1)+"_"+index).val(qty); + $("#qty"+lineId+"_"+(nbrTrs-1)+"_"+index).val(qty); } - $("#qty_"+nbrTrs+"_"+index).val(newlineqty); + $("#qty"+lineId+"_"+nbrTrs+"_"+index).val(newlineqty); // Store arbitrary data for dispatch qty input field change event - $("#qty_" + (nbrTrs - 1) + "_" + index).data('qty', qty); - $("#qty_" + (nbrTrs - 1) + "_" + index).data('type', type); - $("#qty_" + (nbrTrs - 1) + "_" + index).data('index', index); + $("#qty"+lineId+"_" + (nbrTrs - 1) + "_" + index).data('qty', qty); + $("#qty"+lineId+"_" + (nbrTrs - 1) + "_" + index).data('type', type); + $("#qty"+lineId+"_" + (nbrTrs - 1) + "_" + index).data('index', index); // Update dispatched qty when value dispatch qty input field changed //$("#qty_" + (nbrTrs - 1) + "_" + index).change(this.onChangeDispatchLineQty); //set focus on lot of new line (if it exists) - $("#lot_number_" + (nbrTrs) + "_" + index).focus(); + $("#lot_number"+lineId+"_"+(nbrTrs)+"_"+index).focus(); //Clean bad values - $("tr[name^='" + type + "_'][name$='_" + index + "']:last").data("remove", "remove"); - $("#lot_number_" + (nbrTrs) + "_" + index).val("") - $("#idline_" + (nbrTrs) + "_" + index).val("-1") - $("#qty_" + (nbrTrs) + "_" + index).data('expected', "0"); + $("tr[name^='"+type+"_'][name$='_"+index + "']:last").data("remove", "remove"); + $("#lot_number_"+(nbrTrs) + "_"+index).val("") + $("#idline"+lineId+"_"+(nbrTrs)+"_"+index).val("-1") + $("#qty"+lineId+"_"+(nbrTrs)+"_"+index).data('expected', "0"); //$("input[type='hidden']#lot_number_" + (nbrTrs) + "_" + index).remove(); - $("#lot_number_" + (nbrTrs) + "_" + index).removeAttr("disabled"); + $("#lot_number"+lineId+"_"+(nbrTrs)+"_"+index).removeAttr("disabled"); } } @@ -204,13 +209,13 @@ function onChangeDispatchLineQty(element) { index = id[2]; if (index >= 0 && type && qty >= 0) { - nbrTrs = $("tr[name^='" + type + "_'][name$='_" + index + "']").length; + nbrTrs = $("tr[name^='"+type+"_'][name$='_"+index+"']").length; qtyChanged = parseFloat($(element).val()) - qty; // qty changed qtyDispatching = parseFloat($(element).val()); // qty currently being dispatched - qtyOrdered = parseFloat($("#qty_ordered_0_" + index).val()); // qty ordered - qtyDispatched = parseFloat($("#qty_dispatched_0_" + index).val()); // qty already dispatched + qtyOrdered = parseFloat($("#qty_ordered_0_"+index).val()); // qty ordered + qtyDispatched = parseFloat($("#qty_dispatched_0_"+index).val()); // qty already dispatched - console.log("onChangeDispatchLineQty qtyChanged: " + qtyChanged + " qtyDispatching: " + qtyDispatching + " qtyOrdered: " + qtyOrdered + " qtyDispatched: " + qtyDispatched); + console.log("onChangeDispatchLineQty qtyChanged: "+qtyChanged+" qtyDispatching: "+qtyDispatching+" qtyOrdered: "+qtyOrdered+" qtyDispatched: "+qtyDispatched); if ((qtyChanged) <= (qtyOrdered - (qtyDispatched + qtyDispatching))) { $("#qty_dispatched_0_" + index).val(qtyDispatched + qtyChanged); diff --git a/htdocs/install/mysql/migration/17.0.0-18.0.0.sql b/htdocs/install/mysql/migration/17.0.0-18.0.0.sql index 5b65aa5f5bce0..ec4302ac3c1da 100644 --- a/htdocs/install/mysql/migration/17.0.0-18.0.0.sql +++ b/htdocs/install/mysql/migration/17.0.0-18.0.0.sql @@ -581,3 +581,15 @@ ALTER TABLE llx_facture ADD COLUMN fk_input_reason integer NULL DEFAULT NULL AFT -- Product/service managed in stock ALTER TABLE llx_product ADD COLUMN stockable_product integer DEFAULT 1 NOT NULL; UPDATE llx_product set stockable_product = 0 WHERE type = 1; + +-- Dispatcher for virtual products +ALTER TABLE llx_expeditiondet ADD COLUMN fk_parent integer NULL AFTER fk_origin_line; +ALTER TABLE llx_expeditiondet ADD COLUMN fk_product integer NULL AFTER fk_parent; +ALTER TABLE llx_expeditiondet ADD INDEX idx_expeditiondet_fk_parent (fk_parent); +ALTER TABLE llx_expeditiondet ADD INDEX idx_expeditiondet_fk_poduct (fk_product); +ALTER TABLE llx_expeditiondet ADD CONSTRAINT fk_expeditiondet_fk_parent FOREIGN KEY (fk_parent) REFERENCES llx_expeditiondet (rowid); +ALTER TABLE llx_expeditiondet ADD CONSTRAINT fk_expeditiondet_fk_product FOREIGN KEY (fk_product) REFERENCES llx_product (rowid); + +UPDATE llx_expeditiondet as ed LEFT JOIN llx_commandedet as cd ON cd.rowid = ed.fk_origin_line SET ed.fk_product = cd.fk_product WHERE ed.fk_product IS NULL; +ALTER TABLE llx_product_association ADD COLUMN incdec integer DEFAULT 1 AFTER qty; -- was removed on Easya 2022 +ALTER TABLE llx_expeditiondet_batch ADD COLUMN fk_warehouse integer DEFAULT NULL; -- added for compatibility diff --git a/htdocs/install/mysql/tables/llx_expeditiondet.key.sql b/htdocs/install/mysql/tables/llx_expeditiondet.key.sql index b37ae457fe3f4..55524d0af6458 100644 --- a/htdocs/install/mysql/tables/llx_expeditiondet.key.sql +++ b/htdocs/install/mysql/tables/llx_expeditiondet.key.sql @@ -20,4 +20,8 @@ ALTER TABLE llx_expeditiondet ADD INDEX idx_expeditiondet_fk_expedition (fk_expedition); ALTER TABLE llx_expeditiondet ADD INDEX idx_expeditiondet_fk_origin_line (fk_origin_line); +ALTER TABLE llx_expeditiondet ADD INDEX idx_expeditiondet_fk_parent (fk_parent); +ALTER TABLE llx_expeditiondet ADD INDEX idx_expeditiondet_fk_poduct (fk_product); ALTER TABLE llx_expeditiondet ADD CONSTRAINT fk_expeditiondet_fk_expedition FOREIGN KEY (fk_expedition) REFERENCES llx_expedition (rowid); +ALTER TABLE llx_expeditiondet ADD CONSTRAINT fk_expeditiondet_fk_parent FOREIGN KEY (fk_parent) REFERENCES llx_expeditiondet (rowid); +ALTER TABLE llx_expeditiondet ADD CONSTRAINT fk_expeditiondet_fk_product FOREIGN KEY (fk_product) REFERENCES llx_product (rowid); diff --git a/htdocs/install/mysql/tables/llx_expeditiondet.sql b/htdocs/install/mysql/tables/llx_expeditiondet.sql index cccee9e574783..27109e8455a5b 100644 --- a/htdocs/install/mysql/tables/llx_expeditiondet.sql +++ b/htdocs/install/mysql/tables/llx_expeditiondet.sql @@ -24,6 +24,8 @@ create table llx_expeditiondet rowid integer AUTO_INCREMENT PRIMARY KEY, fk_expedition integer NOT NULL, fk_origin_line integer, -- Correspondance de la ligne avec le document d'origine (propal, commande) + fk_parent integer, -- Parent line + fk_product integer, -- Product id fk_entrepot integer, -- Entrepot de depart du produit qty real, -- Quantity rang integer DEFAULT 0 diff --git a/htdocs/install/mysql/tables/llx_expeditiondet_batch.sql b/htdocs/install/mysql/tables/llx_expeditiondet_batch.sql index 6cedc0b5a32be..4c90118e0c56e 100644 --- a/htdocs/install/mysql/tables/llx_expeditiondet_batch.sql +++ b/htdocs/install/mysql/tables/llx_expeditiondet_batch.sql @@ -24,6 +24,7 @@ CREATE TABLE llx_expeditiondet_batch ( sellby date DEFAULT NULL, batch varchar(128) DEFAULT NULL, qty double NOT NULL DEFAULT '0', - fk_origin_stock integer NOT NULL -- id into table llx_product_batch (llx_product_batch may be renamed into llx_product_stock_batch in another version). TODO We should add and use instead a fk_warehouse field + fk_origin_stock integer NOT NULL, -- id into table llx_product_batch (llx_product_batch may be renamed into llx_product_stock_batch in another version). TODO We should add and use instead a fk_warehouse field + fk_warehouse integer DEFAULT NULL -- ID of warehouse to use for the stock change ) ENGINE=innodb; diff --git a/htdocs/langs/en_US/products.lang b/htdocs/langs/en_US/products.lang index 5c87b15e541e4..068436c3f587f 100644 --- a/htdocs/langs/en_US/products.lang +++ b/htdocs/langs/en_US/products.lang @@ -436,3 +436,4 @@ StockableProduct=Stock management StockableProductDescription=If this option is enabled, the stock modification for this element is retained. If disabled, the stock modification for this element is not retained. StockDisabled=Stock disabled StockEnabled=Stock enabled +WhenProductVirtualOnOptionAreForced=When virtual products option is on, automatic stock decrease is forced to 'Decrease real stocks on shipping validation' and automatic increase mode is forced to 'Increase real stocks on manual dispatching into warehouses' and can't be edited. Other options can be defined as you want. diff --git a/htdocs/langs/en_US/sendings.lang b/htdocs/langs/en_US/sendings.lang index b3fc779dd2ffd..4bee864c282c4 100644 --- a/htdocs/langs/en_US/sendings.lang +++ b/htdocs/langs/en_US/sendings.lang @@ -63,6 +63,7 @@ NoProductToShipFoundIntoStock=No product to ship found in warehouse %s. C WeightVolShort=Weight/Vol. ValidateOrderFirstBeforeShipment=You must first validate the order before being able to make shipments. NoLineGoOnTabToAddSome=No line, go on tab "%s" to add +ShipmentUpdated=Shipment successfully updated # Sending methods # ModelDocument @@ -75,6 +76,7 @@ SumOfProductWeights=Sum of product weights # warehouse details DetailWarehouseNumber= Warehouse details DetailWarehouseFormat= W:%s (Qty: %d) +DetailChildrenFormat=%s : %s (Qty: %d) ShipmentDistribution=Shipment distribution diff --git a/htdocs/product/class/product.class.php b/htdocs/product/class/product.class.php index 325640e13b056..7e84be2fb26a9 100644 --- a/htdocs/product/class/product.class.php +++ b/htdocs/product/class/product.class.php @@ -4987,8 +4987,6 @@ public function getFather() */ public function getChildsArbo($id, $firstlevelonly = 0, $level = 1, $parents = array()) { - global $alreadyfound; - if (empty($id)) { return array(); } @@ -5005,9 +5003,6 @@ public function getChildsArbo($id, $firstlevelonly = 0, $level = 1, $parents = a dol_syslog(get_class($this).'::getChildsArbo id='.$id.' level='.$level. ' parents='.(is_array($parents)?implode(',', $parents):$parents), LOG_DEBUG); - if ($level == 1) { - $alreadyfound = array($id=>1); // We init array of found object to start of tree, so if we found it later (should not happened), we stop immediatly - } // Protection against infinite loop if ($level > 30) { return array(); @@ -5016,14 +5011,14 @@ public function getChildsArbo($id, $firstlevelonly = 0, $level = 1, $parents = a $res = $this->db->query($sql); if ($res) { $prods = array(); + if ($this->db->num_rows($sql) > 0) $parents[] = $id; + while ($rec = $this->db->fetch_array($res)) { - if (!empty($alreadyfound[$rec['rowid']])) { + if (in_array($rec['id'], $parents)) { dol_syslog(get_class($this).'::getChildsArbo the product id='.$rec['rowid'].' was already found at a higher level in tree. We discard to avoid infinite loop', LOG_WARNING); - if (in_array($rec['id'], $parents)) { - continue; // We discard this child if it is already found at a higher level in tree in the same branch. - } + continue; // We discard this child if it is already found at a higher level in tree in the same branch. } - $alreadyfound[$rec['rowid']] = 1; + $prods[$rec['rowid']] = array( 0=>$rec['rowid'], 1=>$rec['qty'], @@ -5037,6 +5032,7 @@ public function getChildsArbo($id, $firstlevelonly = 0, $level = 1, $parents = a //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty'],2=>$rec['fk_product_type']); //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty']); if (empty($firstlevelonly)) { + //$parents[] = $rec['rowid']; $listofchilds = $this->getChildsArbo($rec['rowid'], 0, $level + 1, array_push($parents, $rec['rowid'])); foreach ($listofchilds as $keyChild => $valueChild) { $prods[$rec['rowid']]['childs'][$keyChild] = $valueChild; diff --git a/htdocs/product/stock/class/mouvementstock.class.php b/htdocs/product/stock/class/mouvementstock.class.php index 6f81da2d9a25f..f06d6264b52bf 100644 --- a/htdocs/product/stock/class/mouvementstock.class.php +++ b/htdocs/product/stock/class/mouvementstock.class.php @@ -829,15 +829,16 @@ public function livraison($user, $fk_product, $entrepot_id, $qty, $price = 0, $l * @param int $id_product_batch Id product_batch * @param string $inventorycode Inventory code * @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) + * @param int $disablestockchangeforsubproduct Disable stock change for sub-products of kit (useful only if product is a subproduct) * @return int <0 if KO, >0 if OK */ - public function reception($user, $fk_product, $entrepot_id, $qty, $price = 0, $label = '', $eatby = '', $sellby = '', $batch = '', $datem = '', $id_product_batch = 0, $inventorycode = '', $donotcleanemptylines = 0) + public function reception($user, $fk_product, $entrepot_id, $qty, $price = 0, $label = '', $eatby = '', $sellby = '', $batch = '', $datem = '', $id_product_batch = 0, $inventorycode = '', $donotcleanemptylines = 0, $disablestockchangeforsubproduct = 0) { global $conf; $skip_batch = empty($conf->productbatch->enabled); - return $this->_create($user, $fk_product, $entrepot_id, $qty, 3, $price, $label, $inventorycode, $datem, $eatby, $sellby, $batch, $skip_batch, $id_product_batch, 0, $donotcleanemptylines); + return $this->_create($user, $fk_product, $entrepot_id, $qty, 3, $price, $label, $inventorycode, $datem, $eatby, $sellby, $batch, $skip_batch, $id_product_batch, $disablestockchangeforsubproduct, $donotcleanemptylines); } /**