diff --git a/changelog/77_UNRELEASED_xxxx-xx-xx.md b/changelog/77_UNRELEASED_xxxx-xx-xx.md index 342f1400..dee98563 100644 --- a/changelog/77_UNRELEASED_xxxx-xx-xx.md +++ b/changelog/77_UNRELEASED_xxxx-xx-xx.md @@ -34,6 +34,9 @@ - Optimized that when adding missing recipe ingredients with the option "Only check if any amount is in stock" enabled to the shopping list and when no corresponding unit conversion exists, the amount/unit is now taken "as is" (as defined in the recipe ingredient) into the created shopping list item - Added a trendline to the price history chart (product card) +- 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 - Fixed that calories/costs of recipe ingredients were wrong when the ingredient option "Only check if any amount is in stock" was set and the on the ingredient used quantity unit was different from the product's QU stock - Fixed that multi-nested recipes (at least 3 levels of "included recipes") resulted in wrong amounts/costs/calories calculated for the ingredients orginating in those nested recipes (also affected the meal plan) diff --git a/config-dist.php b/config-dist.php index 2485e24a..65710e50 100644 --- a/config-dist.php +++ b/config-dist.php @@ -192,6 +192,7 @@ DefaultUserSetting('stock_default_consume_amount_use_quick_consume_amount', fals DefaultUserSetting('scan_mode_consume_enabled', false); // If scan mode on the consume page is enabled DefaultUserSetting('scan_mode_purchase_enabled', false); // If scan mode on the purchase page is enabled DefaultUserSetting('show_icon_on_stock_overview_page_when_product_is_on_shopping_list', true); // When enabled, an icon is shown on the stock overview page (next to the product name) when the prodcut is currently on a shopping list +DefaultUserSetting('stock_overview_show_all_out_of_stock_products', false); // By default the stock overview page lists all products which are currently in-stock or below their min. stock amount - when this is enabled, all (active) products are always shown DefaultUserSetting('show_purchased_date_on_purchase', false); // Whether the purchased date should be editable on purchase (defaults to today otherwise) DefaultUserSetting('show_warning_on_purchase_when_due_date_is_earlier_than_next', true); // Show a warning on purchase when the due date of the purchased product is earlier than the next due date in stock diff --git a/controllers/StockController.php b/controllers/StockController.php index d06c4c4c..68a87e70 100644 --- a/controllers/StockController.php +++ b/controllers/StockController.php @@ -139,8 +139,14 @@ class StockController extends BaseController $usersService = $this->getUsersService(); $nextXDays = $usersService->GetUserSettings(GROCY_USER_ID)['stock_due_soon_days']; + $where = 'is_in_stock_or_below_min_stock = 1'; + if (boolval($usersService->GetUserSettings(GROCY_USER_ID)['stock_overview_show_all_out_of_stock_products'])) + { + $where = '1=1'; + } + return $this->renderPage($response, 'stockoverview', [ - 'currentStock' => $this->getStockService()->GetCurrentStockOverview(), + 'currentStock' => $this->getDatabase()->uihelper_stock_current_overview()->where($where), 'locations' => $this->getDatabase()->locations()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'), 'currentStockLocations' => $this->getStockService()->GetCurrentStockLocations(), 'nextXDays' => $nextXDays, diff --git a/localization/strings.pot b/localization/strings.pot index 652b42a0..697da863 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -2446,3 +2446,9 @@ msgstr "" msgid "Time of tracking" msgstr "" + +msgid "Show all out of stock products" +msgstr "" + +msgid "By default the stock overview page lists all products which are currently in-stock or below their min. stock amount - when this is enabled, all (active) products are always shown" +msgstr "" diff --git a/migrations/0245.sql b/migrations/0245.sql new file mode 100644 index 00000000..1f892552 --- /dev/null +++ b/migrations/0245.sql @@ -0,0 +1,88 @@ +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, + CASE WHEN sc.is_in_stock_or_below_min_stock = 1 THEN sc.best_before_date ELSE '2888-12-31' END 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 +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/js/grocy.js b/public/js/grocy.js index 452a99b5..74e6adf3 100644 --- a/public/js/grocy.js +++ b/public/js/grocy.js @@ -318,6 +318,7 @@ RefreshContextualTimeago = function(rootSelector = "#page-content") } var isNever = timestamp && timestamp.substring(0, 10) == "2999-12-31"; + var isUnknown = timestamp && timestamp.substring(0, 10) == "2888-12-31"; var isToday = timestamp && timestamp.substring(0, 10) == moment().format("YYYY-MM-DD"); var isDateWithoutTime = element.hasClass("timeago-date-only"); @@ -326,6 +327,11 @@ RefreshContextualTimeago = function(rootSelector = "#page-content") element.prev().text(__t("Never")); element.text(""); } + else if (isUnknown) + { + element.prev().text(__t("Unknown")); + element.text(""); + } else if (isToday) { element.text(__t("Today")); diff --git a/public/viewjs/stockoverview.js b/public/viewjs/stockoverview.js index 2cf64d83..e288a92c 100755 --- a/public/viewjs/stockoverview.js +++ b/public/viewjs/stockoverview.js @@ -289,6 +289,11 @@ function RefreshProductRow(productId) RefreshProductRow(result.product.parent_product_id); } + if (!result.next_due_date) + { + result.next_due_date = "2888-12-31"; // Unknown + } + var productRow = $('#product-' + productId + '-row'); var dueSoonThreshold = moment().add($("#info-duesoon-products").data("next-x-days"), "days"); var now = moment(); @@ -320,7 +325,7 @@ function RefreshProductRow(productId) productRow.addClass("table-info"); } - if (result.stock_amount == 0 && result.stock_amount_aggregated == 0 && result.product.min_stock_amount == 0) + if (!BoolVal(Grocy.UserSettings.stock_overview_show_all_out_of_stock_products) && result.stock_amount == 0 && result.stock_amount_aggregated == 0 && result.product.min_stock_amount == 0) { animateCSS("#product-" + productId + "-row", "fadeOut", function() { @@ -348,6 +353,12 @@ function RefreshProductRow(productId) { $('#product-' + productId + '-opened-amount').text(""); } + + if (result.stock_amount_aggregated == 0) + { + $(".product-consume-button[data-product-id='" + productId + "']").addClass("disabled"); + $(".product-open-button[data-product-id='" + productId + "']").addClass("disabled"); + } } $('#product-' + productId + '-next-due-date').text(result.next_due_date); diff --git a/public/viewjs/stocksettings.js b/public/viewjs/stocksettings.js index 9d241950..ee268a62 100644 --- a/public/viewjs/stocksettings.js +++ b/public/viewjs/stocksettings.js @@ -2,14 +2,17 @@ $("#product_presets_product_group_id").val(Grocy.UserSettings.product_presets_product_group_id); $("#product_presets_qu_id").val(Grocy.UserSettings.product_presets_qu_id); $("#product_presets_default_due_days").val(Grocy.UserSettings.product_presets_default_due_days); + if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING && BoolVal(Grocy.UserSettings.product_presets_treat_opened_as_out_of_stock)) { $("#product_presets_treat_opened_as_out_of_stock").prop("checked", true); } + if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABEL_PRINTER) { $("#product_presets_default_stock_label_type").val(Grocy.UserSettings.product_presets_default_stock_label_type); } + $("#stock_due_soon_days").val(Grocy.UserSettings.stock_due_soon_days); $("#stock_default_purchase_amount").val(Grocy.UserSettings.stock_default_purchase_amount); $("#stock_default_consume_amount").val(Grocy.UserSettings.stock_default_consume_amount); @@ -21,6 +24,12 @@ if (BoolVal(Grocy.UserSettings.show_icon_on_stock_overview_page_when_product_is_ { $("#show_icon_on_stock_overview_page_when_product_is_on_shopping_list").prop("checked", true); } + +if (BoolVal(Grocy.UserSettings.stock_overview_show_all_out_of_stock_products)) +{ + $("#stock_overview_show_all_out_of_stock_products").prop("checked", true); +} + if (BoolVal(Grocy.UserSettings.show_purchased_date_on_purchase)) { $("#show_purchased_date_on_purchase").prop("checked", true); diff --git a/services/StockService.php b/services/StockService.php index 1ffd52d6..3c60714b 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -692,11 +692,6 @@ class StockService extends BaseService return $this->getDatabaseService()->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ); } - public function GetCurrentStockOverview() - { - return $this->getDatabase()->uihelper_stock_current_overview(); - } - public function GetDueProducts(int $days = 5, bool $excludeOverdue = false) { if ($excludeOverdue) diff --git a/views/stocksettings.blade.php b/views/stocksettings.blade.php index b27bffa2..b4ff05d4 100644 --- a/views/stocksettings.blade.php +++ b/views/stocksettings.blade.php @@ -113,6 +113,23 @@ +
+
+ + +
+
+

{{ $__t('Purchase') }}

@include('components.numberpicker', array( 'id' => 'stock_default_purchase_amount',