diff --git a/changelog/77_UNRELEASED_xxxx-xx-xx.md b/changelog/77_UNRELEASED_xxxx-xx-xx.md index 30222266..4676a5b1 100644 --- a/changelog/77_UNRELEASED_xxxx-xx-xx.md +++ b/changelog/77_UNRELEASED_xxxx-xx-xx.md @@ -41,12 +41,17 @@ ### Shopping list -- xxx +- Added a new shopping list setting (top right corner settings menu) "Round up quantity amounts to the nearest whole number" + - When enabled, all quantity amounts on the shopping list are always displayed rounded up to the nearest whole number + - Defaults to disabled, so no changed behavior when not configured ### Recipes - Consuming a recipe is now also possible when not all needed ingredients are currently in stock (the in stock amount, if any, of the corresponding ingredient will be consumed in that case) - For in stock ingredients, the amount actually in stock is now displayed next to the hint "Enough in stock" +- Added a new recipe ingredient option "Round up quantity amounts to the nearest whole number" + - When enabled, resulting quantity amounts (after scaling according the desired serving amount) are always rounded up to the nearest whole number + - Defaults to disabled, so no changed behavior when not configured - 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 - When no price information is available for at least one ingredient, a red exclamation mark is now displayed next to the recipe total cost information - 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 diff --git a/config-dist.php b/config-dist.php index 8df4e9ce..4267c75e 100644 --- a/config-dist.php +++ b/config-dist.php @@ -199,6 +199,7 @@ DefaultUserSetting('show_warning_on_purchase_when_due_date_is_earlier_than_next' // Shopping list settings DefaultUserSetting('shopping_list_to_stock_workflow_auto_submit_when_prefilled', false); // Automatically do the booking using the last price and the amount of the shopping list item, if the product has "Default due days" set DefaultUserSetting('shopping_list_show_calendar', false); // When enabled, a small (month view) calendar will be shown on the shopping list page +DefaultUserSetting('shopping_list_round_up', false); // When enabled, all quantity amounts on the shopping list are always displayed rounded up to the nearest whole number DefaultUserSetting('shopping_list_auto_add_below_min_stock_amount', false); // If products should be automatically added to the shopping list when they are below their min. stock amount DefaultUserSetting('shopping_list_auto_add_below_min_stock_amount_list_id', 1); // When the above setting is enabled, the id of the shopping list to which the products will be added diff --git a/localization/strings.pot b/localization/strings.pot index 33603884..3f3aa1ff 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -2470,3 +2470,6 @@ msgstr "" msgid "Unspecified" msgstr "" + +msgid "Round up quantity amounts to the nearest whole number" +msgstr "" diff --git a/migrations/0249.sql b/migrations/0249.sql new file mode 100644 index 00000000..85275b0b --- /dev/null +++ b/migrations/0249.sql @@ -0,0 +1,141 @@ +ALTER TABLE recipes_pos +ADD round_up TINYINT NOT NULL DEFAULT 0 CHECK(round_up IN (0, 1)); + +DROP VIEW recipes_pos_resolved; +CREATE VIEW recipes_pos_resolved +AS + +-- Multiplication by 1.0 to force conversion to float (REAL) + +-- Resolved amount (here used multiple times): +-- CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END + +SELECT + r.id AS recipe_id, + rp.id AS recipe_pos_id, + rp.product_id AS product_id, + CASE WHEN rp.round_up = 1 THEN CEIL(CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END) ELSE CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END END AS recipe_amount, + IFNULL(sc.amount_aggregated, 0) AS stock_amount, + CASE WHEN IFNULL(sc.amount_aggregated, 0) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 0.00000001 ELSE CASE WHEN rp.round_up = 1 THEN CEIL(CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END) ELSE CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END END END THEN 1 ELSE 0 END AS need_fulfilled, + CASE WHEN IFNULL(sc.amount_aggregated, 0) - CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 0.00000001 ELSE CASE WHEN rp.round_up = 1 THEN CEIL(CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END) ELSE CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END END END < 0 THEN ABS(IFNULL(sc.amount_aggregated, 0) - (CASE WHEN rp.round_up = 1 THEN CEIL(CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END) ELSE CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END END)) ELSE 0 END AS missing_amount, + IFNULL(sl.amount, 0) AS amount_on_shopping_list, + CASE WHEN ROUND(IFNULL(sc.amount_aggregated, 0) + CASE WHEN r.not_check_shoppinglist = 1 THEN 0 ELSE IFNULL(sl.amount, 0) END, 2) >= ROUND(CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 0.00000001 ELSE CASE WHEN rp.round_up = 1 THEN CEIL(CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END) ELSE CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END END END, 2) THEN 1 ELSE 0 END AS need_fulfilled_with_shopping_list, + rp.qu_id, + (r.desired_servings*1.0 / r.base_servings*1.0) * CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN IFNULL(qucr.factor, 1.0) ELSE 1 END * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) * rp.amount * IFNULL(pcp.price, 0) * rp.price_factor * CASE WHEN rp.product_id != p_effective.id THEN IFNULL(qucr.factor, 1.0) ELSE 1.0 END AS costs, + CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos, + rp.ingredient_group, + pg.name as product_group, + rp.id, -- Just a dummy id column + r.type as recipe_type, + rnr.includes_recipe_id as child_recipe_id, + rp.note, + rp.variable_amount AS recipe_variable_amount, + rp.only_check_single_unit_in_stock, + rp.amount * CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN IFNULL(qucr.factor, 1.0) ELSE 1 END / r.base_servings*1.0 * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) * IFNULL(p_effective.calories, 0) * CASE WHEN rp.product_id != p_effective.id THEN IFNULL(qucr.factor, 1.0) ELSE 1.0 END AS calories, + p.active AS product_active, + CASE pvs.current_due_status + WHEN 'ok' THEN 0 + WHEN 'due_soon' THEN 1 + WHEN 'overdue' THEN 10 + WHEN 'expired' THEN 20 + END AS due_score, + IFNULL(pcs.product_id_effective, rp.product_id) AS product_id_effective, + p.name AS product_name +FROM recipes r +JOIN recipes_nestings_resolved rnr + ON r.id = rnr.recipe_id +JOIN recipes rnrr + ON rnr.includes_recipe_id = rnrr.id +JOIN recipes_pos rp + ON rnr.includes_recipe_id = rp.recipe_id +JOIN products p + ON rp.product_id = p.id +JOIN products_volatile_status pvs + ON rp.product_id = pvs.product_id +LEFT JOIN product_groups pg + ON p.product_group_id = pg.id +LEFT JOIN ( + SELECT product_id, SUM(amount) AS amount + FROM shopping_list + GROUP BY product_id) sl + ON rp.product_id = sl.product_id +LEFT JOIN stock_current sc + ON rp.product_id = sc.product_id +LEFT JOIN products_current_substitutions pcs + ON rp.product_id = pcs.parent_product_id +LEFT JOIN products_current_price pcp + ON IFNULL(pcs.product_id_effective, rp.product_id) = pcp.product_id +LEFT JOIN products p_effective + ON IFNULL(pcs.product_id_effective, rp.product_id) = p_effective.id +LEFT JOIN cache__quantity_unit_conversions_resolved qucr + ON IFNULL(pcs.product_id_effective, rp.product_id) = qucr.product_id + AND CASE WHEN rp.product_id != p_effective.id THEN p.qu_id_stock ELSE rp.qu_id END = qucr.from_qu_id + AND IFNULL(p_effective.qu_id_stock, p.qu_id_stock) = qucr.to_qu_id +WHERE rp.not_check_stock_fulfillment = 0 + +UNION + +-- Just add all recipe positions which should not be checked against stock with fulfilled need + +SELECT + r.id AS recipe_id, + rp.id AS recipe_pos_id, + rp.product_id AS product_id, + CASE WHEN rp.round_up = 1 THEN CEIL(CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END) ELSE CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END END AS recipe_amount, + IFNULL(sc.amount_aggregated, 0) AS stock_amount, + 1 AS need_fulfilled, + 0 AS missing_amount, + IFNULL(sl.amount, 0) AS amount_on_shopping_list, + 1 AS need_fulfilled_with_shopping_list, + rp.qu_id, + (r.desired_servings*1.0 / r.base_servings*1.0) * CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN IFNULL(qucr.factor, 1.0) ELSE 1 END * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) * rp.amount * IFNULL(pcp.price, 0) * rp.price_factor * CASE WHEN rp.product_id != p_effective.id THEN IFNULL(qucr.factor, 1.0) ELSE 1.0 END AS costs, + CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos, + rp.ingredient_group, + pg.name as product_group, + rp.id, -- Just a dummy id column + r.type as recipe_type, + rnr.includes_recipe_id as child_recipe_id, + rp.note, + rp.variable_amount AS recipe_variable_amount, + rp.only_check_single_unit_in_stock, + rp.amount * CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN IFNULL(qucr.factor, 1.0) ELSE 1 END / r.base_servings*1.0 * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) * IFNULL(p_effective.calories, 0) * CASE WHEN rp.product_id != p_effective.id THEN IFNULL(qucr.factor, 1.0) ELSE 1.0 END AS calories, + p.active AS product_active, + CASE pvs.current_due_status + WHEN 'ok' THEN 0 + WHEN 'due_soon' THEN 1 + WHEN 'overdue' THEN 10 + WHEN 'expired' THEN 20 + END AS due_score, + IFNULL(pcs.product_id_effective, rp.product_id) AS product_id_effective, + p.name AS product_name +FROM recipes r +JOIN recipes_nestings_resolved rnr + ON r.id = rnr.recipe_id +JOIN recipes rnrr + ON rnr.includes_recipe_id = rnrr.id +JOIN recipes_pos rp + ON rnr.includes_recipe_id = rp.recipe_id +JOIN products p + ON rp.product_id = p.id +JOIN products_volatile_status pvs + ON rp.product_id = pvs.product_id +LEFT JOIN product_groups pg + ON p.product_group_id = pg.id +LEFT JOIN ( + SELECT product_id, SUM(amount) AS amount + FROM shopping_list + GROUP BY product_id) sl + ON rp.product_id = sl.product_id +LEFT JOIN stock_current sc + ON rp.product_id = sc.product_id +LEFT JOIN products_current_substitutions pcs + ON rp.product_id = pcs.parent_product_id +LEFT JOIN products_current_price pcp + ON IFNULL(pcs.product_id_effective, rp.product_id) = pcp.product_id +LEFT JOIN products p_effective + ON IFNULL(pcs.product_id_effective, rp.product_id) = p_effective.id +LEFT JOIN cache__quantity_unit_conversions_resolved qucr + ON IFNULL(pcs.product_id_effective, rp.product_id) = qucr.product_id + AND CASE WHEN rp.product_id != p_effective.id THEN p.qu_id_stock ELSE rp.qu_id END = qucr.from_qu_id + AND IFNULL(p_effective.qu_id_stock, p.qu_id_stock) = qucr.to_qu_id +WHERE rp.not_check_stock_fulfillment = 1; diff --git a/public/viewjs/recipes.js b/public/viewjs/recipes.js index bb7f2480..510fd1fc 100644 --- a/public/viewjs/recipes.js +++ b/public/viewjs/recipes.js @@ -219,7 +219,7 @@ $(document).on('click', '.recipe-shopping-list', function(e) Grocy.Api.Post('recipes/' + objectId + '/add-not-fulfilled-products-to-shoppinglist', { "excludedProductIds": excludedProductIds }, function(result) { - window.location.href = U('/recipes'); + window.location.reload(); }, function(xhr) { diff --git a/public/viewjs/shoppinglistsettings.js b/public/viewjs/shoppinglistsettings.js index b3b49dac..882faadb 100644 --- a/public/viewjs/shoppinglistsettings.js +++ b/public/viewjs/shoppinglistsettings.js @@ -8,6 +8,11 @@ if (BoolVal(Grocy.UserSettings.shopping_list_show_calendar)) $("#shopping_list_show_calendar").prop("checked", true); } +if (BoolVal(Grocy.UserSettings.shopping_list_round_up)) +{ + $("#shopping_list_round_up").prop("checked", true); +} + if (BoolVal(Grocy.UserSettings.shopping_list_auto_add_below_min_stock_amount)) { $("#shopping_list_auto_add_below_min_stock_amount").prop("checked", true); diff --git a/services/DatabaseService.php b/services/DatabaseService.php index e22d0c76..c5914e49 100644 --- a/services/DatabaseService.php +++ b/services/DatabaseService.php @@ -103,6 +103,14 @@ class DatabaseService return $usersService->GetUserSetting(GROCY_USER_ID, $value); }); + + // Unfortunately not included by default + // https://www.sqlite.org/lang_mathfunc.html#ceil + $pdo->sqliteCreateFunction('ceil', function ($value) + { + return ceil($value); + }); + self::$DbConnectionRaw = $pdo; } diff --git a/views/recipeposform.blade.php b/views/recipeposform.blade.php index ba2c6f4b..5feee2c1 100644 --- a/views/recipeposform.blade.php +++ b/views/recipeposform.blade.php @@ -80,6 +80,14 @@ id="variable_amount" name="variable_amount" value="@if($mode == 'edit'){{ $recipePos->variable_amount }}@endif"> + +
+ round_up == 1) checked @endif class="form-check-input custom-control-input" type="checkbox" id="round_up" name="round_up" value="1"> + +
diff --git a/views/shoppinglist.blade.php b/views/shoppinglist.blade.php index d2865def..d074b436 100644 --- a/views/shoppinglist.blade.php +++ b/views/shoppinglist.blade.php @@ -254,6 +254,11 @@ $listItem->amount = $listItem->amount * $productQuConversion->factor; } } + + if(boolval($userSettings['shopping_list_round_up'])) + { + $listItem->amount = ceil($listItem->amount); + } @endphp @endif diff --git a/views/shoppinglistsettings.blade.php b/views/shoppinglistsettings.blade.php index 73ab3a05..1d238e85 100644 --- a/views/shoppinglistsettings.blade.php +++ b/views/shoppinglistsettings.blade.php @@ -14,6 +14,7 @@

{{ $__t('Shopping list') }}

+
+
+
+
+ + +
+
+ +
+

{{ $__t('Shopping list to stock workflow') }}

+