From bc78359dba81186a78c40804c517ba6c1c8b964d Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 19 Jan 2025 14:57:19 +0100 Subject: [PATCH] Make it possible to disable open per product (closes #1911) --- changelog/77_UNRELEASED_xxxx-xx-xx.md | 3 + localization/strings.pot | 3 + migrations/0248.sql | 92 +++++++++++++++++++++++++++ public/viewjs/consume.js | 2 +- public/viewjs/stockentries.js | 5 ++ public/viewjs/stockoverview.js | 5 ++ services/StockService.php | 8 ++- views/productform.blade.php | 28 ++++++-- views/stockentries.blade.php | 2 +- views/stockoverview.blade.php | 2 +- 10 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 migrations/0248.sql diff --git a/changelog/77_UNRELEASED_xxxx-xx-xx.md b/changelog/77_UNRELEASED_xxxx-xx-xx.md index 26672583..ec1e2de6 100644 --- a/changelog/77_UNRELEASED_xxxx-xx-xx.md +++ b/changelog/77_UNRELEASED_xxxx-xx-xx.md @@ -20,6 +20,9 @@ - Added a new stock setting (top right corner settings menu) "Show all out of stock products" to optionally also show all out of stock products on the stock overview page (defaults to disabled, so no changed behavior when not configured) - By default the stock overview page lists all products which are currently in stock or below their min. stock amount - When this new setting is enabled, all (active) products are always shown +- Added a new product option "Can't be opened" + - When enabled the product open functionality for that product is disabled + - Defaults to disabled, so no changed behavior when not configured - Product barcode matching is now case-insensitive - Added a new column "Product picture" on the products list (master data) page (hidden by default) - Optimized that when navigation between the different "Group by"-variants on the stock report "Spendings", the selected date range now remains persistent diff --git a/localization/strings.pot b/localization/strings.pot index 0c4ebc20..3daab67d 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -2458,3 +2458,6 @@ msgstr "" msgid "For ingredients that are only partially in stock, the in stock amount will be consumed." msgstr "" + +msgid "Can't be opened" +msgstr "" diff --git a/migrations/0248.sql b/migrations/0248.sql new file mode 100644 index 00000000..c3b07331 --- /dev/null +++ b/migrations/0248.sql @@ -0,0 +1,92 @@ +ALTER TABLE products +ADD disable_open TINYINT NOT NULL DEFAULT 0 CHECK(disable_open IN (0, 1)); + +DROP VIEW uihelper_stock_current_overview; +CREATE VIEW uihelper_stock_current_overview +AS +SELECT + p.id, + sc.amount_opened AS amount_opened, + p.tare_weight AS tare_weight, + p.enable_tare_weight_handling AS enable_tare_weight_handling, + sc.amount AS amount, + sc.value as value, + sc.product_id AS product_id, + IFNULL(sc.best_before_date, '2888-12-31') AS best_before_date, + EXISTS(SELECT id FROM stock_missing_products WHERE id = sc.product_id) AS product_missing, + p.name AS product_name, + pg.name AS product_group_name, + EXISTS(SELECT * FROM shopping_list WHERE shopping_list.product_id = sc.product_id) AS on_shopping_list, + qu_stock.name AS qu_stock_name, + qu_stock.name_plural AS qu_stock_name_plural, + qu_purchase.name AS qu_purchase_name, + qu_purchase.name_plural AS qu_purchase_name_plural, + qu_consume.name AS qu_consume_name, + qu_consume.name_plural AS qu_consume_name_plural, + qu_price.name AS qu_price_name, + qu_price.name_plural AS qu_price_name_plural, + sc.is_aggregated_amount, + sc.amount_opened_aggregated, + sc.amount_aggregated, + p.calories AS product_calories, + sc.amount * p.calories AS calories, + sc.amount_aggregated * p.calories AS calories_aggregated, + p.quick_consume_amount, + p.quick_consume_amount / p.qu_factor_consume_to_stock AS quick_consume_amount_qu_consume, + p.quick_open_amount, + p.quick_open_amount / p.qu_factor_consume_to_stock AS quick_open_amount_qu_consume, + p.due_type, + plp.purchased_date AS last_purchased, + plp.price AS last_price, + pap.price as average_price, + p.min_stock_amount, + pbcs.barcodes AS product_barcodes, + p.description AS product_description, + l.name AS product_default_location_name, + p_parent.id AS parent_product_id, + p_parent.name AS parent_product_name, + p.picture_file_name AS product_picture_file_name, + p.no_own_stock AS product_no_own_stock, + p.qu_factor_purchase_to_stock AS product_qu_factor_purchase_to_stock, + p.qu_factor_price_to_stock AS product_qu_factor_price_to_stock, + sc.is_in_stock_or_below_min_stock, + p.disable_open +FROM ( + SELECT *, 1 AS is_in_stock_or_below_min_stock + FROM stock_current + WHERE best_before_date IS NOT NULL + UNION + SELECT m.id, 0, 0, 0, null, 0, 0, 0, p.due_type, 1 AS is_in_stock_or_below_min_stock + FROM stock_missing_products m + JOIN products p + ON m.id = p.id + WHERE m.id NOT IN (SELECT product_id FROM stock_current) + UNION + SELECT p2.id, 0, 0, 0, null, 0, 0, 0, p2.due_type, 0 AS is_in_stock_or_below_min_stock + FROM products p2 + WHERE active = 1 + AND p2.id NOT IN (SELECT product_id FROM stock_current UNION SELECT id FROM stock_missing_products) + ) sc +JOIN products_view p + ON sc.product_id = p.id +JOIN locations l + ON p.location_id = l.id +JOIN quantity_units qu_stock + ON p.qu_id_stock = qu_stock.id +JOIN quantity_units qu_purchase + ON p.qu_id_purchase = qu_purchase.id +JOIN quantity_units qu_consume + ON p.qu_id_consume = qu_consume.id +JOIN quantity_units qu_price + ON p.qu_id_price = qu_price.id +LEFT JOIN product_groups pg + ON p.product_group_id = pg.id +LEFT JOIN cache__products_last_purchased plp + ON sc.product_id = plp.product_id +LEFT JOIN cache__products_average_price pap + ON sc.product_id = pap.product_id +LEFT JOIN product_barcodes_comma_separated pbcs + ON sc.product_id = pbcs.product_id +LEFT JOIN products p_parent + ON p.parent_product_id = p_parent.id +WHERE p.hide_on_stock_overview = 0; diff --git a/public/viewjs/consume.js b/public/viewjs/consume.js index 5ed74756..410d5afb 100644 --- a/public/viewjs/consume.js +++ b/public/viewjs/consume.js @@ -494,7 +494,7 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e) $('#display_amount').focus(); }, 500); - if (productDetails.stock_amount == productDetails.stock_amount_opened || productDetails.product.enable_tare_weight_handling == 1) + if (productDetails.stock_amount == productDetails.stock_amount_opened || productDetails.product.enable_tare_weight_handling == 1 || productDetails.product.disable_open == 1) { $("#save-mark-as-open-button").addClass("disabled"); } diff --git a/public/viewjs/stockentries.js b/public/viewjs/stockentries.js index 97f1bf95..9d8a781f 100644 --- a/public/viewjs/stockentries.js +++ b/public/viewjs/stockentries.js @@ -248,6 +248,11 @@ function RefreshStockEntryRow(stockRowId) $('#stock-' + stockRowId + '-price').text(__t("%1$s per %2$s", (result.price * productDetails.qu_conversion_factor_purchase_to_stock).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display }), productDetails.default_quantity_unit_purchase.name)); $('#stock-' + stockRowId + '-price').attr("data-original-title", __t("%1$s per %2$s", result.price.toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display }), productDetails.quantity_unit_stock.name)); + + if (productDetails.product.disable_open == 1) + { + $(".product-open-button[data-stockrow-id='" + stockRowId + "']").addClass("disabled"); + } }, function(xhr) { diff --git a/public/viewjs/stockoverview.js b/public/viewjs/stockoverview.js index a3bc336c..e03c2736 100755 --- a/public/viewjs/stockoverview.js +++ b/public/viewjs/stockoverview.js @@ -359,6 +359,11 @@ function RefreshProductRow(productId) $(".product-consume-button[data-product-id='" + productId + "']").removeClass("disabled"); $(".product-open-button[data-product-id='" + productId + "']").removeClass("disabled"); } + + if (result.product.disable_open == 1) + { + $(".product-open-button[data-product-id='" + productId + "']").addClass("disabled"); + } } $('#product-' + productId + '-next-due-date').text(result.next_due_date); diff --git a/services/StockService.php b/services/StockService.php index 3c60714b..415c6172 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -953,10 +953,16 @@ class StockService extends BaseService throw new \Exception('Product does not exist or is inactive'); } + $product = $this->getDatabase()->products($productId); + + if ($product->disable_open == 1) + { + throw new \Exception('Product can\'t be opened'); + } + $productDetails = (object)$this->GetProductDetails($productId); $productStockAmountUnopened = $productDetails->stock_amount_aggregated - $productDetails->stock_amount_opened_aggregated; $potentialStockEntries = $this->GetProductStockEntries($productId, true, $allowSubproductSubstitution); - $product = $this->getDatabase()->products($productId); if ($product->enable_tare_weight_handling == 1) { diff --git a/views/productform.blade.php b/views/productform.blade.php index 10492a23..1429436d 100644 --- a/views/productform.blade.php +++ b/views/productform.blade.php @@ -570,10 +570,18 @@ @endif - @include('components.userfieldsform', array( - 'userfields' => $userfields, - 'entity' => 'products' - )) + @if(GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING) +
+
+ disable_open == 1) checked @endif class="form-check-input custom-control-input" type="checkbox" id="disable_open" name="disable_open" value="1"> + +
+
+ @endif
@@ -589,7 +597,7 @@
-
+
+ @include('components.userfieldsform', array( + 'userfields' => $userfields, + 'entity' => 'products' + )) + +
diff --git a/views/stockentries.blade.php b/views/stockentries.blade.php index b92d59d0..8b31c971 100644 --- a/views/stockentries.blade.php +++ b/views/stockentries.blade.php @@ -121,7 +121,7 @@ @if(GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING) - {{ $__t('All') }} @if(GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING) -