diff --git a/htdocs/core/lib/product.lib.php b/htdocs/core/lib/product.lib.php index 4fe4ad6c9dfec..6c755b7d772b0 100644 --- a/htdocs/core/lib/product.lib.php +++ b/htdocs/core/lib/product.lib.php @@ -130,7 +130,7 @@ function product_prepare_head($object) $h++; } - if ($object->isProduct() || ($object->isService() && getDolGlobalString('STOCK_SUPPORTS_SERVICES'))) { // If physical product we can stock (or service with option) + if (($object->isProduct() || ($object->isService() && getDolGlobalString('STOCK_SUPPORTS_SERVICES'))) && $object->stockable_product == Product::ENABLED_STOCK) { // If physical product we can stock (or service with option) if (isModEnabled('stock') && $user->hasRight('stock', 'lire')) { $head[$h][0] = DOL_URL_ROOT."/product/stock/product.php?id=".$object->id; $head[$h][1] = $langs->trans("Stock"); diff --git a/htdocs/expedition/card.php b/htdocs/expedition/card.php index 6541faf65616b..fb74ca425934c 100644 --- a/htdocs/expedition/card.php +++ b/htdocs/expedition/card.php @@ -270,6 +270,7 @@ $subtotalqty = 0; $j = 0; + $batch = "batchl".$i."_0"; $stockLocation = "ent1".$i."_0"; $qty = "qtyl".$i; @@ -342,6 +343,30 @@ $qty = "qtyl".$i.'_'.$j; } } else { + $p = new Product($db); + $res = $p->fetch($objectsrc->lines[$i]->fk_product); + if ($res > 0) { + if (GETPOST('entrepot_id', 'int') == -1) { + $qty .= '_'.$j; + } + + if ($p->stockable_product == Product::DISABLED_STOCK) { + $w = new Entrepot($db); + $Tw = $w->list_array(); + if (count($Tw) > 0) { + $w_Id = array_keys($Tw); + $stockLine[$i][$j]['qty'] = GETPOST($qty, 'int'); + + // lorsque que l'on a le stock désactivé sur un produit/service + // on force l'entrepot pour passer le test d'ajout de ligne dans expedition.class.php + // + $stockLine[$i][$j]['warehouse_id'] = $w_Id[0]; + $stockLine[$i][$j]['ix_l'] = GETPOST($idl, 'int'); + } else { + setEventMessage($langs->trans('NoWarehouseInBase')); + } + } + } //shipment line for product with no batch management and no multiple stock location if (GETPOSTINT($qty) > 0) { $totalqty += price2num(GETPOST($qty, 'alpha'), 'MS'); @@ -1234,7 +1259,7 @@ $text = $product_static->getNomUrl(1); $text .= ' - '.(!empty($line->label) ? $line->label : $line->product_label); $description = ($showdescinproductdesc ? '' : dol_htmlentitiesbr($line->desc)); - + $description .= empty($product->stockable_product) ? $langs->trans('StockDisabled') : $langs->trans('StockEnabled'); print $form->textwithtooltip($text, $description, 3, '', '', $i); // Show range @@ -1344,8 +1369,11 @@ if (!getDolGlobalInt('STOCK_ALLOW_NEGATIVE_TRANSFER')) { $stockMin = 0; } - print $formproduct->selectWarehouses($tmpentrepot_id, 'entl'.$indiceAsked, '', 1, 0, $line->fk_product, '', 1, 0, array(), 'minwidth200', '', 1, $stockMin, 'stock DESC, e.ref'); - + if ($product->stockable_product == Product::ENABLED_STOCK) { + print $formproduct->selectWarehouses($tmpentrepot_id, 'entl'.$indiceAsked, '', 1, 0, $line->fk_product, '', 1, 0, array(), 'minwidth200', '', 1, $stockMin, 'stock DESC, e.ref'); + } else { + print img_warning().' '.$langs->trans('StockDisabled'); + } if ($tmpentrepot_id > 0 && $tmpentrepot_id == $warehouse_id) { //print $stock.' '.$quantityToBeDelivered; if ($stock < $quantityToBeDelivered) { @@ -1576,10 +1604,13 @@ if (isModEnabled('stock')) { print ''; if ($line->product_type == Product::TYPE_PRODUCT || getDolGlobalString('STOCK_SUPPORTS_SERVICES')) { - print $tmpwarehouseObject->getNomUrl(0).' '; - - print ''; - print '('.$stock.')'; + if ($product->stockable_product == Product::ENABLED_STOCK) { + print $tmpwarehouseObject->getNomUrl(0).' '; + print ''; + print '('.$stock.')'; + } else { + print img_warning().' '.$langs->trans('StockDisabled'); + } } else { print '('.$langs->trans("Service").')'; } @@ -1739,6 +1770,10 @@ if ($warehouse_selected_id <= 0) { // We did not force a given warehouse, so we won't have no warehouse to change qty. $disabled = 'disabled="disabled"'; } + // finally we overwrite the input with the product status stockable_product if it's disabled + if ($product->stockable_product == Product::DISABLED_STOCK) { + $disabled = ''; + } print ' '; if (empty($disabled) && getDolGlobalString('STOCK_ALLOW_NEGATIVE_TRANSFER')) { print ''; @@ -1756,7 +1791,11 @@ print img_warning().' '.$langs->trans("NoProductToShipFoundIntoStock", $warehouseObject->label); } else { if ($line->fk_product) { - print img_warning().' '.$langs->trans("StockTooLow"); + if ($product->stockable_product == Product::ENABLED_STOCK) { + print img_warning().' '.$langs->trans('StockTooLow'); + } else { + print img_warning().' '.$langs->trans('StockDisabled'); + } } else { print ''; } @@ -2364,6 +2403,7 @@ $product_static->surface_units = $lines[$i]->surface_units; $product_static->volume = $lines[$i]->volume; $product_static->volume_units = $lines[$i]->volume_units; + $product_static->stockable_product = $lines[$i]->stockable_product; $text = $product_static->getNomUrl(1); $text .= ' - '.$label; @@ -2534,7 +2574,7 @@ print ''; if ($lines[$i]->product_type == Product::TYPE_SERVICE && getDolGlobalString('SHIPMENT_SUPPORTS_SERVICES')) { print '('.$langs->trans("Service").')'; - } elseif ($lines[$i]->entrepot_id > 0) { + } elseif ($lines[$i]->entrepot_id > 0 && $lines[$i]->stockable_product == Product::ENABLED_STOCK) { $entrepot = new Entrepot($db); $entrepot->fetch($lines[$i]->entrepot_id); print $entrepot->getNomUrl(1); diff --git a/htdocs/expedition/class/expedition.class.php b/htdocs/expedition/class/expedition.class.php index ebfefa24a4441..76e41f4842ce9 100644 --- a/htdocs/expedition/class/expedition.class.php +++ b/htdocs/expedition/class/expedition.class.php @@ -976,7 +976,7 @@ public function addline($entrepot_id, $id, $qty, $array_options = []) $isavirtualproduct = ($product->hasFatherOrChild(1) > 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) { + if ($product_stock < $qty && $product->stockable_product == Product::ENABLED_STOCK) { $langs->load("errors"); $this->error = $langs->trans('ErrorStockIsNotEnoughToAddProductOnShipment', $product->ref); $this->errorhidden = 'ErrorStockIsNotEnoughToAddProductOnShipment'; @@ -1632,7 +1632,9 @@ public function fetch_lines() $sql .= ", cd.fk_multicurrency, cd.multicurrency_code, cd.multicurrency_subprice, cd.multicurrency_total_ht, cd.multicurrency_total_tva, cd.multicurrency_total_ttc, cd.rang, cd.date_start, cd.date_end"; $sql .= ", ed.rowid as line_id, ed.qty as qty_shipped, ed.fk_element, ed.fk_elementdet, ed.element_type, ed.fk_entrepot"; $sql .= ", p.ref as product_ref, p.label as product_label, p.fk_product_type, p.barcode as product_barcode"; - $sql .= ", p.weight, p.weight_units, p.length, p.length_units, p.width, p.width_units, p.height, p.height_units, p.surface, p.surface_units, p.volume, p.volume_units, p.tosell as product_tosell, p.tobuy as product_tobuy, p.tobatch as product_tobatch"; + $sql .= ", p.weight, p.weight_units, p.length, p.length_units, p.width, p.width_units, p.height, p.height_units"; + $sql .= ", p.surface, p.surface_units, p.volume, p.volume_units, p.tosell as product_tosell, p.tobuy as product_tobuy"; + $sql .= ", p.tobatch as product_tobatch, p.stockable_product"; $sql .= " FROM ".MAIN_DB_PREFIX."expeditiondet as ed, ".MAIN_DB_PREFIX."commandedet as cd"; $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product"; $sql .= " WHERE ed.fk_expedition = ".((int) $this->id); @@ -1695,6 +1697,7 @@ public function fetch_lines() $line->fk_expedition = $this->id; // id of parent + $line->stockable_product = $obj->stockable_product; $line->product_type = $obj->product_type; $line->fk_product = $obj->fk_product; $line->fk_product_type = $obj->fk_product_type; @@ -1722,8 +1725,9 @@ public function fetch_lines() $line->surface = $obj->surface; $line->surface_units = $obj->surface_units; $line->volume = $obj->volume; - $line->volume_units = $obj->volume_units; - $line->fk_unit = $obj->fk_unit; + $line->volume_units = $obj->volume_units; + $line->stockable_product = $obj->stockable_product; + $line->fk_unit = $obj->fk_unit; $line->pa_ht = $obj->pa_ht; @@ -2825,6 +2829,13 @@ class ExpeditionLigne extends CommonObjectLine public $volume; public $volume_units; + /** + * 0=This service or product is not managed in stock, 1=This service or product is managed in stock + * + * @var boolean + */ + public $stockable_product = true; + // Invoicing public $remise_percent; public $tva_tx; diff --git a/htdocs/expedition/dispatch.php b/htdocs/expedition/dispatch.php index cd5c703835a25..5993d119aabad 100644 --- a/htdocs/expedition/dispatch.php +++ b/htdocs/expedition/dispatch.php @@ -572,7 +572,7 @@ //$sql = "SELECT l.rowid, l.fk_product, l.subprice, l.remise_percent, l.ref AS sref, SUM(l.qty) as qty,"; $sql = "SELECT l.rowid, l.fk_product, l.subprice, l.remise_percent, '' AS sref, l.qty as qty,"; - $sql .= " p.ref, p.label, p.tobatch, p.fk_default_warehouse, p.barcode"; + $sql .= " p.ref, p.label, p.tobatch, p.fk_default_warehouse, p.barcode, p.stockable_product"; // Enable hooks to alter the SQL query (SELECT) $parameters = array(); $reshook = $hookmanager->executeHooks( @@ -901,13 +901,19 @@ // 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); + 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, 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"); + } } else { - $langs->load("errors"); - print $langs->trans("ErrorNoWarehouseDefined"); + // 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"; @@ -1160,7 +1166,7 @@ var errortab3 = []; var errortab4 = []; - function barcodescannerjs(){ + function barcodescannerjs() { console.log("We catch inputs in scanner box"); jQuery("#scantoolmessage").text(); @@ -1177,11 +1183,11 @@ function barcodescannerjs(){ errortab3 = []; errortab4 = []; - textarray = textarray.filter(function(value){ + textarray = textarray.filter(function(value) { return value != ""; }); - if(textarray.some((element) => element != "")){ - $(".qtydispatchinput").each(function(){ + if (textarray.some((element) => element != "")) { + $(".qtydispatchinput").each(function() { id = $(this).attr(\'id\'); idarray = id.split(\'_\'); idproduct = idarray[2]; @@ -1192,7 +1198,7 @@ function barcodescannerjs(){ productbarcode = $("#product_"+idproduct).attr(\'data-barcode\'); console.log(productbarcode); productbatchcode = $("#lot_number_"+id).val(); - if(productbatchcode == undefined){ + if (productbatchcode == undefined) { productbatchcode = ""; } console.log(productbatchcode); @@ -1200,25 +1206,25 @@ function barcodescannerjs(){ if (barcodemode != "barcodeforproduct") { tabproduct.forEach(product=>{ console.log("product.Batch="+product.Batch+" productbatchcode="+productbatchcode); - if(product.Batch != "" && product.Batch == productbatchcode){ + if (product.Batch != "" && product.Batch == productbatchcode) { console.log("duplicate batch code found for batch code "+productbatchcode); duplicatedbatchcode.push(productbatchcode); } }) } productinput = $("#qty_"+id).val(); - if(productinput == ""){ + if (productinput == "") { productinput = 0 } tabproduct.push({\'Id\':id,\'Warehouse\':warehouse,\'Barcode\':productbarcode,\'Batch\':productbatchcode,\'Qty\':productinput,\'fetched\':false}); }); console.log("Loop on each record entered in the textarea"); - textarray.forEach(function(element,index){ + textarray.forEach(function(element,index) { console.log("Process record element="+element+" id="+id); var verify_batch = false; var verify_barcode = false; - switch(barcodemode){ + switch(barcodemode) { case "barcodeforautodetect": verify_barcode = barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,warehousetouse,selectaddorreplace,"barcode",true); verify_batch = barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,warehousetouse,selectaddorreplace,"lotserial",true); @@ -1249,12 +1255,12 @@ function barcodescannerjs(){ if (Object.keys(errortab1).length < 1 && Object.keys(errortab2).length < 1 && Object.keys(errortab3).length < 1) { tabproduct.forEach(product => { - if(product.Qty!=0){ - if(product.hasOwnProperty("reelqty")){ + if (product.Qty!=0) { + if (product.hasOwnProperty("reelqty")) { idprod = $("td[data-idproduct=\'"+product.fk_product+"\']").attr("id"); idproduct = idprod.split("_")[1]; console.log("We create a new line for product_"+idproduct); - if(product.Barcode != null){ + if (product.Barcode != null) { modedispatch = "dispatch"; } else { modedispatch = "batch"; @@ -1266,7 +1272,7 @@ function barcodescannerjs(){ $("#qty_"+(nbrTrs-1)+"_"+idproduct).val(product.Qty); $("#entrepot_"+(nbrTrs-1)+"_"+idproduct).val(product.Warehouse); - if(modedispatch == "batch"){ + if (modedispatch == "batch") { $("#lot_number_"+(nbrTrs-1)+"_"+idproduct).val(product.Batch); } @@ -1317,7 +1323,7 @@ function barcodescannerjs(){ } /* This methode is called by parent barcodescannerjs() */ - function barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,warehousetouse,selectaddorreplace,mode,autodetect=false){ + function barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,warehousetouse,selectaddorreplace,mode,autodetect=false) { BarcodeIsInProduct=0; newproductrow=0 result=false; @@ -1327,13 +1333,13 @@ function barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,ware type: \'POST\', async: false, success: function(response) { - if (response.status == "success"){ + if (response.status == "success") { console.log(response.message); - if(!newproductrow){ + if (!newproductrow) { newproductrow = response.object; } }else{ - if (mode!="lotserial" && autodetect==false && !errortab4.includes(element)){ + if (mode!="lotserial" && autodetect==false && !errortab4.includes(element)) { errortab4.push(element); console.error(response.message); } @@ -1344,18 +1350,18 @@ function barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,ware }, }); console.log("Product "+(index+=1)+": "+element); - if(mode == "barcode"){ + if (mode == "barcode") { testonproduct = product.Barcode - }else if (mode == "lotserial"){ + }else if (mode == "lotserial") { testonproduct = product.Batch } testonwarehouse = product.Warehouse; - if(testonproduct == element && testonwarehouse == warehousetouse){ - if(selectaddorreplace == "add"){ + if (testonproduct == element && testonwarehouse == warehousetouse) { + if (selectaddorreplace == "add") { productqty = parseInt(product.Qty,10); product.Qty = productqty + parseInt(barcodeproductqty,10); - }else if(selectaddorreplace == "replace"){ - if(product.fetched == false){ + }else if (selectaddorreplace == "replace") { + if (product.fetched == false) { product.Qty = barcodeproductqty product.fetched=true }else{ @@ -1366,11 +1372,11 @@ function barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,ware BarcodeIsInProduct+=1; } }) - if(BarcodeIsInProduct==0 && newproductrow!=0){ + if (BarcodeIsInProduct==0 && newproductrow!=0) { tabproduct.push({\'Id\':tabproduct.length-1,\'Warehouse\':newproductrow.fk_warehouse,\'Barcode\':mode=="barcode"?element:null,\'Batch\':mode=="lotserial"?element:null,\'Qty\':barcodeproductqty,\'fetched\':true,\'reelqty\':newproductrow.reelqty,\'fk_product\':newproductrow.fk_product,\'mode\':mode}); result = true; } - if(BarcodeIsInProduct > 0){ + if (BarcodeIsInProduct > 0) { result = true; } return result; @@ -1394,7 +1400,7 @@ function barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,ware $("#autoreset").click(function() { console.log("we click on autoreset"); - $(".autoresettr").each(function(){ + $(".autoresettr").each(function() { id = $(this).attr("name"); idtab = id.split("_"); console.log("we process line "+id+" "+idtab); @@ -1417,8 +1423,8 @@ function barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,ware return false; }); - $("#resetalltoexpected").click(function(){ - $(".qtydispatchinput").each(function(){ + $("#resetalltoexpected").click(function() { + $(".qtydispatchinput").each(function() { console.log("We reset to expected "+$(this).attr("id")+" qty to dispatch"); $(this).val($(this).data("expected")); }); diff --git a/htdocs/langs/en_US/products.lang b/htdocs/langs/en_US/products.lang index 3fbe6417b08e3..316c829b7da4e 100644 --- a/htdocs/langs/en_US/products.lang +++ b/htdocs/langs/en_US/products.lang @@ -435,3 +435,7 @@ AllowStockMovementVariantParentHelp=By default, a parent of a variant is a virtu ConfirmSetToDraftInventory=Are you sure you want to go back to Draft status?
The quantities currently set in the inventory will be reset. WarningLineProductNotToSell=Product or service "%s" is not to sell and was cloned PriceLabel=Price label +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 diff --git a/htdocs/langs/en_US/sendings.lang b/htdocs/langs/en_US/sendings.lang index 71d4214c8a5fc..f47172723a6dd 100644 --- a/htdocs/langs/en_US/sendings.lang +++ b/htdocs/langs/en_US/sendings.lang @@ -66,6 +66,7 @@ NoLineGoOnTabToAddSome=No line, go on tab "%s" to add CreateInvoiceForThisCustomerFromSendings=Bill sendings IfValidateInvoiceIsNoSendingStayUnbilled=If invoice validation is 'No', the sending will remain to status 'Unbilled' until the invoice is validated. OptionToSetSendingBilledNotEnabled=Option from module Workflow, to set sending to 'Billed' automatically when invoice is validated, is not enabled, so you will have to set the status of sendings to 'Billed' manually after the invoice has been generated. +NoWarehouseInBase=No warehouse in base # Sending methods # ModelDocument diff --git a/htdocs/langs/fr_FR/products.lang b/htdocs/langs/fr_FR/products.lang index f9ae44a96b99c..5fc57191ffeac 100644 --- a/htdocs/langs/fr_FR/products.lang +++ b/htdocs/langs/fr_FR/products.lang @@ -435,3 +435,7 @@ AllowStockMovementVariantParentHelp=Par défaut, un parent d'une variante est un ConfirmSetToDraftInventory=Êtes-vous sûr de vouloir revenir à l'état de brouillon ?
Les quantités actuellement définies dans l'inventaire seront réinitialisées. WarningLineProductNotToSell=Le produit ou le service "%s" n'est pas à vendre et a été cloné. PriceLabel=Étiquette de prix +StockableProduct=Activer la gestion des stocks +StockableProductDescription=Si cette option est désactivée, la modification du stock pour cet élément n'est pas prise en compte. Les mouvements de stock ne seront pas pris en compte dans les commandes client, commande fournisseur, expédition, réception, ordre de fabrication.
Dans les faits, cette option se comporte comme une activation/désactivation du module stock mais à l'échelle du produit/service +StockDisabled=Stock désactivé +StockEnabled=Stock activé \ No newline at end of file diff --git a/htdocs/product/card.php b/htdocs/product/card.php index c418ac59c21a0..b894f6ea1ac15 100644 --- a/htdocs/product/card.php +++ b/htdocs/product/card.php @@ -632,6 +632,9 @@ $object->fk_unit = null; } + // managed_in_stock + $object->stockable_product = ($type == 0 || ($type == 1 && !empty($conf->global->STOCK_SUPPORTS_SERVICES))) ? 1 : 0; + $accountancy_code_sell = GETPOST('accountancy_code_sell', 'alpha'); $accountancy_code_sell_intra = GETPOST('accountancy_code_sell_intra', 'alpha'); $accountancy_code_sell_export = GETPOST('accountancy_code_sell_export', 'alpha'); @@ -810,6 +813,9 @@ $object->fk_default_bom = 0; } + // managed_in_stock + $object->stockable_product = GETPOSTISSET('stockable_product'); + $units = GETPOSTINT('units'); if ($units > 0) { $object->fk_unit = $units; @@ -1983,7 +1989,7 @@ $(document).ready(function() { console.log($("#statusBatchWarning")) $("#status_batch").on("change", function() { - if ($("#status_batch")[0].value == 0){ + if ($("#status_batch")[0].value == 0) { $("#statusBatchMouvToGlobal").show() } else { $("#statusBatchMouvToGlobal").hide() @@ -1997,7 +2003,7 @@ $(document).ready(function() { console.log($("#statusBatchWarning")) $("#status_batch").on("change", function() { - if ($("#status_batch")[0].value == 2){ + if ($("#status_batch")[0].value == 2) { $("#statusBatchWarning").show() } else { $("#statusBatchWarning").hide() @@ -2151,6 +2157,10 @@ print ''; print ''; */ + + print '' . $langs->trans("StockableProduct") . ''; + $checked = $object->stockable_product == 1 ? "checked" : ""; + print ''; } if ($object->isService() && isModEnabled('workstation')) { @@ -2183,6 +2193,12 @@ print ''; print ''; + + if (!empty($conf->stock->enabled) && !empty($conf->global->STOCK_SUPPORTS_SERVICES)) { + print '' . $langs->trans("StockableProduct") . ''; + $checked = $object->stockable_product == 1 ? "checked" : ""; + print ''; + } } else { if (!getDolGlobalString('PRODUCT_DISABLE_NATURE')) { // Nature @@ -2665,6 +2681,12 @@ print ''; } + // View stockable_product + if (($object->isProduct() || ($object->isService() && !empty($conf->global->STOCK_SUPPORTS_SERVICES))) && !empty($conf->stock->enabled)) { + print '' . $form->textwithpicto($langs->trans("StockableProduct"), $langs->trans('StockableProductDescription')) . ''; + print 'stockable_product == 1 ? 'checked' : '').'>'; + } + // Parent product. if (isModEnabled('variants') && ($object->isProduct() || $object->isService())) { $combination = new ProductCombination($db); diff --git a/htdocs/product/class/product.class.php b/htdocs/product/class/product.class.php index e9a89f7e33783..e035c9be7cac0 100644 --- a/htdocs/product/class/product.class.php +++ b/htdocs/product/class/product.class.php @@ -545,6 +545,12 @@ class Product extends CommonObject public $mandatory_period; + /** + * 0=This service or product is not managed in stock, 1=This service or product is managed in stock + * + * @var boolean + */ + public $stockable_product = true; /** * 'type' if the field format ('integer', 'integer:ObjectClass:PathToClass[:AddCreateButtonOrNot[:Filter]]', 'varchar(x)', 'double(24,8)', 'real', 'price', 'text', 'html', 'date', 'datetime', 'timestamp', 'duration', 'mail', 'phone', 'url', 'password') @@ -598,6 +604,7 @@ class Product extends CommonObject //'tosell' =>array('type'=>'integer', 'label'=>'Status', 'enabled'=>1, 'visible'=>1, 'notnull'=>1, 'default'=>'0', 'index'=>1, 'position'=>1000, 'arrayofkeyval'=>array(0=>'Draft', 1=>'Active', -1=>'Cancel')), //'tobuy' =>array('type'=>'integer', 'label'=>'Status', 'enabled'=>1, 'visible'=>1, 'notnull'=>1, 'default'=>'0', 'index'=>1, 'position'=>1000, 'arrayofkeyval'=>array(0=>'Draft', 1=>'Active', -1=>'Cancel')), 'mandatory_period' => array('type' => 'integer', 'label' => 'mandatoryperiod', 'enabled' => 1, 'visible' => 1, 'notnull' => 1, 'default' => '0', 'index' => 1, 'position' => 1000), + 'stockable_product' => array('type' => 'integer', 'label' => 'stockable_product', 'enabled' => 1, 'visible' => 1, 'default' => 1, 'notnull' => 1, 'index' => 1, 'position' => 502), ); /** @@ -609,6 +616,13 @@ class Product extends CommonObject */ const TYPE_SERVICE = 1; + /** + * Stockable product + */ + const NOT_MANAGED_IN_STOCK = 0; + const DISABLED_STOCK = 0; + const ENABLED_STOCK = 1; + /** * Constructor * @@ -714,6 +728,9 @@ public function create($user, $notrigger = 0) if (empty($this->status_buy)) { $this->status_buy = 0; } + if (empty($this->stockable_product)) { + $this->stockable_product = false; + } $price_ht = 0; $price_ttc = 0; @@ -843,6 +860,7 @@ public function create($user, $notrigger = 0) $sql .= ", batch_mask"; $sql .= ", fk_unit"; $sql .= ", mandatory_period"; + $sql .= ", stockable_product"; $sql .= ") VALUES ("; $sql .= "'".$this->db->idate($this->date_creation)."'"; $sql .= ", ".(!empty($this->entity) ? (int) $this->entity : (int) $conf->entity); @@ -874,6 +892,7 @@ public function create($user, $notrigger = 0) $sql .= ", '".$this->db->escape($this->batch_mask)."'"; $sql .= ", ".($this->fk_unit > 0 ? ((int) $this->fk_unit) : 'NULL'); $sql .= ", '".$this->db->escape($this->mandatory_period)."'"; + $sql .= ", ".((int) $this->stockable_product); $sql .= ")"; dol_syslog(get_class($this)."::Create", LOG_DEBUG); @@ -1154,6 +1173,10 @@ public function update($id, $user, $notrigger = 0, $action = 'update', $updatety $this->state_id = 0; } + if (empty($this->stockable_product)) { + $this->stockable_product = false; + } + // Barcode value $this->barcode = (empty($this->barcode) ? '' : trim($this->barcode)); @@ -1313,7 +1336,9 @@ public function update($id, $user, $notrigger = 0, $action = 'update', $updatety $sql .= ", price_autogen = ".(!$this->price_autogen ? 0 : 1); $sql .= ", fk_price_expression = ".($this->fk_price_expression != 0 ? (int) $this->fk_price_expression : 'NULL'); $sql .= ", fk_user_modif = ".($user->id > 0 ? $user->id : 'NULL'); - $sql .= ", mandatory_period = ".($this->mandatory_period); + $sql .= ", mandatory_period = ".($this->mandatory_period ); + $sql .= ", stockable_product = ".(int) $this->stockable_product; + // stock field is not here because it is a denormalized value from product_stock. $sql .= " WHERE rowid = ".((int) $id); @@ -2587,7 +2612,7 @@ public function fetch($id = 0, $ref = '', $ref_ext = '', $barcode = '', $ignore_ $sql .= " p.pmp,"; } $sql .= " p.datec, p.tms, p.import_key, p.entity, p.desiredstock, p.tobatch, p.sell_or_eat_by_mandatory, p.batch_mask, p.fk_unit,"; - $sql .= " p.fk_price_expression, p.price_autogen, p.model_pdf,"; + $sql .= " p.fk_price_expression, p.price_autogen, p.stockable_product, p.model_pdf,"; $sql .= " p.price_label,"; if ($separatedStock) { $sql .= " SUM(sp.reel) as stock"; @@ -2703,12 +2728,12 @@ public function fetch($id = 0, $ref = '', $ref_ext = '', $barcode = '', $ignore_ $this->height = $obj->height; $this->height_units = $obj->height_units; - $this->surface = $obj->surface; - $this->surface_units = $obj->surface_units; - $this->volume = $obj->volume; - $this->volume_units = $obj->volume_units; - $this->barcode = $obj->barcode; - $this->barcode_type = $obj->fk_barcode_type; + $this->surface = $obj->surface; + $this->surface_units = $obj->surface_units; + $this->volume = $obj->volume; + $this->volume_units = $obj->volume_units; + $this->barcode = $obj->barcode; + $this->barcode_type = $obj->fk_barcode_type; $this->accountancy_code_buy = $obj->accountancy_code_buy; $this->accountancy_code_buy_intra = $obj->accountancy_code_buy_intra; @@ -2717,17 +2742,18 @@ public function fetch($id = 0, $ref = '', $ref_ext = '', $barcode = '', $ignore_ $this->accountancy_code_sell_intra = $obj->accountancy_code_sell_intra; $this->accountancy_code_sell_export = $obj->accountancy_code_sell_export; - $this->fk_default_warehouse = $obj->fk_default_warehouse; - $this->fk_default_workstation = $obj->fk_default_workstation; - $this->seuil_stock_alerte = $obj->seuil_stock_alerte; - $this->desiredstock = $obj->desiredstock; - $this->stock_reel = $obj->stock; - $this->pmp = $obj->pmp; - - $this->date_creation = $obj->datec; - $this->date_modification = $obj->tms; - $this->import_key = $obj->import_key; - $this->entity = $obj->entity; + $this->fk_default_warehouse = $obj->fk_default_warehouse; + $this->fk_default_workstation = $obj->fk_default_workstation; + $this->seuil_stock_alerte = $obj->seuil_stock_alerte; + $this->desiredstock = $obj->desiredstock; + $this->stock_reel = $obj->stock; + $this->stockable_product = $obj->stockable_product; + $this->pmp = $obj->pmp; + + $this->date_creation = $obj->datec; + $this->date_modification = $obj->tms; + $this->import_key = $obj->import_key; + $this->entity = $obj->entity; $this->ref_ext = $obj->ref_ext; $this->fk_price_expression = $obj->fk_price_expression; diff --git a/htdocs/product/list.php b/htdocs/product/list.php index cc2d1a26ed408..f52b74b78962d 100644 --- a/htdocs/product/list.php +++ b/htdocs/product/list.php @@ -96,11 +96,13 @@ if (!empty($catid) && empty($searchCategoryProductList)) { $searchCategoryProductList = array($catid); } + $search_tosell = GETPOST("search_tosell"); $search_tobuy = GETPOST("search_tobuy"); $search_country = GETPOST("search_country", 'aZ09'); $search_state = GETPOST("state_id", 'intcomma'); $search_tobatch = GETPOST("search_tobatch"); +$search_stockable_product = GETPOST('search_stockable_product', 'int'); $search_accountancy_code_sell = GETPOST("search_accountancy_code_sell", 'alpha'); $search_accountancy_code_sell_intra = GETPOST("search_accountancy_code_sell_intra", 'alpha'); $search_accountancy_code_sell_export = GETPOST("search_accountancy_code_sell_export", 'alpha'); @@ -277,6 +279,18 @@ 'p.tobuy' => array('label' => $langs->transnoentitiesnoconv("Status").' ('.$langs->transnoentitiesnoconv("Buy").')', 'checked' => 1, 'position' => 1000), 'p.import_key' => array('type' => 'varchar(14)', 'label' => 'ImportId', 'enabled' => 1, 'visible' => -2, 'notnull' => -1, 'index' => 0, 'checked' => -1, 'position' => 1100), ); + +if (! empty($conf->stock->enabled)) { + // service + if ($type == 1) { + if (! empty($conf->global->STOCK_SUPPORTS_SERVICES)) { + $arrayfields['p.stockable_product'] = array('label' => $langs->trans('StockableProduct'), 'checked' => 0, 'position' => 1001); + } + } else { + //product + $arrayfields['p.stockable_product'] = array('label' => $langs->trans('StockableProduct'), 'checked' => 0, 'position' => 1001); + } +} /*foreach ($object->fields as $key => $val) { // If $val['visible']==0, then we never show the field if (!empty($val['visible'])) { @@ -370,6 +384,7 @@ //$search_type=''; // There is 2 types of list: a list of product and a list of services. No list with both. So when we clear search criteria, we must keep the filter on type. $show_childproducts = ''; + $search_stockable_product = ''; $search_import_key = ''; $search_accountancy_code_sell = ''; $search_accountancy_code_sell_intra = ''; @@ -456,7 +471,7 @@ } $sql .= ' p.datec as date_creation, p.tms as date_modification, p.pmp, p.stock, p.cost_price,'; $sql .= ' p.weight, p.weight_units, p.length, p.length_units, p.width, p.width_units, p.height, p.height_units, p.surface, p.surface_units, p.volume, p.volume_units,'; -$sql .= ' p.fk_country, p.fk_state,'; +$sql .= ' p.fk_country, p.fk_state, p.stockable_product,'; $sql .= ' p.import_key,'; if (getDolGlobalString('PRODUCT_USE_UNITS')) { $sql .= ' p.fk_unit, cu.label as cu_label,'; @@ -563,6 +578,9 @@ if (isset($search_tobuy) && dol_strlen($search_tobuy) > 0 && $search_tobuy != -1) { $sql .= " AND p.tobuy = ".((int) $search_tobuy); } +if (isset($search_stockable_product) && dol_strlen($search_stockable_product) > 0 && $search_stockable_product != -1) { + $sql .= " AND p.stockable_product = '". ((int) $search_stockable_product) . "'"; +} if (isset($search_tobatch) && dol_strlen($search_tobatch) > 0 && $search_tobatch != -1) { $sql .= " AND p.tobatch = ".((int) $search_tobatch); } @@ -648,7 +666,7 @@ $sql .= " ppe.accountancy_code_sell, ppe.accountancy_code_sell_intra, ppe.accountancy_code_sell_export, ppe.accountancy_code_buy, ppe.accountancy_code_buy_intra, ppe.accountancy_code_buy_export,"; } $sql .= ' p.weight, p.weight_units, p.length, p.length_units, p.width, p.width_units, p.height, p.height_units, p.surface, p.surface_units, p.volume, p.volume_units,'; -$sql .= ' p.fk_country, p.fk_state,'; +$sql .= ' p.fk_country, p.fk_state, p.stockable_product,'; $sql .= ' p.import_key'; if (getDolGlobalString('PRODUCT_USE_UNITS')) { $sql .= ', p.fk_unit, cu.label'; @@ -836,6 +854,9 @@ if ($search_finished) { $param .= "&search_finished=".urlencode($search_finished); } +if ($search_stockable_product != '') { + $param .= "&search_stockable_product=".urlencode($search_stockable_product); +} // Add $param from extra fields include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_list_search_param.tpl.php'; @@ -1175,6 +1196,11 @@ print ' '; print ''; } +// Managed_in_stock +$array = array('-1'=>' ', '0'=>$langs->trans('No'), '1'=>$langs->trans('Yes')); +if (!empty($arrayfields['p.stockable_product']['checked'])) { + print ''.Form::selectarray('search_stockable_product', $array, $search_stockable_product).''; +} // Desired stock if (!empty($arrayfields['p.desiredstock']['checked'])) { print ''; @@ -1423,6 +1449,9 @@ print_liste_field_titre($arrayfields['p.seuil_stock_alerte']['label'], $_SERVER["PHP_SELF"], "p.seuil_stock_alerte", "", $param, '', $sortfield, $sortorder, 'right '); $totalarray['nbfield']++; } +if (!empty($arrayfields['p.stockable_product']['checked'])) { + print_liste_field_titre($arrayfields['p.stockable_product']['label'], $_SERVER['PHP_SELF'], 'p.stockable_product', '', $param, '', $sortfield, $sortorder, 'center '); +} if (!empty($arrayfields['p.desiredstock']['checked'])) { print_liste_field_titre($arrayfields['p.desiredstock']['label'], $_SERVER["PHP_SELF"], "p.desiredstock", "", $param, '', $sortfield, $sortorder, 'right '); $totalarray['nbfield']++; @@ -1572,6 +1601,7 @@ $product_static->volume_units = $obj->volume_units; $product_static->surface = $obj->surface; $product_static->surface_units = $obj->surface_units; + $product_static->stockable_product = $obj->stockable_product; if (getDolGlobalString('PRODUCT_USE_UNITS')) { $product_static->fk_unit = $obj->fk_unit; } @@ -2079,6 +2109,14 @@ $totalarray['nbfield']++; } } + + // not managed in stock + if (! empty($arrayfields['p.stockable_product']['checked'])) { + print ''; + print ($product_static->stockable_product == '1') ? $langs->trans('Yes') : $langs->trans('No'); + print ''; + } + // Desired stock if (!empty($arrayfields['p.desiredstock']['checked'])) { print ''; diff --git a/htdocs/product/stock/class/mouvementstock.class.php b/htdocs/product/stock/class/mouvementstock.class.php index 1c74f32d5531e..5ab474c885535 100644 --- a/htdocs/product/stock/class/mouvementstock.class.php +++ b/htdocs/product/stock/class/mouvementstock.class.php @@ -446,7 +446,7 @@ public function _create($user, $fk_product, $entrepot_id, $qty, $type, $price = return -8; } } else { - if (isset($product->stock_warehouse[$entrepot_id]) && (empty($product->stock_warehouse[$entrepot_id]->real) || $product->stock_warehouse[$entrepot_id]->real < abs($qty))) { + if (isset($product->stock_warehouse[$entrepot_id]) && (empty($product->stock_warehouse[$entrepot_id]->real) || $product->stock_warehouse[$entrepot_id]->real < abs($qty)) && $product->stockable_product == Product::ENABLED_STOCK) { $langs->load("stocks"); $this->error = $langs->trans('qtyToTranferIsNotEnough').' : '.$product->ref; $this->errors[] = $langs->trans('qtyToTranferIsNotEnough').' : '.$product->ref; @@ -456,7 +456,7 @@ public function _create($user, $fk_product, $entrepot_id, $qty, $type, $price = } } - if ($movestock) { // Change stock for current product, change for subproduct is done after + if ($movestock && $product->stockable_product == Product::ENABLED_STOCK) { // Change stock for current product, change for subproduct is done after // Set $origin_type, origin_id and fk_project $fk_project = $this->fk_project; if (!empty($this->origin_type)) { // This is set by caller for tracking reason @@ -632,9 +632,11 @@ public function _create($user, $fk_product, $entrepot_id, $qty, $type, $price = if ($movestock && !$error) { // Call trigger - $result = $this->call_trigger('STOCK_MOVEMENT', $user); - if ($result < 0) { - $error++; + if ($product->stockable_product != Product::NOT_MANAGED_IN_STOCK ) { + $result = $this->call_trigger('STOCK_MOVEMENT', $user); + if ($result < 0) { + $error++; + } } // End call triggers // Check unicity for serial numbered equipment once all movement were done.