mirror of
https://github.com/grocy/grocy.git
synced 2025-04-29 01:32:38 +00:00
Added a "consume this recipe"-button to the meal plan (and also a button to consume all recipes for a whole week) (closes #283)
This commit is contained in:
parent
09b23847b5
commit
1eb1aa8b11
@ -2,6 +2,7 @@
|
||||
- Fixed that "Track date only" chores were always tracked today, regardless of the given date
|
||||
- Fixed that the "week costs" were wrong after removing a meal plan entry
|
||||
- Fixed wrong recipes costs calculation with nested recipes when the base recipe servings are > 1 (also affected the meal plan when adding such a recipe there)
|
||||
- Fixed consuming recipes did not consume ingredients of the nested recipes
|
||||
- Improved recipes API - added new endpoints to get stock fulfillment information (thanks @Aerex)
|
||||
- Improved date display for products that never expires (instead of "2999-12-31" now just "Never" will be shown)
|
||||
- Improved date display for dates of today and no time (instead of the hours since midnight now just "Today" will be shown)
|
||||
@ -9,4 +10,5 @@
|
||||
- Items can now be switched between lists (there is a shopping list dropdown on the item edit page)
|
||||
- Items can now be marked as "done" (new check mark button per item, when clicked, the item will be displayed greyed out, when clicked again the item will be displayed normally again)
|
||||
- Improved that products can now also be consumed as spoiled from the stock overview page (option in the more/context menu per line)
|
||||
- Added a "consume this recipe"-button to the meal plan (and also a button to consume all recipes for a whole week)
|
||||
- Added a new `config.php` setting `DISABLE_AUTH` to be able to disable authentication / the login screen
|
||||
|
@ -1280,3 +1280,6 @@ msgstr ""
|
||||
|
||||
msgid "Consume %1$s of %2$s as spoiled"
|
||||
msgstr ""
|
||||
|
||||
msgid "Not all ingredients of recipe \"%s\" are in stock, nothing removed"
|
||||
msgstr ""
|
||||
|
87
migrations/0076.sql
Normal file
87
migrations/0076.sql
Normal file
@ -0,0 +1,87 @@
|
||||
DROP VIEW recipes_pos_resolved;
|
||||
CREATE VIEW recipes_pos_resolved
|
||||
AS
|
||||
|
||||
-- Multiplication by 1.0 to force conversion to float (REAL)
|
||||
|
||||
SELECT
|
||||
r.id AS recipe_id,
|
||||
rp.id AS recipe_pos_id,
|
||||
rp.product_id AS product_id,
|
||||
rp.amount * (r.desired_servings*1.0 / 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) AS recipe_amount,
|
||||
IFNULL(sc.amount, 0) AS stock_amount,
|
||||
CASE WHEN IFNULL(sc.amount, 0) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / 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) END THEN 1 ELSE 0 END AS need_fulfilled,
|
||||
CASE WHEN IFNULL(sc.amount, 0) - CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / 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) END < 0 THEN ABS(IFNULL(sc.amount, 0) - (CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / 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) END)) ELSE 0 END AS missing_amount,
|
||||
IFNULL(sl.amount, 0) * p.qu_factor_purchase_to_stock AS amount_on_shopping_list,
|
||||
CASE WHEN IFNULL(sc.amount, 0) + (CASE WHEN r.not_check_shoppinglist = 1 THEN 0 ELSE IFNULL(sl.amount, 0) END * p.qu_factor_purchase_to_stock) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / 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) END THEN 1 ELSE 0 END AS need_fulfilled_with_shopping_list,
|
||||
rp.qu_id,
|
||||
(CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / 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) END / p.qu_factor_purchase_to_stock) * pcp.last_price AS costs,
|
||||
CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos,
|
||||
rp.ingredient_group,
|
||||
rp.id, -- Just a dummy id column
|
||||
rnr.includes_recipe_id as child_recipe_id,
|
||||
rp.note,
|
||||
rp.variable_amount AS recipe_variable_amount,
|
||||
rp.only_check_single_unit_in_stock
|
||||
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
|
||||
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_price pcp
|
||||
ON rp.product_id = pcp.product_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,
|
||||
rp.amount * (r.desired_servings*1.0 / 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) AS recipe_amount,
|
||||
IFNULL(sc.amount, 0) AS stock_amount,
|
||||
1 AS need_fulfilled,
|
||||
0 AS missing_amount,
|
||||
IFNULL(sl.amount, 0) * p.qu_factor_purchase_to_stock AS amount_on_shopping_list,
|
||||
1 AS need_fulfilled_with_shopping_list,
|
||||
rp.qu_id,
|
||||
(CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / 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) END / p.qu_factor_purchase_to_stock) * pcp.last_price AS costs,
|
||||
CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos,
|
||||
rp.ingredient_group,
|
||||
rp.id, -- Just a dummy id column
|
||||
rnr.includes_recipe_id as child_recipe_id,
|
||||
rp.note,
|
||||
rp.variable_amount AS recipe_variable_amount,
|
||||
rp.only_check_single_unit_in_stock
|
||||
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
|
||||
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_price pcp
|
||||
ON rp.product_id = pcp.product_id
|
||||
WHERE rp.not_check_stock_fulfillment = 1;
|
@ -29,6 +29,7 @@ var calendar = $("#calendar").fullCalendar({
|
||||
|
||||
var weekCosts = 0;
|
||||
var weekRecipeOrderMissingButtonHtml = "";
|
||||
var weekRecipeConsumeButtonHtml = "";
|
||||
if (weekRecipe !== null)
|
||||
{
|
||||
weekCosts = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).costs;
|
||||
@ -38,9 +39,15 @@ var calendar = $("#calendar").fullCalendar({
|
||||
{
|
||||
weekRecipeOrderMissingButtonDisabledClasses = "disabled";
|
||||
}
|
||||
var weekRecipeConsumeButtonDisabledClasses = "";
|
||||
if (FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).need_fulfilled == 0)
|
||||
{
|
||||
weekRecipeConsumeButtonDisabledClasses = "disabled";
|
||||
}
|
||||
weekRecipeOrderMissingButtonHtml = '<a class="ml-1 btn btn-outline-primary btn-xs recipe-order-missing-button ' + weekRecipeOrderMissingButtonDisabledClasses + '" href="#" data-toggle="tooltip" title="' + __t("Put missing products on shopping list") + '" data-recipe-id="' + weekRecipe.id.toString() + '" data-recipe-name="' + weekRecipe.name + '" data-recipe-type="' + weekRecipe.type + '"><i class="fas fa-cart-plus"></i></a>'
|
||||
weekRecipeConsumeButtonHtml = '<a class="ml-1 btn btn-outline-success btn-xs recipe-consume-button ' + weekRecipeConsumeButtonDisabledClasses + '" href="#" data-toggle="tooltip" title="' + __t("Consume all ingredients needed by this recipe") + '" data-recipe-id="' + weekRecipe.id.toString() + '" data-recipe-name="' + weekRecipe.name + '" data-recipe-type="' + weekRecipe.type + '"><i class="fas fa-utensils"></i></a>'
|
||||
}
|
||||
$(".fc-header-toolbar .fc-center").html("<h4>" + __t("Week costs") + ': <span class="locale-number-format" data-format="currency">' + weekCosts.toString() + "</span> " + weekRecipeOrderMissingButtonHtml + "</h4>");
|
||||
$(".fc-header-toolbar .fc-center").html("<h4>" + __t("Week costs") + ': <span class="locale-number-format" data-format="currency">' + weekCosts.toString() + "</span> " + weekRecipeOrderMissingButtonHtml + weekRecipeConsumeButtonHtml + "</h4>");
|
||||
},
|
||||
"eventRender": function(event, element)
|
||||
{
|
||||
@ -60,6 +67,12 @@ var calendar = $("#calendar").fullCalendar({
|
||||
recipeOrderMissingButtonDisabledClasses = "disabled";
|
||||
}
|
||||
|
||||
var recipeConsumeButtonDisabledClasses = "";
|
||||
if (resolvedRecipe.need_fulfilled == 0)
|
||||
{
|
||||
recipeConsumeButtonDisabledClasses = "disabled";
|
||||
}
|
||||
|
||||
var fulfillmentInfoHtml = __t('Enough in stock');
|
||||
var fulfillmentIconHtml = '<i class="fas fa-check text-success"></i>';
|
||||
if (resolvedRecipe.need_fulfilled != 1)
|
||||
@ -77,6 +90,7 @@ var calendar = $("#calendar").fullCalendar({
|
||||
<h5> \
|
||||
<a class="ml-1 btn btn-outline-danger btn-xs remove-recipe-button" href="#"><i class="fas fa-trash"></i></a> \
|
||||
<a class="ml-1 btn btn-outline-primary btn-xs recipe-order-missing-button ' + recipeOrderMissingButtonDisabledClasses + '" href="#" data-toggle="tooltip" title="' + __t("Put missing products on shopping list") + '" data-recipe-id="' + recipe.id.toString() + '" data-recipe-name="' + recipe.name + '" data-recipe-type="' + recipe.type + '"><i class="fas fa-cart-plus"></i></a> \
|
||||
<a class="ml-1 btn btn-outline-success btn-xs recipe-consume-button ' + recipeConsumeButtonDisabledClasses + '" href="#" data-toggle="tooltip" title="' + __t("Consume all ingredients needed by this recipe") + '" data-recipe-id="' + recipe.id.toString() + '" data-recipe-name="' + recipe.name + '" data-recipe-type="' + recipe.type + '"><i class="fas fa-utensils"></i></a> \
|
||||
</h5> \
|
||||
</div>');
|
||||
|
||||
@ -230,3 +244,44 @@ $(document).on('click', '.recipe-order-missing-button', function(e)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on('click', '.recipe-consume-button', function(e)
|
||||
{
|
||||
var objectName = $(e.currentTarget).attr('data-recipe-name');
|
||||
var objectId = $(e.currentTarget).attr('data-recipe-id');
|
||||
|
||||
bootbox.confirm({
|
||||
message: __t('Are you sure to consume all ingredients needed by recipe "%s" (ingredients marked with "check only if a single unit is in stock" will be ignored)?', objectName),
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: __t('Yes'),
|
||||
className: 'btn-success'
|
||||
},
|
||||
cancel: {
|
||||
label: __t('No'),
|
||||
className: 'btn-danger'
|
||||
}
|
||||
},
|
||||
callback: function(result)
|
||||
{
|
||||
if (result === true)
|
||||
{
|
||||
Grocy.FrontendHelpers.BeginUiBusy();
|
||||
|
||||
Grocy.Api.Post('recipes/' + objectId + '/consume', { },
|
||||
function(result)
|
||||
{
|
||||
Grocy.FrontendHelpers.EndUiBusy();
|
||||
toastr.success(__t('Removed all ingredients of recipe "%s" from stock', objectName));
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
toastr.warning(__t('Not all ingredients of recipe "%s" are in stock, nothing removed', objectName));
|
||||
Grocy.FrontendHelpers.EndUiBusy();
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -187,6 +187,7 @@ $("#selectedRecipeConsumeButton").on('click', function(e)
|
||||
function(xhr)
|
||||
{
|
||||
Grocy.FrontendHelpers.EndUiBusy();
|
||||
toastr.warning(__t('Not all ingredients of recipe "%s" are in stock, nothing removed', objectName));
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
|
@ -67,12 +67,12 @@ class RecipesService extends BaseService
|
||||
throw new \Exception('Recipe does not exist');
|
||||
}
|
||||
|
||||
$recipePositions = $this->Database->recipes_pos()->where('recipe_id', $recipeId)->fetchAll();
|
||||
$recipePositions = $this->Database->recipes_pos_resolved()->where('recipe_id', $recipeId)->fetchAll();
|
||||
foreach ($recipePositions as $recipePosition)
|
||||
{
|
||||
if ($recipePosition->only_check_single_unit_in_stock == 0)
|
||||
{
|
||||
$this->StockService->ConsumeProduct($recipePosition->product_id, $recipePosition->amount, false, StockService::TRANSACTION_TYPE_CONSUME, 'default', $recipeId);
|
||||
$this->StockService->ConsumeProduct($recipePosition->product_id, $recipePosition->recipe_amount, false, StockService::TRANSACTION_TYPE_CONSUME, 'default', $recipeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -121,10 +121,10 @@
|
||||
<div id="selectedRecipeCard" class="card">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-cocktail"></i> {{ $selectedRecipe->name }}
|
||||
<a id="selectedRecipeConsumeButton" class="btn btn-sm btn-outline-success py-0" href="#" data-toggle="tooltip" title="{{ $__t('Consume all ingredients needed by this recipe') }}" data-recipe-id="{{ $selectedRecipe->id }}" data-recipe-name="{{ $selectedRecipe->name }}">
|
||||
<a id="selectedRecipeConsumeButton" class="btn btn-sm btn-outline-success py-0 @if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $selectedRecipe->id)->need_fulfilled == 0) disabled @endif" href="#" data-toggle="tooltip" title="{{ $__t('Consume all ingredients needed by this recipe') }}" data-recipe-id="{{ $selectedRecipe->id }}" data-recipe-name="{{ $selectedRecipe->name }}">
|
||||
<i class="fas fa-utensils"></i>
|
||||
</a>
|
||||
<a class="btn btn-sm btn-outline-primary py-0 recipe-order-missing-button @if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $selectedRecipe->id)->need_fulfilled_with_shopping_list == 1){{ disabled }}@endif" href="#" data-toggle="tooltip" title="{{ $__t('Put missing products on shopping list') }}" data-recipe-id="{{ $selectedRecipe->id }}" data-recipe-name="{{ $selectedRecipe->name }}">
|
||||
<a class="btn btn-sm btn-outline-primary py-0 recipe-order-missing-button @if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $selectedRecipe->id)->need_fulfilled_with_shopping_list == 1) disabled @endif" href="#" data-toggle="tooltip" title="{{ $__t('Put missing products on shopping list') }}" data-recipe-id="{{ $selectedRecipe->id }}" data-recipe-name="{{ $selectedRecipe->name }}">
|
||||
<i class="fas fa-cart-plus"></i>
|
||||
</a>
|
||||
<a id="selectedRecipeEditButton" class="btn btn-sm btn-outline-info py-0" href="{{ $U('/recipe/') }}{{ $selectedRecipe->id }}">
|
||||
|
Loading…
x
Reference in New Issue
Block a user