diff --git a/htdocs/admin/expedition.php b/htdocs/admin/expedition.php index c5463ae2553..a85f726a8de 100644 --- a/htdocs/admin/expedition.php +++ b/htdocs/admin/expedition.php @@ -456,6 +456,13 @@ print ''; print ajax_constantonoff('SHIPPING_DISPLAY_STOCK_ENTRY_DATE'); print ''; +print ''; +print ''.$langs->trans('SHIPPING_SELL_EAT_BY_DATE_PRE_SELECT_EARLIEST'); +print ''; +print ''; +print ajax_constantonoff('SHIPPING_SELL_EAT_BY_DATE_PRE_SELECT_EARLIEST'); +print ''; + $substitutionarray = pdf_getSubstitutionArray($langs, null, null, 2); $substitutionarray['__(AnyTranslationKey)__'] = $langs->trans("Translation"); $htmltext = ''.$langs->trans("AvailableVariables").':
'; diff --git a/htdocs/admin/stock.php b/htdocs/admin/stock.php index 04606f92580..ae6196d3477 100644 --- a/htdocs/admin/stock.php +++ b/htdocs/admin/stock.php @@ -228,6 +228,15 @@ $formproduct = new FormProduct($db); $disableStockCalculateOn = array(); +if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) { + $langs->load('products'); + $disableStockCalculateOn[] = 'BILL'; + $disableStockCalculateOn[] = 'VALIDATE_ORDER'; + $disableStockCalculateOn[] = 'SUPPLIER_BILL'; + $disableStockCalculateOn[] = 'SUPPLIER_VALIDATE_ORDER'; + $disableStockCalculateOn[] = 'SHIPMENT_CLOSE'; + 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"); diff --git a/htdocs/commande/class/commande.class.php b/htdocs/commande/class/commande.class.php index 63b767ff1bd..b8f248371ba 100644 --- a/htdocs/commande/class/commande.class.php +++ b/htdocs/commande/class/commande.class.php @@ -1646,13 +1646,38 @@ class Commande extends CommonOrder $result = $product->fetch($fk_product); $product_type = $product->type; - if (getDolGlobalString('STOCK_MUST_BE_ENOUGH_FOR_ORDER') && $product_type == 0 && $product->stock_reel < $qty) { - $langs->load("errors"); - $this->error = $langs->trans('ErrorStockIsNotEnoughToAddProductOnOrder', $product->ref); - $this->errors[] = $this->error; - dol_syslog(get_class($this)."::addline error=Product ".$product->ref.": ".$this->error, LOG_ERR); - $this->db->rollback(); - return self::STOCK_NOT_ENOUGH_FOR_ORDER; + if (getDolGlobalString('STOCK_MUST_BE_ENOUGH_FOR_ORDER') && $product_type == 0) { + // get real stock + $productChildrenNb = 0; + if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) { + $productChildrenNb = $product->hasFatherOrChild(1); + } + if ($productChildrenNb > 0) { + // compute real stock from each subcomponent + $product_stock = null; + $product->loadStockForVirtualProduct('warehouseopen', $qty); + foreach ($product->stock_warehouse as $componentStockWarehouse) { + if ($product_stock === null) { + $product_stock = $componentStockWarehouse->real; + } else { + $product_stock = min($product_stock, $componentStockWarehouse->real); + } + } + if ($product_stock === null) { + $product_stock = 0; + } + } else { + $product_stock = $product->stock_reel; + } + + if ($product_stock < $qty) { + $langs->load("errors"); + $this->error = $langs->trans('ErrorStockIsNotEnoughToAddProductOnOrder', $product->ref); + $this->errors[] = $this->error; + dol_syslog(get_class($this)."::addline error=Product ".$product->ref.": ".$this->error, LOG_ERR); + $this->db->rollback(); + return self::STOCK_NOT_ENOUGH_FOR_ORDER; + } } } // Calcul du total TTC et de la TVA pour la ligne a partir de @@ -3187,15 +3212,40 @@ class Commande extends CommonOrder $result = $product->fetch($line->fk_product); $product_type = $product->type; - if (getDolGlobalString('STOCK_MUST_BE_ENOUGH_FOR_ORDER') && $product_type == 0 && $product->stock_reel < $qty) { - $langs->load("errors"); - $this->error = $langs->trans('ErrorStockIsNotEnoughToAddProductOnOrder', $product->ref); - $this->errors[] = $this->error; + if (getDolGlobalString('STOCK_MUST_BE_ENOUGH_FOR_ORDER') && $product_type == 0) { + // get real stock + $productChildrenNb = 0; + if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) { + $productChildrenNb = $product->hasFatherOrChild(1); + } + if ($productChildrenNb > 0) { + // compute real stock from each subcomponent + $product_stock = null; + $product->loadStockForVirtualProduct('warehouseopen', $qty); + foreach ($product->stock_warehouse as $componentStockWarehouse) { + if ($product_stock === null) { + $product_stock = $componentStockWarehouse->real; + } else { + $product_stock = min($product_stock, $componentStockWarehouse->real); + } + } + if ($product_stock === null) { + $product_stock = 0; + } + } else { + $product_stock = $product->stock_reel; + } - dol_syslog(get_class($this)."::addline error=Product ".$product->ref.": ".$this->error, LOG_ERR); + if ($product_stock < $qty) { + $langs->load("errors"); + $this->error = $langs->trans('ErrorStockIsNotEnoughToAddProductOnOrder', $product->ref); + $this->errors[] = $this->error; - $this->db->rollback(); - return self::STOCK_NOT_ENOUGH_FOR_ORDER; + dol_syslog(get_class($this)."::addline error=Product ".$product->ref.": ".$this->error, LOG_ERR); + + $this->db->rollback(); + return self::STOCK_NOT_ENOUGH_FOR_ORDER; + } } } diff --git a/htdocs/compta/facture/class/facture.class.php b/htdocs/compta/facture/class/facture.class.php index 2918239365c..c4d535574c4 100644 --- a/htdocs/compta/facture/class/facture.class.php +++ b/htdocs/compta/facture/class/facture.class.php @@ -4256,11 +4256,36 @@ class Facture extends CommonInvoice $result = $product->fetch($fk_product); $product_type = $product->type; - if (getDolGlobalString('STOCK_MUST_BE_ENOUGH_FOR_INVOICE') && $product_type == 0 && $product->stock_reel < $qty) { - $langs->load("errors"); - $this->error = $langs->trans('ErrorStockIsNotEnoughToAddProductOnInvoice', $product->ref); - $this->db->rollback(); - return -3; + if (getDolGlobalString('STOCK_MUST_BE_ENOUGH_FOR_INVOICE') && $product_type == 0) { + // get real stock + $productChildrenNb = 0; + if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) { + $productChildrenNb = $product->hasFatherOrChild(1); + } + if ($productChildrenNb > 0) { + // compute real stock from each subcomponent + $product_stock = null; + $product->loadStockForVirtualProduct('warehouseopen', $qty); + foreach ($product->stock_warehouse as $componentStockWarehouse) { + if ($product_stock === null) { + $product_stock = $componentStockWarehouse->real; + } else { + $product_stock = min($product_stock, $componentStockWarehouse->real); + } + } + if ($product_stock === null) { + $product_stock = 0; + } + } else { + $product_stock = $product->stock_reel; + } + + if ($product_stock < $qty) { + $langs->load("errors"); + $this->error = $langs->trans('ErrorStockIsNotEnoughToAddProductOnInvoice', $product->ref); + $this->db->rollback(); + return -3; + } } } @@ -4574,11 +4599,36 @@ class Facture extends CommonInvoice $result = $product->fetch($line->fk_product); $product_type = $product->type; - if (getDolGlobalString('STOCK_MUST_BE_ENOUGH_FOR_INVOICE') && $product_type == 0 && $product->stock_reel < $qty) { - $langs->load("errors"); - $this->error = $langs->trans('ErrorStockIsNotEnoughToAddProductOnInvoice', $product->ref); - $this->db->rollback(); - return -3; + if (getDolGlobalString('STOCK_MUST_BE_ENOUGH_FOR_INVOICE') && $product_type == 0) { + // get real stock + $productChildrenNb = 0; + if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) { + $productChildrenNb = $product->hasFatherOrChild(1); + } + if ($productChildrenNb > 0) { + // compute real stock from each subcomponent + $product_stock = null; + $product->loadStockForVirtualProduct('warehouseopen', $qty); + foreach ($product->stock_warehouse as $componentStockWarehouse) { + if ($product_stock === null) { + $product_stock = $componentStockWarehouse->real; + } else { + $product_stock = min($product_stock, $componentStockWarehouse->real); + } + } + if ($product_stock === null) { + $product_stock = 0; + } + } else { + $product_stock = $product->stock_reel; + } + + if ($product_stock < $qty) { + $langs->load("errors"); + $this->error = $langs->trans('ErrorStockIsNotEnoughToAddProductOnInvoice', $product->ref); + $this->db->rollback(); + return -3; + } } } diff --git a/htdocs/expedition/ajax/interface.php b/htdocs/expedition/ajax/interface.php new file mode 100644 index 00000000000..94aa3f60fc4 --- /dev/null +++ b/htdocs/expedition/ajax/interface.php @@ -0,0 +1,149 @@ + + * Copyright (C) 2024 Lionel Vessiller + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * \file htdocs/expedition/ajax/interface.php + * \brief Ajax search component for Shipment. + */ + +if (!defined('NOREQUIRESOC')) { + define('NOREQUIRESOC', '1'); +} +if (!defined('NOCSRFCHECK')) { + define('NOCSRFCHECK', '1'); +} +if (!defined('NOTOKENRENEWAL')) { + define('NOTOKENRENEWAL', '1'); +} +if (!defined('NOREQUIREMENU')) { + define('NOREQUIREMENU', '1'); +} +if (!defined('NOREQUIREHTML')) { + define('NOREQUIREHTML', '1'); +} +if (!defined('NOREQUIREAJAX')) { + define('NOREQUIREAJAX', '1'); +} + +require '../../main.inc.php'; // Load $user and permissions +/** + * @var DoliDB $db + * @var Translate $langs + * @var User $user + */ + +$warehouse_id = GETPOSTINT('warehouse_id'); +$batch = GETPOST('batch', 'alphanohtml'); +$product_id = GETPOSTINT('product_id'); +$action = GETPOST('action', 'alphanohtml'); + +$result = restrictedArea($user, 'expedition'); + +$permissiontowrite = $user->hasRight('expedition', 'write'); + +$is_eat_by_enabled = !getDolGlobalInt('PRODUCT_DISABLE_EATBY'); +$is_sell_by_enabled = !getDolGlobalInt('PRODUCT_DISABLE_SELLBY'); + + +/* + * View + */ + +top_httphead("application/json"); + +if ($action == 'updateselectbatchbywarehouse' && $permissiontowrite) { + $resArr = array(); + + $sql = "SELECT pb.batch, pb.rowid, ps.fk_entrepot, pb.qty, e.ref as label, ps.fk_product"; + if ($is_eat_by_enabled) { + $sql .= ", pl.eatby"; + } + if ($is_sell_by_enabled) { + $sql .= ", pl.sellby"; + } + $sql .= " FROM ".$db->prefix()."product_batch as pb"; + $sql .= " LEFT JOIN ".$db->prefix()."product_stock as ps on ps.rowid = pb.fk_product_stock"; + $sql .= " LEFT JOIN ".$db->prefix()."entrepot as e on e.rowid = ps.fk_entrepot AND e.entity IN (".getEntity('stock').")"; + if ($is_eat_by_enabled || $is_sell_by_enabled) { + $sql .= " LEFT JOIN ".$db->prefix()."product_lot as pl on ps.fk_product = pl.fk_product AND pb.batch = pl.batch"; + } + $sql .= " WHERE ps.fk_product = ".((int) $product_id); + if ($warehouse_id > 0) { + $sql .= " AND fk_entrepot = '".((int) $warehouse_id)."'"; + } + $sql .= " ORDER BY e.ref, pb.batch"; + + $resql = $db->query($sql); + + if ($resql) { + while ($obj = $db->fetch_object($resql)) { + $eat_by_date_formatted = ''; + if ($is_eat_by_enabled && !empty($obj->eatby)) { + $eat_by_date_formatted = dol_print_date($db->jdate($obj->eatby), 'day'); + } + $sell_by_date_formatted = ''; + if ($is_sell_by_enabled && !empty($obj->sellby)) { + $sell_by_date_formatted = dol_print_date($db->jdate($obj->sellby), 'day'); + } + + // set qty + if (!isset($resArr[$obj->batch])) { + $resArr[$obj->batch] = array( + 'qty' => (float) $obj->qty, + ); + } else { + $resArr[$obj->batch]['qty'] += $obj->qty; + } + + // set eat-by date + if (!isset($resArr[$obj->batch]['eatbydate'])) { + $resArr[$obj->batch]['eatbydate'] = $eat_by_date_formatted; + } + + // set sell-by date + if (!isset($resArr[$obj->batch]['sellbydate'])) { + $resArr[$obj->batch]['sellbydate'] = $sell_by_date_formatted; + } + } + } + + echo json_encode($resArr); +} elseif ($action == 'updateselectwarehousebybatch' && $permissiontowrite) { + $res = 0; + + $sql = "SELECT pb.batch, pb.rowid, ps.fk_entrepot, e.ref, pb.qty"; + $sql .= " FROM ".$db->prefix()."product_batch as pb"; + $sql .= " JOIN ".$db->prefix()."product_stock as ps on ps.rowid = pb.fk_product_stock"; + $sql .= " JOIN ".$db->prefix()."entrepot as e on e.rowid = ps.fk_entrepot AND e.entity IN (".getEntity('stock').")"; + $sql .= " WHERE ps.fk_product = ".((int) $product_id); + if ($batch) { + $sql .= " AND pb.batch = '".$db->escape($batch)."'"; + } + $sql .= " ORDER BY e.ref, pb.batch"; + + $resql = $db->query($sql); + + if ($resql) { + if ($db->num_rows($resql) == 1) { + $obj = $db->fetch_object($resql); + $res = $obj->fk_entrepot; + } + } + + echo json_encode($res); +} diff --git a/htdocs/expedition/card.php b/htdocs/expedition/card.php index 17d37a295fb..6543705f137 100644 --- a/htdocs/expedition/card.php +++ b/htdocs/expedition/card.php @@ -439,7 +439,14 @@ if (empty($reshook)) { } 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++; @@ -1252,13 +1259,22 @@ if ($action == 'create') { print ''."\n"; print ''."\n"; + $qtyProdCom = $line->qty; + $productChildrenNb = 0; // Product label if ($line->fk_product > 0) { // If predefined product $res = $product->fetch($line->fk_product); if ($res < 0) { dol_print_error($db, $product->error, $product->errors); } - $product->load_stock('warehouseopen'); // Load all $product->stock_warehouse[idwarehouse]->detail_batch + if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) { + $productChildrenNb = $product->hasFatherOrChild(1); + } + if ($productChildrenNb > 0) { + $product->loadStockForVirtualProduct('warehouseopen', $qtyProdCom); + } else { + $product->load_stock('warehouseopen'); // Load all $product->stock_warehouse[idwarehouse]->detail_batch + } //var_dump($product->stock_warehouse[1]); print ''; @@ -1319,7 +1335,6 @@ if ($action == 'create') { print ''.$line->qty; print ''; print ''.$unit_order.''; - $qtyProdCom = $line->qty; // Qty already shipped print ''; @@ -1391,10 +1406,14 @@ if ($action == 'create') { if (getDolGlobalInt('STOCK_DISALLOW_NEGATIVE_TRANSFER')) { $stockMin = 0; } - if ($product->stockable_product == Product::ENABLED_STOCK) { - print $formproduct->selectWarehouses($tmpentrepot_id, 'entl'.$indiceAsked, '', 1, 0, $line->fk_product, '', 1, 0, array(), 'minwidth200', array(), 1, $stockMin, 'stock DESC, e.ref'); + if ($productChildrenNb > 0) { + print $formproduct->selectWarehouses($tmpentrepot_id, 'entl'.$indiceAsked, '', 1, 0, 0, '', 0, 0, array(), 'minwidth200', array(), 1, $stockMin, 'stock DESC, e.ref'); } else { - print img_warning().' '.$langs->trans('StockDisabled'); + if ($product->stockable_product == Product::ENABLED_STOCK) { + print $formproduct->selectWarehouses($tmpentrepot_id, 'entl'.$indiceAsked, '', 1, 0, $line->fk_product, '', 1, 0, array(), 'minwidth200', array(), 1, $stockMin, 'stock DESC, e.ref'); + } else { + print img_warning().' '.$langs->trans('StockDisabled'); + } } if ($tmpentrepot_id > 0 && $tmpentrepot_id == $warehouse_id) { @@ -1636,10 +1655,12 @@ if ($action == 'create') { if (isModEnabled('stock')) { print ''; if ($line->product_type == Product::TYPE_PRODUCT || getDolGlobalString('STOCK_SUPPORTS_SERVICES')) { - if ($product->stockable_product == Product::ENABLED_STOCK) { + if ($product->stockable_product == Product::ENABLED_STOCK || $productChildrenNb > 0) { print $tmpwarehouseObject->getNomUrl(0).' '; - print ''; - print '('.$stock.')'; + if ($productChildrenNb <= 0) { + print ''; + print '('.$stock.')'; + } } else { print img_warning().' '.$langs->trans('StockDisabled'); } @@ -2686,6 +2707,34 @@ if ($action == 'create') { } } 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 (!isset($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]; + } + $child_product_label = $child_product->ref . ' ' . $child_product->label; + + // get warehouse from cache + if (!isset($conf->cache['warehouse'][$warehouse_id])) { + $child_warehouse = new Entrepot($db); + $child_warehouse->fetch($warehouse_id); + $conf->cache['warehouse'][$warehouse_id] = $child_warehouse; + } else { + $child_warehouse = $conf->cache['warehouse'][$warehouse_id]; + } + + $detail .= $langs->trans('DetailChildrenFormat', $child_product_label, $child_warehouse->label, price2num($total_qty, 'MS')).'
'; + } + } + print $form->textwithtooltip(img_picto('', 'object_stock').' '.$langs->trans('DetailWarehouseNumber'), $detail); } print ''; } @@ -2746,9 +2795,25 @@ if ($action == 'create') { print '
'; print ''; } elseif ($object->status == 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 (!isset($conf->cache['product'][$product_id])) { + $product = new Product($db); + $product->fetch($product_id); + $conf->cache['product'][$product_id] = $product; + } 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 b8111939c2f..2350c570fbb 100644 --- a/htdocs/expedition/class/expedition.class.php +++ b/htdocs/expedition/class/expedition.class.php @@ -258,7 +258,7 @@ class Expedition extends CommonObject public $commande; /** - * @var ExpeditionLigne[] array of shipping lines + * @var array array of shipping lines */ public $lines = array(); @@ -496,15 +496,140 @@ class Expedition extends CommonObject if ($this->db->query($sql)) { // Insert of lines $num = count($this->lines); - for ($i = 0; $i < $num; $i++) { - if (empty($this->lines[$i]->product_type) || getDolGlobalString('STOCK_SUPPORTS_SERVICES') || getDolGlobalString('SHIPMENT_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++; + $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 (!is_object($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; + } } - } else { // with batch management - if ($this->create_line_batch($this->lines[$i], $this->lines[$i]->array_options) <= 0) { - $error++; + } + } + } + $kits_id_cached = array(); + $sub_kits_id_cached = array(); + for ($i = 0; $i < $num; $i++) { + $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 { + $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']; + $product_child_qty = (float) $product_child_arr['nb_total']; // by default + $warehouse_id = $line->entrepot_id; // by default + if ($is_kit || !$product_child_incdec) { + if (!$product_child_incdec) { + $product_child_qty = 0; + } + $warehouse_id = 0; // no warehouse used for a kit or if stock is not managed (empty incdec) + } + + // 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; } } } @@ -589,9 +714,11 @@ class Expedition extends CommonObject * @param float $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 Return integer <0 if KO, line_id if OK */ - public function create_line($entrepot_id, $origin_line_id, $qty, $rang = 0, $array_options = []) + public function create_line($entrepot_id, $origin_line_id, $qty, $rang = 0, $array_options = [], $parent_line_id = 0, $product_id = 0) { //phpcs:enable global $user; @@ -601,10 +728,18 @@ class Expedition extends CommonObject $expeditionline->entrepot_id = $entrepot_id; $expeditionline->fk_elementdet = $origin_line_id; $expeditionline->element_type = $this->origin; + $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_elementdet); + $expeditionline->fk_product = $order_line->fk_product; + } + if (($lineId = $expeditionline->insert($user)) < 0) { $this->errors[] = $expeditionline->error; } @@ -995,9 +1130,11 @@ class Expedition extends CommonObject * @param int $id Id of source line (order line) * @param float $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 Return integer <0 if KO, >0 if OK */ - public function addline($entrepot_id, $id, $qty, $array_options = []) + public function addline($entrepot_id, $id, $qty, $array_options = [], $fk_product = 0, $fk_parent = 0) { global $conf, $langs; @@ -1008,6 +1145,8 @@ class Expedition extends CommonObject $line->origin_line_id = $id; $line->fk_elementdet = $id; $line->element_type = 'order'; + $line->fk_parent = $fk_parent; + $line->fk_product = $fk_product; $line->qty = $qty; $orderline = new OrderLine($this->db); @@ -1016,6 +1155,9 @@ class Expedition extends CommonObject // 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)) { $product = new Product($this->db); @@ -1028,20 +1170,45 @@ class Expedition extends CommonObject } if (getDolGlobalString('STOCK_MUST_BE_ENOUGH_FOR_SHIPMENT')) { - // Check must be done for stock of product into warehouse if $entrepot_id defined - if ($entrepot_id > 0) { - $product->load_stock('warehouseopen'); - $product_stock = $product->stock_warehouse[$entrepot_id]->real; + $productChildrenNb = 0; + if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) { + $productChildrenNb = $product->hasFatherOrChild(1); + } + if ($productChildrenNb > 0) { + $product_stock = null; + $product->loadStockForVirtualProduct('warehouseopen', $line->qty); + if ($entrepot_id > 0) { + if (isset($product->stock_warehouse[$entrepot_id])) { + $product_stock = $product->stock_warehouse[$entrepot_id]->real; + } + } else { + foreach ($product->stock_warehouse as $componentStockWarehouse) { + if ($product_stock === null) { + $product_stock = $componentStockWarehouse->real; + } else { + $product_stock = min($product_stock, $componentStockWarehouse->real); + } + } + } + if ($product_stock === null) { + $product_stock = 0; + } } else { - $product_stock = $product->stock_reel; + // Check must be done for stock of product into warehouse if $entrepot_id defined + if ($entrepot_id > 0) { + $product->load_stock('warehouseopen'); + $product_stock = $product->stock_warehouse[$entrepot_id]->real; + } else { + $product_stock = $product->stock_reel; + } } $product_type = $product->type; if ($product_type == 0 || getDolGlobalString('STOCK_SUPPORTS_SERVICES')) { - $isavirtualproduct = ($product->hasFatherOrChild(1) > 0); + $isavirtualproduct = ($productChildrenNb > 0); // The product is qualified for a check of quantity (must be enough in stock to be added into shipment). if (!$isavirtualproduct || !getDolGlobalString('PRODUIT_SOUSPRODUITS') || ($isavirtualproduct && !getDolGlobalString('STOCK_EXCLUDE_VIRTUAL_PRODUCTS'))) { // If STOCK_EXCLUDE_VIRTUAL_PRODUCTS is set, we do not manage stock for kits/virtual products. - if ($product_stock < $qty && $product->stockable_product == Product::ENABLED_STOCK) { + if ($product->stockable_product == Product::ENABLED_STOCK && $product_stock < $qty) { $langs->load("errors"); $this->error = $langs->trans('ErrorStockIsNotEnoughToAddProductOnShipment', $product->ref); $this->errorhidden = 'ErrorStockIsNotEnoughToAddProductOnShipment'; @@ -1056,8 +1223,8 @@ class Expedition extends CommonObject // 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; } @@ -1077,9 +1244,10 @@ class Expedition extends CommonObject * * @param array{detail:array,qty:int|float,ix_l:int} $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 Return integer <0 if KO, >0 if OK */ - public function addline_batch($dbatch, $array_options = []) + public function addline_batch($dbatch, $array_options = [], $origin_line = null) { // phpcs:enable global $conf, $langs; @@ -1131,6 +1299,12 @@ class Expedition extends CommonObject $line->fk_elementdet = $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 (!getDolGlobalString('MAIN_EXTRAFIELDS_DISABLED') && is_array($array_options) && count($array_options) > 0) { // For avoid conflicts if trigger used @@ -1331,19 +1505,27 @@ class Expedition extends CommonObject } // Stock control - if (!$error && isModEnabled('stock') && + $can_update_stock = isModEnabled('stock') && ((getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') && $this->status > self::STATUS_DRAFT) || - (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE') && $this->status == self::STATUS_CLOSED && $also_update_stock))) { + (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE') && $this->status == 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 ".$this->db->prefix()."commandedet as cd,"; - $sql .= " ".$this->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_elementdet"; + $sql .= " GROUP BY ed.fk_product, ed.qty, ed.fk_entrepot, ed.rowid, pai.incdec"; + $sql .= $this->db->order("ed.rowid", "DESC"); dol_syslog(get_class($this)."::delete select details", LOG_DEBUG); $resql = $this->db->query($sql); @@ -1355,45 +1537,68 @@ class Expedition extends CommonObject 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 = ''; - // 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 = ''; + // 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, (string) $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, (string) $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 in case 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++; @@ -1401,87 +1606,67 @@ class Expedition extends CommonObject } } - // 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(); - } - } - - if (!$error) { - $sql = "DELETE FROM ".$this->db->prefix()."expeditiondet"; - $sql .= " WHERE fk_expedition = ".((int) $this->id); + // Delete linked object + $res = $this->deleteObjectLinked(); + if ($res < 0) { + $error++; + } - if ($this->db->query($sql)) { - // Delete linked object - $res = $this->deleteObjectLinked(); - if ($res < 0) { - $error++; - } + // No delete expedition + if (!$error) { + $sql = "SELECT rowid FROM ".$this->db->prefix()."expedition"; + $sql .= " WHERE rowid = ".((int) $this->id); - // 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_object = $this->origin_object; + '@phan-var-force Facture|Commande $origin_object'; + if ($origin_object->status == 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" + $origin_object->loadExpeditions(); + //var_dump($this->$origin->expeditions);exit; + if (count($origin_object->expeditions) <= 0) { + $origin_object->setStatut(Commande::STATUS_VALIDATED); + } + } + } - if ($this->db->query($sql)) { - if (!empty($this->origin) && $this->origin_id > 0) { - $this->fetch_origin(); - $origin_object = $this->origin_object; - '@phan-var-force Facture|Commande $origin_object'; - if ($origin_object->status == 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" - $origin_object->loadExpeditions(); - //var_dump($this->$origin->expeditions);exit; - if (count($origin_object->expeditions) <= 0) { - $origin_object->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 (file_exists($dir)) { + if (!dol_delete_dir_recursive($dir)) { + $this->error = $langs->trans("ErrorCanNotDeleteDir", $dir); + 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; - } - } - } - - 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(); @@ -1530,9 +1715,10 @@ class Expedition extends CommonObject } // Stock control - if (!$error && isModEnabled('stock') && + $can_update_stock = isModEnabled('stock') && ((getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') && $this->status > self::STATUS_DRAFT) || - (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE') && $this->status == self::STATUS_CLOSED && $also_update_stock))) { + (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE') && $this->status == self::STATUS_CLOSED && $also_update_stock)); + if (!$error) { require_once DOL_DOCUMENT_ROOT."/product/stock/class/mouvementstock.class.php"; $langs->load("agenda"); @@ -1540,12 +1726,19 @@ class Expedition extends CommonObject // 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 ".$this->db->prefix()."commandedet as cd,"; - $sql .= " ".$this->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_elementdet"; + $sql .= " GROUP BY ed.fk_product, ed.qty, ed.fk_entrepot, ed.rowid, pai.incdec"; + $sql .= $this->db->order("ed.rowid", "DESC"); dol_syslog(get_class($this)."::delete select details", LOG_DEBUG); $resql = $this->db->query($sql); @@ -1554,41 +1747,64 @@ class Expedition extends CommonObject 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 = ''; - // 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 = ''; + // 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, (string) $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, (string) $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 in case 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++; @@ -1596,106 +1812,83 @@ class Expedition extends CommonObject } } - // delete batch expedition line if (!$error) { - $shipmentlinebatch = new ExpeditionLineBatch($this->db); - if ($shipmentlinebatch->deleteFromShipment($this->id) < 0) { + // Delete linked object + $res = $this->deleteObjectLinked(); + if ($res < 0) { $error++; - $this->errors[] = "Error ".$this->db->lasterror(); } - } - if (!$error) { - $main = $this->db->prefix().'expeditiondet'; - $ef = $main."_extrafields"; - $sqlef = "DELETE FROM $ef WHERE fk_object IN (SELECT rowid FROM $main WHERE fk_expedition = ".((int) $this->id).")"; + // delete extrafields + $res = $this->deleteExtraFields(); + if ($res < 0) { + $error++; + } - $sql = "DELETE FROM ".$this->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 (!$error) { + // Delete linked contacts + $res = $this->delete_linked_contact(); if ($res < 0) { $error++; } + } + if (!$error) { + $sql = "DELETE FROM ".$this->db->prefix()."expedition"; + $sql .= " WHERE rowid = ".((int) $this->id); - // delete extrafields - $res = $this->deleteExtraFields(); - if ($res < 0) { - $error++; - } - - if (!$error) { - // Delete linked contacts - $res = $this->delete_linked_contact(); - if ($res < 0) { - $error++; + if ($this->db->query($sql)) { + if (!empty($this->origin) && $this->origin_id > 0) { + $this->fetch_origin(); + $origin_object = $this->origin_object; + '@phan-var-force Facture|Commande $origin_object'; + if ($origin_object->status == 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" + $origin_object->loadExpeditions(); + //var_dump($this->$origin->expeditions);exit; + if (count($origin_object->expeditions) <= 0) { + $origin_object->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_object = $this->origin_object; - '@phan-var-force Facture|Commande $origin_object'; - if ($origin_object->status == 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" - $origin_object->loadExpeditions(); - //var_dump($this->$origin->expeditions);exit; - if (count($origin_object->expeditions) <= 0) { - $origin_object->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(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 (!$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(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; - } - } - } - - 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(); @@ -1890,6 +2083,33 @@ class Expedition extends CommonObject } } + // 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) { + 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_elementdet) { @@ -2479,17 +2699,18 @@ class Expedition extends CommonObject $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 " . $this->db->prefix() . "commandedet as cd,"; - $sql .= " " . $this->db->prefix() . "expeditiondet as ed"; + $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_elementdet"; $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_elementdet"; + //$sql .= " AND cd.rowid = ed.fk_elementdet"; dol_syslog(get_class($this) . "::valid select details", LOG_DEBUG); $resql = $this->db->query($sql); @@ -2505,7 +2726,7 @@ class Expedition extends CommonObject 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; diff --git a/htdocs/expedition/class/expeditionligne.class.php b/htdocs/expedition/class/expeditionligne.class.php index 0ab98517920..2fee97e8d0a 100644 --- a/htdocs/expedition/class/expeditionligne.class.php +++ b/htdocs/expedition/class/expeditionligne.class.php @@ -91,6 +91,11 @@ class ExpeditionLigne extends CommonObjectLine */ public $origin_line_id; + /** + * @var int Id of parent line for children of virtual product + */ + public $fk_parent; + /** * @var string Type of object the fk_element refers to. Example: 'order'. */ @@ -137,6 +142,12 @@ class ExpeditionLigne extends CommonObjectLine */ public $detail_batch; + /** + * Virtual products : array of total of quantities group product id and warehouse id ([id_product][id_warehouse] -> qty (int|float)) + * @var array> + */ + public $detail_children; + /** detail of warehouses and qty * We can use this to know warehouse when there is no lot. * @var stdClass[] @@ -361,7 +372,10 @@ class ExpeditionLigne extends CommonObjectLine $error = 0; // Check parameters - if (empty($this->fk_expedition) || empty($this->fk_elementdet) || !is_numeric($this->qty)) { + if (empty($this->fk_expedition) + || empty($this->fk_product) // product id is mandatory + || (empty($this->fk_elementdet) && empty($this->fk_parent)) // at least origin line id of parent line id is set + || !is_numeric($this->qty)) { $this->error = 'ErrorMandatoryParametersNotProvided'; return -1; } @@ -383,13 +397,17 @@ class ExpeditionLigne extends CommonObjectLine $sql .= "fk_expedition"; $sql .= ", fk_entrepot"; $sql .= ", fk_elementdet"; + $sql .= ", fk_parent"; + $sql .= ", fk_product"; $sql .= ", element_type"; $sql .= ", qty"; $sql .= ", rang"; $sql .= ") VALUES ("; $sql .= $this->fk_expedition; $sql .= ", ".(empty($this->entrepot_id) ? 'NULL' : $this->entrepot_id); - $sql .= ", ".((int) $this->fk_elementdet); + $sql .= ", ".(empty($this->fk_elementdet) ? 'NULL' : $this->fk_elementdet); + $sql .= ", ".(empty($this->fk_parent) ? 'NULL' : $this->fk_parent); + $sql .= ", ".(empty($this->fk_product) ? 'NULL' : $this->fk_product); $sql .= ", '".(empty($this->element_type) ? 'order' : $this->db->escape($this->element_type))."'"; $sql .= ", ".price2num($this->qty, 'MS'); $sql .= ", ".((int) $ranktouse); @@ -418,7 +436,7 @@ class ExpeditionLigne extends CommonObjectLine 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); } } @@ -435,6 +453,69 @@ class ExpeditionLigne extends CommonObjectLine } } + /** + * Find all children + * + * @param int $line_id Line id + * @param stdClass[] $list List of sub-lines for a virtual product line (array of object with attributes : rowid, fk_product, fk_parent, qty, fk_warehouse, batch, eatby, sellby, iskit, incdec) + * @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 = 0; + $line_obj->incdec = 0; + $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. * @@ -448,41 +529,82 @@ class ExpeditionLigne extends CommonObjectLine $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 + $line_id_list = array(); + $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"; + } + } - if (!$this->db->query($sql)) { - $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 ($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) { diff --git a/htdocs/expedition/dispatch.php b/htdocs/expedition/dispatch.php index a96f628aeba..fdcadcfe8eb 100644 --- a/htdocs/expedition/dispatch.php +++ b/htdocs/expedition/dispatch.php @@ -143,35 +143,36 @@ if (empty($reshook)) { 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|productbatch)([0-9]+)_([0-9]+)_([0-9]+)$/i', $key, $reg)) { $pos++; $modebatch = null; - 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 = GETPOSTINT("idline_".$reg[1].'_'.$reg[2]); + $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_".$reg[1].'_'.$reg[2]; // This is unit price including discount + //$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, GETPOSTINT('dluo_'.$reg[1].'_'.$reg[2].'month'), GETPOSTINT('dluo_'.$reg[1].'_'.$reg[2].'day'), GETPOSTINT('dluo_'.$reg[1].'_'.$reg[2].'year')); - $dDLC = dol_mktime(12, 0, 0, GETPOSTINT('dlc_'.$reg[1].'_'.$reg[2].'month'), GETPOSTINT('dlc_'.$reg[1].'_'.$reg[2].'day'), GETPOSTINT('dlc_'.$reg[1].'_'.$reg[2].'year')); + $lot = GETPOST('lot_number'.$dispatch_line_suffix); + $dDLUO = dol_mktime(12, 0, 0, GETPOSTINT('dluo'.$dispatch_line_suffix.'month'), GETPOSTINT('dluo'.$dispatch_line_suffix.'day'), GETPOSTINT('dluo'.$dispatch_line_suffix.'year')); + $dDLC = dol_mktime(12, 0, 0, GETPOSTINT('dlc'.$dispatch_line_suffix.'month'), GETPOSTINT('dlc'.$dispatch_line_suffix.'day'), GETPOSTINT('dlc'.$dispatch_line_suffix.'year')); } $newqty = GETPOSTFLOAT($qty, 'MS'); @@ -241,13 +242,12 @@ if (empty($reshook)) { if (!$error && $modebatch == "batch") { if ($newqty > 0) { - $suffixkeyfordate = preg_replace('/^product_batch/', '', $key); - $sellby = dol_mktime(0, 0, 0, GETPOSTINT('dlc'.$suffixkeyfordate.'month'), GETPOSTINT('dlc'.$suffixkeyfordate.'day'), GETPOSTINT('dlc'.$suffixkeyfordate.'year'), ''); - $eatby = dol_mktime(0, 0, 0, GETPOSTINT('dluo'.$suffixkeyfordate.'month'), GETPOSTINT('dluo'.$suffixkeyfordate.'day'), GETPOSTINT('dluo'.$suffixkeyfordate.'year')); + $suffixkeyfordate = preg_replace('/^productbatch/', '', $key); + $sellby = dol_mktime(12, 0, 0, GETPOSTINT('dlc'.$suffixkeyfordate.'month'), GETPOSTINT('dlc'.$suffixkeyfordate.'day'), GETPOSTINT('dlc'.$suffixkeyfordate.'year'), ''); + $eatby = dol_mktime(12, 0, 0, GETPOSTINT('dluo'.$suffixkeyfordate.'month'), GETPOSTINT('dluo'.$suffixkeyfordate.'day'), GETPOSTINT('dluo'.$suffixkeyfordate.'year')); $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); $objsearchdet = null; @@ -259,10 +259,11 @@ if (empty($reshook)) { if ($objsearchdet) { $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); - $sql .= " , fk_warehouse = ".((int) $warehouse_id); + $sql .= " batch = '".$db->escape($lot)."'"; + $sql .= ", eatby = ".($eatby ? "'".$db->idate($eatby)."'" : "null"); + $sql .= ", sellby = ".($sellby ? "'".$db->idate($sellby)."'" : "null"); + $sql .= ", qty = ".((float) $newqty); + $sql .= ", fk_warehouse = ".((int) $warehouse_id); $sql .= " WHERE rowid = ".((int) $objsearchdet->rowid); } else { $sql = "INSERT INTO ".$db->prefix().$expeditionlinebatch->table_element." ("; @@ -271,7 +272,7 @@ if (empty($reshook)) { $sql .= " '".$db->escape($lot)."', ".((float) $newqty).", 0, ".((int) $warehouse_id).")"; } } else { - $sql = " DELETE FROM ".$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)."'"; } @@ -286,7 +287,11 @@ if (empty($reshook)) { } else { $expeditiondispatch->fk_expedition = $object->id; $expeditiondispatch->entrepot_id = GETPOSTINT($ent); - $expeditiondispatch->fk_elementdet = GETPOSTINT($fk_commandedet); + $expeditiondispatch->fk_parent = GETPOSTINT('fk_parent'.$dispatch_line_suffix); + $expeditiondispatch->fk_product = $prod_id; + if (!($expeditiondispatch->fk_parent > 0)) { + $expeditiondispatch->fk_elementdet = GETPOSTINT($fk_commandedet); + } $expeditiondispatch->qty = $newqty; if ($newqty > 0) { @@ -297,8 +302,8 @@ if (empty($reshook)) { } if ($modebatch == "batch" && !$error) { - $expeditionlinebatch->sellby = $dDLUO; - $expeditionlinebatch->eatby = $dDLC; + $expeditionlinebatch->sellby = $dDLC; // DLC is sellByDate + $expeditionlinebatch->eatby = $dDLUO; // DLUO is eatByDate $expeditionlinebatch->batch = $lot; $expeditionlinebatch->qty = $newqty; $expeditionlinebatch->fk_origin_stock = 0; @@ -793,7 +798,7 @@ if ($object->id > 0 || !empty($object->ref)) { $sql = "SELECT ed.rowid"; $sql .= ", cd.fk_product"; $sql .= ", ".$db->ifsql('eb.rowid IS NULL', 'ed.qty', 'eb.qty')." as qty"; - $sql .= ", ed.fk_entrepot"; + $sql .= ", ".$db->ifsql('eb.rowid IS NULL OR eb.fk_warehouse 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"; @@ -806,167 +811,324 @@ if ($object->id > 0 || !empty($object->ref)) { $j = 0; if ($resultsql) { $numd = $db->num_rows($resultsql); + while ($obj_exp = $db->fetch_object($resultsql)) { + $suffix = "_" . $j . "_" . $i; - while ($j < $numd) { - $suffix = "_".$j."_".$i; - $objd = $db->fetch_object($resultsql); + $productChildrenNb = 0; + $expedition_line_child_list = array(); + if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) { + // virtual product : find all children + $productChildrenNb = $tmpproduct->hasFatherOrChild(1); + if ($productChildrenNb > 0) { + $line_id_list = array(); - if ($is_mod_batch_enabled && (!empty($objd->batch) || (is_null($objd->batch) && $tmpproduct->status_batch > 0))) { - $type = 'batch'; + // 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]; + } - // 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'); + // sub-product is a batch and get selected batch from database or all batches for selected warehouse + $batch_list = array(); + if ($is_mod_batch_enabled && $child_product->hasbatch()) { + // search if batch is not exist in shipment lines + $sql_line_batch_search = "SELECT eb.rowid, eb.qty, eb.batch, eb.sellby, eb.eatby"; + $sql_line_batch_search .= " FROM ".$db->prefix()."expeditiondet_batch as eb"; + $sql_line_batch_search .= " WHERE eb.fk_expeditiondet = ".((int) $line_obj->rowid); + $res_line_batch_search = $db->query($sql_line_batch_search); + if ($res_line_batch_search) { + while ($obj_batch = $db->fetch_object($res_line_batch_search)) { + // set the selected bath by default + if ($obj_batch->batch != '') { + $line_obj->batch = $obj_batch->batch; + } + $obj_batch->eatby = dol_print_date($obj_batch->eatby, 'day'); + $obj_batch->sellby = dol_print_date($obj_batch->sellby, 'day'); + $batch_list[] = $obj_batch; + } + $db->free($res_line_batch_search); + } + + // no batch found for this sub-product so retrieve all batch numbers for this sub-product id and warehouse id + if (empty($batch_list)) { + $batch_sort_field_arr = array(); + $batch_sort_order_arr = array(); + if ($is_sell_by_enabled) { + $batch_sort_field_arr[] = 'pl.sellby'; // order by sell by (DLC) + $batch_sort_order_arr[] = 'ASC'; + } + if ($is_eat_by_enabled) { + $batch_sort_field_arr[] = 'pl.eatby'; // order by eat by (DLUO) + $batch_sort_order_arr[] = 'ASC'; + } + $batch_sort_field_arr[] = 'pb.qty'; // order by qty + $batch_sort_order_arr[] = 'ASC'; + $batch_sort_field_arr[] = 'pl.rowid'; // order by rowid + $batch_sort_order_arr[] = 'ASC'; + $product_batch = new Productbatch($db); + $product_batch_result = $product_batch->findAllForProduct($child_product_id, $line_obj->fk_warehouse, (getDolGlobalInt('STOCK_DISALLOW_NEGATIVE_TRANSFER') ? 0 : null), implode(',', $batch_sort_field_arr), implode(',', $batch_sort_order_arr)); + if (is_array($product_batch_result)) { + foreach ($product_batch_result as $batch_current) { + $batch_current->eatby = dol_print_date($batch_current->eatby, 'day'); + $batch_current->sellby = dol_print_date($batch_current->sellby, 'day'); + $batch_list[] = $batch_current; + } + } + } + } + $line_obj->batch_list = $batch_list; + + // determine if line is virtual product and stock is managed + $line_obj->iskit = 0; + if ($child_product->stockable_product == Product::ENABLED_STOCK) { + $can_manage_stock = 1; + } else { + $can_manage_stock = 0; // the value of "incdec" can't be modified + } + $line_obj->incdec = $can_manage_stock; // set value by default before this request + $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 = ".((int) $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; + if ($can_manage_stock) { + $line_obj->incdec = (int) $child_obj->incdec; // reset value to 0 or 1 if stock can be managed + } + } + $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 ''; - - print ''; - print ''; - print ''; - print ''; - print ''; - print ''; - - print ''; - print ''; - - print ''; - - print ''; - print ''; - //print ''; - print ''; - if ($is_sell_by_enabled) { - print ''; - $dlcdatesuffix = !empty($objd->sellby) ? dol_stringtotime($objd->sellby) : dol_mktime(0, 0, 0, GETPOSTINT('dlc'.$suffix.'month'), GETPOSTINT('dlc'.$suffix.'day'), GETPOSTINT('dlc'.$suffix.'year')); - print $form->selectDate($dlcdatesuffix, 'dlc'.$suffix, 0, 0, 1, ''); - print ''; - } - if ($is_eat_by_enabled) { - print ''; - $dluodatesuffix = !empty($objd->eatby) ? dol_stringtotime($objd->eatby) : dol_mktime(0, 0, 0, GETPOSTINT('dluo'.$suffix.'month'), GETPOSTINT('dluo'.$suffix.'day'), GETPOSTINT('dluo'.$suffix.'year')); - print $form->selectDate($dluodatesuffix, 'dluo'.$suffix, 0, 0, 1, ''); - print ''; - } - print ' '; // Supplier ref + Qty ordered + qty already dispatched - } else { - $type = 'dispatch'; - $colspan = 6; - $colspan = $is_sell_by_enabled ? $colspan : --$colspan; - $colspan = $is_eat_by_enabled ? $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 ''; } - // Qty to dispatch - print ''; - print ''.img_picto($langs->trans("Reset"), 'eraser', 'class="pictofixedwidth opacitymedium"').''; - $suggestedvalue = (GETPOSTISSET('qty'.$suffix) ? GETPOSTFLOAT('qty'.$suffix) : $objd->qty); - //var_dump($suggestedvalue);exit; - print ''; - print ''; - print ''; - if ($is_mod_batch_enabled && $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 ($objp->stockable_product == Product::ENABLED_STOCK) { - 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, array(), '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, array(), 'csswarehouse'.$suffix); + if (empty($expedition_line_child_list)) { + $obj_exp->iskit = 0; // is not virtual product + // manage stock if enabled for product + if ($objp->stockable_product == Product::ENABLED_STOCK) { + $obj_exp->incdec = 1; } else { - $langs->load("errors"); - print $langs->trans("ErrorNoWarehouseDefined"); + $obj_exp->incdec = 0; } - } else { - // on force l'entrepot pour passer le test d'ajout de ligne dans expedition.class.php - print ''; - print img_warning().' '.$langs->trans('StockDisabled'); + $expedition_line_child_list[] = $obj_exp; } - 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'); + $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; + + // set default batch values for this dispatched line (lot/serial number of virtual product) + $dispatch_line_batch_current = null; + if (!empty($objd->batch_list)) { + $dispatch_line_batch_count = count($objd->batch_list); + // if only one batch found, this batch is pre-selected + if ($dispatch_line_batch_count == 1) { + $dispatch_line_batch_current = current($objd->batch_list); + } + } + if (is_object($dispatch_line_batch_current)) { + $objd->batch = $dispatch_line_batch_current->batch; + $objd->eatby = $dispatch_line_batch_current->eatby; + $objd->sellby = $dispatch_line_batch_current->sellby; + } + + if ($is_mod_batch_enabled + && ( + !empty($objd->batch) + || (is_null($objd->batch) && $tmpproduct->status_batch > 0) + || !empty($objd->batch_list) + ) + ) { + $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 ''; + if (!empty($objd->html_label)) { + print $objd->html_label; + } + print ''; + + print ''; + print ''; + print $formproduct->selectLotDataList('lot_number'.$suffix, 0, $objd->fk_product, GETPOST("entrepot".$suffix) ? GETPOST("entrepot".$suffix) : $objd->fk_warehouse, array()); + print ''; + + if ($is_sell_by_enabled) { + print ''; + $dlcdatesuffix = !empty($objd->sellby) ? dol_stringtotime($objd->sellby) : dol_mktime(0, 0, 0, GETPOSTINT('dlc'.$suffix.'month'), GETPOSTINT('dlc'.$suffix.'day'), GETPOSTINT('dlc'.$suffix.'year')); + print $form->selectDate($dlcdatesuffix, 'dlc'.$suffix, 0, 0, 1, ''); + print ''; + } + if ($is_eat_by_enabled) { + print ''; + $dluodatesuffix = !empty($objd->eatby) ? dol_stringtotime($objd->eatby) : dol_mktime(0, 0, 0, GETPOSTINT('dluo'.$suffix.'month'), GETPOSTINT('dluo'.$suffix.'day'), GETPOSTINT('dluo'.$suffix.'year')); + print $form->selectDate($dluodatesuffix, 'dluo'.$suffix, 0, 0, 1, ''); + print ''; + } + print ' '; // Supplier ref + Qty ordered + qty already dispatched + } else { + $type = 'dispatch'; + $colspan = 6; + $colspan = $is_sell_by_enabled ? $colspan : --$colspan; + $colspan = $is_eat_by_enabled ? $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) ? GETPOSTFLOAT('qty'.$suffix) : $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 ''; + if ($can_update_stock) { + print img_picto($langs->trans('AddStockLocationLine'), 'split.png', 'class="splitbutton" onClick="addDispatchLine('.$i.', \''.$type.'-'.$child_line_id.'\')"'); + } + 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, $objd->fk_product, '', 1, 0, array(), 'csswarehouse'.$suffix); + } elseif (count($listwarehouses) == 1) { + print $formproduct->selectWarehouses(GETPOST("entrepot".$suffix) ? GETPOST("entrepot".$suffix) : $objd->fk_warehouse, "entrepot".$suffix, '', 0, 0, $objd->fk_product, '', 1, 0, array(), 'csswarehouse'.$suffix); + } else { + $langs->load("errors"); + print $langs->trans("ErrorNoWarehouseDefined"); + } + } else { + // on force l'entrepot pour passer le test d'ajout de ligne dans expedition.class.php + print ''; + print img_warning().' '.$langs->trans('StockDisabled'); + } + 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, + 'objd' => $objd, + ); + $reshook = $hookmanager->executeHooks( + 'printFieldListValue', + $parameters, + $object, + $action + ); + if ($reshook < 0) { + setEventMessages($hookmanager->error, $hookmanager->errors, 'errors'); + } + print $hookmanager->resPrint; + + print "\n"; } - print $hookmanager->resPrint; - print "\n"; $j++; - $numline++; } - $suffix = "_".$j."_".$i; + + //$suffix = "_".$j."_".$i; } else { $errorMsg = 'Shipment dispatch SQL error : '.$db->lasterror(); setEventMessage($errorMsg, 'errors'); dol_syslog($errorMsg, LOG_ERR); } + /* if ($j == 0) { if ($is_mod_batch_enabled && !empty($objp->tobatch)) { $type = 'batch'; @@ -997,7 +1159,7 @@ if ($object->id > 0 || !empty($object->ref)) { print ''; print ''; print ''; - print ''; + print ''; print ''; print ''; @@ -1110,10 +1272,128 @@ if ($object->id > 0 || !empty($object->ref)) { print $hookmanager->resPrint; print "\n"; } + */ } } $i++; } + + // reload batch select and warehouse select on change (Ajax) + $out_js_line_list = array(); + $out_js_line = 'function updateselectbatchbywarehouse() {'; + $out_js_line .= ' jQuery(document).on("change", "select[name*=\"entrepot\"]", function() {'; + $out_js_line .= ' var selectwarehouse = jQuery(this);'; + $out_js_line .= ' var selectbatch_name = selectwarehouse.attr("name").replace("entrepot", "lot_number");'; + $out_js_line .= ' var selectbatch = jQuery("datalist[id*=\""+selectbatch_name+"\"]");'; + $out_js_line .= ' var selectedbatch = selectbatch.val();'; + $out_js_line .= ' var product_element_name = selectwarehouse.attr("name").replace("entrepot", "productbatch");'; + $out_js_line .= ' jQuery.ajax({'; + $out_js_line .= ' type: "POST",'; + $out_js_line .= ' url: "'.dol_escape_js(dol_buildpath('/expedition/ajax/interface.php', 1)).'",'; + $out_js_line .= ' data: {'; + $out_js_line .= ' action: "updateselectbatchbywarehouse",'; + $out_js_line .= ' warehouse_id: jQuery(this).val(),'; + $out_js_line .= ' token: "'.currentToken().'",'; + $out_js_line .= ' product_id: jQuery("input[name=\""+product_element_name+"\"]").val()'; + $out_js_line .= ' }'; + $out_js_line .= ' }).done(function(data) {'; + $out_js_line .= ' selectbatch.empty();'; + $out_js_line .= ' if (typeof data == "object") {'; + $out_js_line .= ' console.log("data is already type object, no need to parse it");'; + $out_js_line .= ' } else {'; + $out_js_line .= ' console.log("data is type "+(typeof data));'; + $out_js_line .= ' data = JSON.parse(data);'; + $out_js_line .= ' }'; + $out_js_line .= ' selectbatch.append(jQuery("