Allow partially in stock recipes to be consumed (closes #386)

This commit is contained in:
Bernd Bestel 2025-01-18 10:23:31 +01:00
parent f9c7c67dc7
commit 23d7b6ad3c
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
6 changed files with 24 additions and 30 deletions

View File

@ -37,6 +37,7 @@
### Recipes ### 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" - For in-stock ingredients, the amount actually in-stock is now displayed next to the hint "Enough in stock"
- 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 - 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 - 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

View File

@ -585,7 +585,7 @@ msgstr ""
msgid "Are you sure you want to consume all ingredients needed by recipe \"%s\" (ingredients marked with \"only check if any amount is in stock\" will be ignored)?" msgid "Are you sure you want to consume all ingredients needed by recipe \"%s\" (ingredients marked with \"only check if any amount is in stock\" will be ignored)?"
msgstr "" msgstr ""
msgid "Removed all ingredients of recipe \"%s\" from stock" msgid "Removed all in stock ingredients needed by recipe \"%s\" from stock"
msgstr "" msgstr ""
msgid "Consume all ingredients needed by this recipe" msgid "Consume all ingredients needed by this recipe"
@ -2455,3 +2455,6 @@ msgstr ""
msgid "No price information is available for at least one ingredient" msgid "No price information is available for at least one ingredient"
msgstr "" msgstr ""
msgid "For ingredients that are only partially in stock, the in stock amount will be consumed."
msgstr ""

View File

@ -98,19 +98,13 @@ $(".calendar").each(function()
weekRecipeOrderMissingButtonDisabledClasses = "disabled"; weekRecipeOrderMissingButtonDisabledClasses = "disabled";
} }
var weekRecipeConsumeButtonDisabledClasses = "";
if (weekRecipeResolved.need_fulfilled == 0 || weekCosts == 0)
{
weekRecipeConsumeButtonDisabledClasses = "disabled";
}
var weekRecipeOrderMissingButtonHtml = ""; var weekRecipeOrderMissingButtonHtml = "";
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_SHOPPINGLIST) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_SHOPPINGLIST)
{ {
weekRecipeOrderMissingButtonHtml = '<a class="ml-2 btn btn-outline-primary btn-xs recipe-order-missing-button d-print-none ' + 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="fa-solid fa-cart-plus"></i></a>'; weekRecipeOrderMissingButtonHtml = '<a class="ml-2 btn btn-outline-primary btn-xs recipe-order-missing-button d-print-none ' + 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="fa-solid fa-cart-plus"></i></a>';
} }
weekRecipeConsumeButtonHtml = '<a class="ml-2 btn btn-outline-success btn-xs recipe-consume-button d-print-none ' + weekRecipeConsumeButtonDisabledClasses + '" href="#" data-toggle="tooltip" title="' + __t("Consume all ingredients needed by this weeks recipes or products") + '" data-recipe-id="' + weekRecipe.id.toString() + '" data-recipe-name="' + weekRecipe.name + '" data-recipe-type="' + weekRecipe.type + '"><i class="fa-solid fa-utensils"></i></a>' weekRecipeConsumeButtonHtml = '<a class="ml-2 btn btn-outline-success btn-xs recipe-consume-button d-print-none" href="#" data-toggle="tooltip" title="' + __t("Consume all ingredients needed by this weeks recipes or products") + '" data-recipe-id="' + weekRecipe.id.toString() + '" data-recipe-name="' + weekRecipe.name + '" data-recipe-type="' + weekRecipe.type + '"><i class="fa-solid fa-utensils"></i></a>'
} }
$(".calendar[data-primary-section='true'] .fc-header-toolbar .fc-center").html("<h4>" + weekCostsHtml + weekRecipeOrderMissingButtonHtml + weekRecipeConsumeButtonHtml + "</h4>"); $(".calendar[data-primary-section='true'] .fc-header-toolbar .fc-center").html("<h4>" + weekCostsHtml + weekRecipeOrderMissingButtonHtml + weekRecipeConsumeButtonHtml + "</h4>");
}, },
@ -157,12 +151,6 @@ $(".calendar").each(function()
recipeOrderMissingButtonDisabledClasses = "disabled"; recipeOrderMissingButtonDisabledClasses = "disabled";
} }
var recipeConsumeButtonDisabledClasses = "";
if (resolvedRecipe.need_fulfilled == 0)
{
recipeConsumeButtonDisabledClasses = "disabled";
}
var fulfillmentInfoHtml = __t('Enough in stock'); var fulfillmentInfoHtml = __t('Enough in stock');
var fulfillmentIconHtml = '<i class="fa-solid fa-check text-success"></i>'; var fulfillmentIconHtml = '<i class="fa-solid fa-check text-success"></i>';
if (resolvedRecipe.need_fulfilled != 1) if (resolvedRecipe.need_fulfilled != 1)
@ -201,7 +189,7 @@ $(".calendar").each(function()
<h5 class="d-print-none"> \ <h5 class="d-print-none"> \
<a class="ml-2 btn btn-outline-info btn-xs edit-meal-plan-entry-button" href="#" data-toggle="tooltip" title="' + __t("Edit this item") + '"><i class="fa-solid fa-edit"></i></a> \ <a class="ml-2 btn btn-outline-info btn-xs edit-meal-plan-entry-button" href="#" data-toggle="tooltip" title="' + __t("Edit this item") + '"><i class="fa-solid fa-edit"></i></a> \
<a class="btn btn-outline-danger btn-xs remove-recipe-button" href="#" data-toggle="tooltip" title="' + __t("Delete this item") + '"><i class="fa-solid fa-trash"></i></a> \ <a class="btn btn-outline-danger btn-xs remove-recipe-button" href="#" data-toggle="tooltip" title="' + __t("Delete this item") + '"><i class="fa-solid fa-trash"></i></a> \
<a class="ml-2 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="' + internalShadowRecipe.id.toString() + '" data-mealplan-entry-id="' + mealPlanEntry.id.toString() + '" data-recipe-name="' + recipe.name + '" data-recipe-type="' + recipe.type + '"><i class="fa-solid fa-utensils"></i></a> \ <a class="ml-2 btn btn-outline-success btn-xs recipe-consume-button" href="#" data-toggle="tooltip" title="' + __t("Consume all ingredients needed by this recipe") + '" data-recipe-id="' + internalShadowRecipe.id.toString() + '" data-mealplan-entry-id="' + mealPlanEntry.id.toString() + '" data-recipe-name="' + recipe.name + '" data-recipe-type="' + recipe.type + '"><i class="fa-solid fa-utensils"></i></a> \
' + shoppingListButtonHtml + ' \ ' + shoppingListButtonHtml + ' \
' + doneButtonHtml + ' \ ' + doneButtonHtml + ' \
</h5> \ </h5> \
@ -865,7 +853,8 @@ $(document).on('click', '.recipe-consume-button', function(e)
var mealPlanEntryId = $(e.currentTarget).attr('data-mealplan-entry-id'); var mealPlanEntryId = $(e.currentTarget).attr('data-mealplan-entry-id');
bootbox.confirm({ bootbox.confirm({
message: __t('Are you sure you want to consume all ingredients needed by recipe "%s" (ingredients marked with "only check if any amount is in stock" will be ignored)?', objectName), message: __t('Are you sure you want to consume all ingredients needed by recipe "%s" (ingredients marked with "only check if any amount is in stock" will be ignored)?', objectName) +
"<br><br>(" + __t("For ingredients that are only partially in stock, the in stock amount will be consumed.") + ")",
closeButton: false, closeButton: false,
buttons: { buttons: {
confirm: { confirm: {
@ -890,7 +879,7 @@ $(document).on('click', '.recipe-consume-button', function(e)
function(result) function(result)
{ {
Grocy.FrontendHelpers.EndUiBusy(); Grocy.FrontendHelpers.EndUiBusy();
toastr.success(__t('Removed all ingredients of recipe "%s" from stock', objectName)); toastr.success(__t('Removed all in stock ingredients needed by recipe \"%s\" from stock', objectName));
window.location.reload(); window.location.reload();
}, },
function(xhr) function(xhr)

View File

@ -238,7 +238,8 @@ $(".recipe-consume").on('click', function(e)
var objectId = $(e.currentTarget).attr('data-recipe-id'); var objectId = $(e.currentTarget).attr('data-recipe-id');
bootbox.confirm({ bootbox.confirm({
message: __t('Are you sure you want to consume all ingredients needed by recipe "%s" (ingredients marked with "only check if any amount is in stock" will be ignored)?', objectName), message: __t('Are you sure you want to consume all ingredients needed by recipe "%s" (ingredients marked with "only check if any amount is in stock" will be ignored)?', objectName) +
"<br><br>(" + __t("For ingredients that are only partially in stock, the in stock amount will be consumed.") + ")",
closeButton: false, closeButton: false,
buttons: { buttons: {
confirm: { confirm: {
@ -260,7 +261,7 @@ $(".recipe-consume").on('click', function(e)
function(result) function(result)
{ {
Grocy.FrontendHelpers.EndUiBusy(); Grocy.FrontendHelpers.EndUiBusy();
toastr.success(__t('Removed all ingredients of recipe "%s" from stock', objectName)); toastr.success(__t('Removed all in stock ingredients needed by recipe \"%s\" from stock', objectName));
}, },
function(xhr) function(xhr)
{ {

View File

@ -78,12 +78,6 @@ class RecipesService extends BaseService
throw new \Exception('Recipe does not exist'); throw new \Exception('Recipe does not exist');
} }
$recipeResolved = $this->getDatabase()->recipes_resolved()->where('recipe_id', $recipeId)->fetch();
if ($recipeResolved->need_fulfilled == 0)
{
throw new \Exception('Recipe need is not fulfilled, consuming not possible');
}
$transactionId = uniqid(); $transactionId = uniqid();
$recipePositions = $this->getDatabase()->recipes_pos_resolved()->where('recipe_id', $recipeId)->fetchAll(); $recipePositions = $this->getDatabase()->recipes_pos_resolved()->where('recipe_id', $recipeId)->fetchAll();
@ -92,9 +86,15 @@ class RecipesService extends BaseService
{ {
foreach ($recipePositions as $recipePosition) foreach ($recipePositions as $recipePosition)
{ {
if ($recipePosition->only_check_single_unit_in_stock == 0) if ($recipePosition->only_check_single_unit_in_stock == 0 && $recipePosition->stock_amount > 0)
{ {
$this->getStockService()->ConsumeProduct($recipePosition->product_id, $recipePosition->recipe_amount, false, StockService::TRANSACTION_TYPE_CONSUME, 'default', $recipeId, null, $transactionId, true, true); $amount = $recipePosition->recipe_amount;
if ($recipePosition->stock_amount > 0 && $recipePosition->stock_amount < $recipePosition->recipe_amount)
{
$amount = $recipePosition->stock_amount;
}
$this->getStockService()->ConsumeProduct($recipePosition->product_id, $amount, false, StockService::TRANSACTION_TYPE_CONSUME, 'default', $recipeId, null, $transactionId, true, true);
} }
} }
} }

View File

@ -333,7 +333,7 @@
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<h3 class="card-title mb-0">{{ $recipe->name }}</h3> <h3 class="card-title mb-0">{{ $recipe->name }}</h3>
<div class="card-icons d-flex flex-wrap justify-content-end flex-shrink-1"> <div class="card-icons d-flex flex-wrap justify-content-end flex-shrink-1">
<a class="btn @if(!GROCY_FEATURE_FLAG_STOCK) d-none @endif recipe-consume @if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 0) disabled @endif" <a class="btn @if(!GROCY_FEATURE_FLAG_STOCK) d-none @endif recipe-consume"
href="#" href="#"
data-toggle="tooltip" data-toggle="tooltip"
title="{{ $__t('Consume all ingredients needed by this recipe') }}" title="{{ $__t('Consume all ingredients needed by this recipe') }}"
@ -369,7 +369,7 @@
<div class="mb-4 @if(!empty($recipe->picture_file_name)) d-none @else d-flex @endif d-print-block justify-content-between align-items-center"> <div class="mb-4 @if(!empty($recipe->picture_file_name)) d-none @else d-flex @endif d-print-block justify-content-between align-items-center">
<h1 class="card-title mb-0">{{ $recipe->name }}</h1> <h1 class="card-title mb-0">{{ $recipe->name }}</h1>
<div class="card-icons d-flex flex-wrap justify-content-end flex-shrink-1 d-print-none"> <div class="card-icons d-flex flex-wrap justify-content-end flex-shrink-1 d-print-none">
<a class="btn recipe-consume @if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 0) disabled @endif" <a class="btn recipe-consume"
href="#" href="#"
data-toggle="tooltip" data-toggle="tooltip"
title="{{ $__t('Consume all ingredients needed by this recipe') }}" title="{{ $__t('Consume all ingredients needed by this recipe') }}"
@ -427,7 +427,7 @@
<h3> <h3>
<span class="locale-number locale-number-currency pt-0">{{ $costs }}</span> <span class="locale-number locale-number-currency pt-0">{{ $costs }}</span>
@if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->prices_incomplete) @if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->prices_incomplete)
<i class="fa-solid fa-exclamation text-danger" <i class="small fa-solid fa-exclamation text-danger"
data-toggle="tooltip" data-toggle="tooltip"
data-trigger="hover click" data-trigger="hover click"
title="{{ $__t('No price information is available for at least one ingredient') }}"></i> title="{{ $__t('No price information is available for at least one ingredient') }}"></i>