diff --git a/changelog/53_UNRELEASED_2019-xx-xx.md b/changelog/53_UNRELEASED_2019-xx-xx.md index fc4244eb..989ccdb1 100644 --- a/changelog/53_UNRELEASED_2019-xx-xx.md +++ b/changelog/53_UNRELEASED_2019-xx-xx.md @@ -1,12 +1,26 @@ -- Fixed that barcode lookups now compare the whole barcode, not parts of it (e. g. when you have two products with the barcodes "$1" and "$10" and scan "$1" maybe the product of "$10" was found till now) +### Stock improvements/fixes +- Fixed that barcode lookups now compare the whole barcode, not parts of it (e. g. when you have two products with the barcodes `$1` and `$10` and scan `$1` maybe the product of `$10` was found till now) +- Fixed that the "X products are already expired" count on the stock overview page was wrong +- It's now possible to accumulate min. stock amounts on parent product level (new option per product, means the sub product will never be "missing" then, only the parent product) +- When adding a product to the shopping list from the new context/more menu from the stock overview page and if the product is already on the shopping list, the amount of that entry will be updated acccordingly instead of adding a new (double) shopping list item + +### Recipe improvements/fixes +- Fixed a problem regarding quantity unit conversion handling for recipe ingredients of products with no unit relations, but only a different purchase/stock quantity unit - It's now possible to display a recipe directly from the meal plan (new "eye button") (thanks @kriddles) - Improved the responsiveness of the meal plan and calendar page by automatically switching to a day calendar view on smaller screens (thanks for the idea @kriddles) + +### Chores improvements +- There is now a new sub feature flag `FEATURE_FLAG_CHORES_ASSIGNMENTS` to disable chore assignments if you don't need them (defaults to `true`, so no changed behavior when not configured) + +### Calendar improvements - The calendar now also contains all planned recipes from the meal plan on the corresponding day -- When adding a product to the shopping list from the new context/more menu from the stock overview page and if the product is already on the shopping list, the amount of that entry will be updated acccordingly instead of adding a new (double) shopping list item +- Improved that dates in the iCal calendar export now include the server timezone + +### General & other improvements/fixes - Fixed that the browser barcode scanner button was not clickable on iOS Safari (thanks @DeeeeLAN) -- Fixed a problem regarding quantity unit conversion handling for recipe ingredients of products with no unit relations, but only a different purchase/stock quantity unit -- Improved that dates in the iCal calendar export now includes the server timezone - It's now also possible to set the meal plan page as the default/entry page (`config.php` setting `ENTRY_PAGE`) (thanks @lwis) +- Some UI detail-refinements + +### API improvements/fixes - The API Endpoint `GET /files/{group}/{fileName}` now also returns a `Cache-Control` header (defaults fixed to 30 days) to further increase page load times - Fixed that the API endpoint `/stock/shoppinglist/add-product` failed when a product should be added which was not already on the shopping list (thanks @Forceu) -- Some style/CSS detail-refinements diff --git a/config-dist.php b/config-dist.php index 53817a24..c83286bb 100644 --- a/config-dist.php +++ b/config-dist.php @@ -124,6 +124,7 @@ Setting('FEATURE_FLAG_STOCK_PRICE_TRACKING', true); Setting('FEATURE_FLAG_STOCK_LOCATION_TRACKING', true); Setting('FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING', true); Setting('FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING', true); +Setting('FEATURE_FLAG_CHORES_ASSIGNMENTS', true); # Feature settings diff --git a/controllers/StockController.php b/controllers/StockController.php index 64dd3fc6..ec946ae0 100644 --- a/controllers/StockController.php +++ b/controllers/StockController.php @@ -28,7 +28,7 @@ class StockController extends BaseController 'products' => $this->Database->products()->orderBy('name'), 'quantityunits' => $this->Database->quantity_units()->orderBy('name'), 'locations' => $this->Database->locations()->orderBy('name'), - 'currentStock' => $this->StockService->GetCurrentStock(), + 'currentStock' => $this->StockService->GetCurrentStock(true), 'currentStockLocations' => $this->StockService->GetCurrentStockLocations(), 'missingProducts' => $this->StockService->GetMissingProducts(), 'nextXDays' => $nextXDays, diff --git a/localization/strings.pot b/localization/strings.pot index 136583c5..89cc8cfe 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1519,3 +1519,12 @@ msgstr "" msgid "Display recipe" msgstr "" + +msgid "Accumulate sub products min. stock amount" +msgstr "" + +msgid "If enabled, the min. stock amount of sub products will be accumulated into this product, means the sub product will never be \"missing\", only this product" +msgstr "" + +msgid "Are you sure to remove this conversion?" +msgstr "" diff --git a/migrations/0092.sql b/migrations/0092.sql new file mode 100644 index 00000000..84c39cab --- /dev/null +++ b/migrations/0092.sql @@ -0,0 +1,129 @@ +ALTER TABLE products +ADD cumulate_min_stock_amount_of_sub_products TINYINT DEFAULT 0; + +CREATE VIEW products_view +AS +SELECT *, CASE WHEN (SELECT 1 FROM products WHERE parent_product_id = p.id) NOTNULL THEN 1 ELSE 0 END AS has_sub_products +FROM products p; + +DROP VIEW stock_missing_products; +CREATE VIEW stock_missing_products +AS + +-- Products WITHOUT sub products where the amount of the sub products SHOULD NOT be cumulated +SELECT + p.id, + MAX(p.name) AS name, + p.min_stock_amount - IFNULL(SUM(s.amount), 0) AS amount_missing, + CASE WHEN IFNULL(SUM(s.amount), 0) > 0 THEN 1 ELSE 0 END AS is_partly_in_stock +FROM products_view p +LEFT JOIN stock_current s + ON p.id = s.product_id +WHERE p.min_stock_amount != 0 + AND p.cumulate_min_stock_amount_of_sub_products = 0 + AND p.has_sub_products = 0 + AND p.parent_product_id IS NULL +GROUP BY p.id +HAVING IFNULL(SUM(s.amount), 0) < p.min_stock_amount + +UNION + +-- Parent products WITH sub products where the amount of the sub products SHOULD be cumulated +SELECT + p.id, + MAX(p.name) AS name, + SUM(sub_p.min_stock_amount) - IFNULL(SUM(s.amount_aggregated), 0) AS amount_missing, + CASE WHEN IFNULL(SUM(s.amount), 0) > 0 THEN 1 ELSE 0 END AS is_partly_in_stock +FROM products_view p +JOIN products_resolved pr + ON p.id = pr.parent_product_id +JOIN products sub_p + ON pr.sub_product_id = sub_p.id +LEFT JOIN stock_current s + ON pr.sub_product_id = s.product_id +WHERE sub_p.min_stock_amount != 0 + AND p.cumulate_min_stock_amount_of_sub_products = 1 +GROUP BY p.id +HAVING IFNULL(SUM(s.amount_aggregated), 0) < SUM(sub_p.min_stock_amount) + +UNION + +-- Sub products where the amount SHOULD NOT be cumulated into the parent product +SELECT + sub_p.id, + MAX(sub_p.name) AS name, + SUM(sub_p.min_stock_amount) - IFNULL(SUM(s.amount), 0) AS amount_missing, + CASE WHEN IFNULL(SUM(s.amount), 0) > 0 THEN 1 ELSE 0 END AS is_partly_in_stock +FROM products p +JOIN products_resolved pr + ON p.id = pr.parent_product_id +JOIN products sub_p + ON pr.sub_product_id = sub_p.id +LEFT JOIN stock_current s + ON pr.sub_product_id = s.product_id +WHERE sub_p.min_stock_amount != 0 + AND p.cumulate_min_stock_amount_of_sub_products = 0 +GROUP BY sub_p.id +HAVING IFNULL(SUM(s.amount), 0) < sub_p.min_stock_amount; + +DROP VIEW stock_missing_products_including_opened; +CREATE VIEW stock_missing_products_including_opened +AS + +/* This is basically the same view as stock_missing_products, but the column "amount_missing" includes opened amounts */ + +-- Products WITHOUT sub products where the amount of the sub products SHOULD NOT be cumulated +SELECT + p.id, + MAX(p.name) AS name, + p.min_stock_amount - (IFNULL(SUM(s.amount), 0) - IFNULL(SUM(s.amount_opened), 0)) AS amount_missing, + CASE WHEN IFNULL(SUM(s.amount), 0) > 0 THEN 1 ELSE 0 END AS is_partly_in_stock +FROM products_view p +LEFT JOIN stock_current s + ON p.id = s.product_id +WHERE p.min_stock_amount != 0 + AND p.cumulate_min_stock_amount_of_sub_products = 0 + AND p.has_sub_products = 0 + AND p.parent_product_id IS NULL +GROUP BY p.id +HAVING IFNULL(SUM(s.amount), 0) < p.min_stock_amount + +UNION + +-- Parent products WITH sub products where the amount of the sub products SHOULD be cumulated +SELECT + p.id, + MAX(p.name) AS name, + SUM(sub_p.min_stock_amount) - (IFNULL(SUM(s.amount_aggregated), 0) - IFNULL(SUM(s.amount_opened), 0)) AS amount_missing, + CASE WHEN IFNULL(SUM(s.amount), 0) > 0 THEN 1 ELSE 0 END AS is_partly_in_stock +FROM products_view p +JOIN products_resolved pr + ON p.id = pr.parent_product_id +JOIN products sub_p + ON pr.sub_product_id = sub_p.id +LEFT JOIN stock_current s + ON pr.sub_product_id = s.product_id +WHERE sub_p.min_stock_amount != 0 + AND p.cumulate_min_stock_amount_of_sub_products = 1 +GROUP BY p.id +HAVING IFNULL(SUM(s.amount_aggregated), 0) < SUM(sub_p.min_stock_amount) + +UNION + +-- Sub products where the amount SHOULD NOT be cumulated into the parent product +SELECT + sub_p.id, + MAX(sub_p.name) AS name, + SUM(sub_p.min_stock_amount) - (IFNULL(SUM(s.amount), 0) - IFNULL(SUM(s.amount_opened), 0)) AS amount_missing, + CASE WHEN IFNULL(SUM(s.amount), 0) > 0 THEN 1 ELSE 0 END AS is_partly_in_stock +FROM products p +JOIN products_resolved pr + ON p.id = pr.parent_product_id +JOIN products sub_p + ON pr.sub_product_id = sub_p.id +LEFT JOIN stock_current s + ON pr.sub_product_id = s.product_id +WHERE sub_p.min_stock_amount != 0 + AND p.cumulate_min_stock_amount_of_sub_products = 0 +GROUP BY sub_p.id +HAVING IFNULL(SUM(s.amount), 0) < sub_p.min_stock_amount; diff --git a/public/css/grocy_night_mode.css b/public/css/grocy_night_mode.css index 89d7091d..801f9901 100644 --- a/public/css/grocy_night_mode.css +++ b/public/css/grocy_night_mode.css @@ -4,7 +4,8 @@ .night-mode .table-info, .night-mode .table-info > td, -.night-mode .table-info > th { +.night-mode .table-info > th, +.night-mode .alert-info { background-color: #07373f; color: #6c757d; } @@ -54,14 +55,17 @@ border-color: #0d3a18; } -.night-mode .btn-light { +.night-mode .btn-light, +.night-mode .input-group-text, +.night-mode .note-editor.note-frame .note-statusbar, +.night-mode .img-thumbnail { color: #c1c1c1; background-color: #292b2a; border-color: #292b2a; } .night-mode .form-control { - color: #495057; + color: #ced4da; background-color: #333131; border: 1px solid #ced4da; } @@ -100,7 +104,7 @@ } .night-mode .form-control:focus { - color: #495057; + color: #ced4da; background-color: #333131; border-color: #80bdff; } diff --git a/public/js/grocy.js b/public/js/grocy.js index 8d3e725e..637e5503 100644 --- a/public/js/grocy.js +++ b/public/js/grocy.js @@ -298,8 +298,21 @@ RefreshContextualTimeago = function() $("time.timeago").each(function() { var element = $(this); + + if (!element.hasAttr("datetime")) + { + element.text("") + return + } + var timestamp = element.attr("datetime"); + if (timestamp.isEmpty()) + { + element.text("") + return + } + var isNever = timestamp && timestamp.substring(0, 10) == "2999-12-31"; var isToday = timestamp && timestamp.substring(0, 10) == moment().format("YYYY-MM-DD"); var isDateWithoutTime = element.hasClass("timeago-date-only"); @@ -515,18 +528,33 @@ $("#about-dialog-link").on("click", function() function RefreshLocaleNumberDisplay() { - $(".locale-number-format[data-format='currency']").each(function() + $(".locale-number.locale-number-currency").each(function() { + if (isNaN(parseFloat($(this).text()))) + { + return; + } + $(this).text(parseFloat($(this).text()).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency })); }); - $(".locale-number-format[data-format='quantity-amount']").each(function() + $(".locale-number.locale-number-quantity-amount").each(function() { + if (isNaN(parseFloat($(this).text()))) + { + return; + } + $(this).text(parseFloat($(this).text()).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 3 })); }); - $(".locale-number-format[data-format='generic']").each(function () + $(".locale-number.locale-number-generic").each(function () { + if (isNaN(parseFloat($(this).text()))) + { + return; + } + $(this).text(parseFloat($(this).text()).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 })); }); } diff --git a/public/viewjs/choreform.js b/public/viewjs/choreform.js index 730a2bcd..1bd77713 100644 --- a/public/viewjs/choreform.js +++ b/public/viewjs/choreform.js @@ -3,7 +3,11 @@ e.preventDefault(); var jsonData = $('#chore-form').serializeJSON({ checkboxUncheckedValue: "0" }); - jsonData.assignment_config = $("#assignment_config").val().join(","); + if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_CHORES_ASSIGNMENTS) + { + jsonData.assignment_config = $("#assignment_config").val().join(","); + } + Grocy.FrontendHelpers.BeginUiBusy("chore-form"); if (Grocy.EditMode === 'create') diff --git a/public/viewjs/choretracking.js b/public/viewjs/choretracking.js index 6218008b..05b60fa9 100644 --- a/public/viewjs/choretracking.js +++ b/public/viewjs/choretracking.js @@ -8,7 +8,7 @@ Grocy.Api.Get('chores/' + jsonForm.chore_id, function (choreDetails) { - Grocy.Api.Post('chores/' + jsonForm.chore_id + '/execute', { 'tracked_time': Grocy.Components.DateTimePicker.GetValue(), 'done_by': Grocy.Components.UserPicker.GetValue() }, + Grocy.Api.Post('chores/' + jsonForm.chore_id + '/execute', { 'tracked_time': Grocy.Components.DateTimePicker.GetValue(), 'done_by': $("#user_id").val() }, function(result) { Grocy.FrontendHelpers.EndUiBusy("choretracking-form"); diff --git a/public/viewjs/equipment.js b/public/viewjs/equipment.js index 7d376510..f9198cf9 100644 --- a/public/viewjs/equipment.js +++ b/public/viewjs/equipment.js @@ -131,4 +131,5 @@ $("#selectedEquipmentDescriptionToggleFullscreenButton").on('click', function(e) $("#selectedEquipmentDescriptionCard").toggleClass("fullscreen"); $("#selectedEquipmentDescriptionCard .card-header").toggleClass("fixed-top"); $("#selectedEquipmentDescriptionCard .card-body").toggleClass("mt-5"); + $("body").toggleClass("fullscreen-card"); }); diff --git a/public/viewjs/mealplan.js b/public/viewjs/mealplan.js index 240d502c..9c987d07 100644 --- a/public/viewjs/mealplan.js +++ b/public/viewjs/mealplan.js @@ -54,7 +54,7 @@ var calendar = $("#calendar").fullCalendar({ weekRecipeOrderMissingButtonHtml = '' weekRecipeConsumeButtonHtml = '' } - $(".fc-header-toolbar .fc-center").html("
{{ $__t('The selected equipment has no instruction manual') }}
+{{ $__t('The selected equipment has no instruction manual') }}
-
-