diff --git a/controllers/RecipesController.php b/controllers/RecipesController.php index 3ff3316d..f6c3cb14 100644 --- a/controllers/RecipesController.php +++ b/controllers/RecipesController.php @@ -17,40 +17,27 @@ class RecipesController extends BaseController public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { $recipes = $this->Database->recipes()->orderBy('name'); + $recipesResolved = $this->RecipesService->GetRecipesResolved(); $selectedRecipe = null; - $selectedRecipePositions = null; + $selectedRecipePositionsResolved = null; if (isset($request->getQueryParams()['recipe'])) { $selectedRecipe = $this->Database->recipes($request->getQueryParams()['recipe']); - $selectedRecipePositions = $this->Database->recipes_pos()->where('recipe_id', $request->getQueryParams()['recipe'])->orderBy('ingredient_group'); + $selectedRecipePositionsResolved = $this->Database->recipes_pos_resolved()->where('recipe_id', $request->getQueryParams()['recipe'])->orderBy('ingredient_group'); } else { foreach ($recipes as $recipe) { $selectedRecipe = $recipe; - $selectedRecipePositions = $this->Database->recipes_pos()->where('recipe_id', $recipe->id)->orderBy('ingredient_group'); + $selectedRecipePositionsResolved = $this->Database->recipes_pos_resolved()->where('recipe_id', $recipe->id)->orderBy('ingredient_group'); break; } } $selectedRecipeSubRecipes = $this->Database->recipes()->where('id IN (SELECT includes_recipe_id FROM recipes_nestings_resolved WHERE recipe_id = :1 AND includes_recipe_id != :1)', $selectedRecipe->id)->orderBy('name')->fetchAll(); - $selectedRecipeSubRecipesPositions = $this->Database->recipes_pos()->where('recipe_id IN (SELECT includes_recipe_id FROM recipes_nestings_resolved WHERE recipe_id = :1 AND includes_recipe_id != :1)', $selectedRecipe->id)->orderBy('ingredient_group')->fetchAll(); - - // Scale ingredients amount based on desired servings & calculate total costs - $recipesFulfillment = $this->RecipesService->GetRecipesFulfillment(); - $totalRecipeCosts = 0; - foreach ($selectedRecipePositions as $selectedRecipePosition) - { - $selectedRecipePosition->amount = $selectedRecipePosition->amount * ($selectedRecipe->desired_servings / $selectedRecipe->base_servings); - $totalRecipeCosts += FindObjectInArrayByPropertyValue($recipesFulfillment, 'recipe_pos_id', $selectedRecipePosition->id)->costs; - } - foreach ($selectedRecipeSubRecipesPositions as $selectedSubRecipePosition) - { - $selectedSubRecipePosition->amount = $selectedSubRecipePosition->amount * ($selectedRecipe->desired_servings / $selectedRecipe->base_servings); - $totalRecipeCosts += FindObjectInArrayByPropertyValue($recipesFulfillment, 'recipe_pos_id', $selectedSubRecipePosition->id)->costs; - } + $selectedRecipeSubRecipesPositions = $this->Database->recipes_pos_resolved()->where('recipe_id = :1', $selectedRecipe->id)->orderBy('ingredient_group')->fetchAll(); $includedRecipeIdsAbsolute = array(); $includedRecipeIdsAbsolute[] = $selectedRecipe->id; @@ -61,16 +48,16 @@ class RecipesController extends BaseController return $this->AppContainer->view->render($response, 'recipes', [ 'recipes' => $recipes, - 'recipesFulfillment' => $recipesFulfillment, - 'recipesSumFulfillment' => $this->RecipesService->GetRecipesSumFulfillment(), + 'recipesResolved' => $recipesResolved, + 'recipePositionsResolved' => $this->Database->recipes_pos_resolved(), 'selectedRecipe' => $selectedRecipe, - 'selectedRecipePositions' => $selectedRecipePositions, + 'selectedRecipePositionsResolved' => $selectedRecipePositionsResolved, 'products' => $this->Database->products(), 'quantityunits' => $this->Database->quantity_units(), 'selectedRecipeSubRecipes' => $selectedRecipeSubRecipes, 'selectedRecipeSubRecipesPositions' => $selectedRecipeSubRecipesPositions, 'includedRecipeIdsAbsolute' => $includedRecipeIdsAbsolute, - 'totalRecipeCosts' => $totalRecipeCosts + 'selectedRecipeTotalCosts' => FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $selectedRecipe->id)->costs ]); } @@ -93,8 +80,8 @@ class RecipesController extends BaseController 'mode' => 'edit', 'products' => $this->Database->products(), 'quantityunits' => $this->Database->quantity_units(), - 'recipesFulfillment' => $this->RecipesService->GetRecipesFulfillment(), - 'recipesSumFulfillment' => $this->RecipesService->GetRecipesSumFulfillment(), + 'recipePositionsResolved' => $this->RecipesService->GetRecipesPosResolved(), + 'recipesResolved' => $this->RecipesService->GetRecipesResolved(), 'recipes' => $this->Database->recipes()->orderBy('name'), 'recipeNestings' => $this->Database->recipes_nestings()->where('recipe_id', $recipeId) ]); diff --git a/localization/en/strings.php b/localization/en/strings.php index ea5998e9..24797948 100644 --- a/localization/en/strings.php +++ b/localization/en/strings.php @@ -344,5 +344,6 @@ return array( 'Tare weight handling enabled - please weigh the whole container, the amount to be posted will be automatically calculcated' => 'Tare weight handling enabled - please weigh the whole container, the amount to be posted will be automatically calculcated', 'You have to select a location' => 'You have to select a location', 'List' => 'List', - 'Gallery' => 'Gallery' + 'Gallery' => 'Gallery', + 'The current picture will be deleted when you save the recipe' => 'The current picture will be deleted when you save the recipe' ); diff --git a/migrations/0058.sql b/migrations/0058.sql new file mode 100644 index 00000000..400d5559 --- /dev/null +++ b/migrations/0058.sql @@ -0,0 +1,109 @@ +ALTER TABLE recipes_nestings +ADD servings INTEGER DEFAULT 1; + +DROP VIEW recipes_nestings_resolved; +CREATE VIEW recipes_nestings_resolved +AS +WITH RECURSIVE r1(recipe_id, includes_recipe_id, includes_servings) +AS ( + SELECT id, id, 1 + FROM recipes + + UNION ALL + + SELECT rn.recipe_id, r1.includes_recipe_id, rn.servings + FROM recipes_nestings rn, r1 r1 + WHERE rn.includes_recipe_id = r1.recipe_id + LIMIT 100 -- This is just a safety limit to prevent infinite loops due to infinite nested recipes +) +SELECT * +FROM r1; + +DROP VIEW recipes_fulfillment; +CREATE VIEW recipes_pos_resolved +AS +SELECT + r.id AS recipe_id, + rp.id AS recipe_pos_id, + rp.product_id AS product_id, + rp.amount * (r.desired_servings / r.base_servings) * rnr.includes_servings 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 IFNULL(rp.amount, 0) * (r.desired_servings / r.base_servings) * rnr.includes_servings 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 IFNULL(rp.amount, 0) * (r.desired_servings / r.base_servings) * rnr.includes_servings END < 0 THEN ABS(IFNULL(sc.amount, 0) - (CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE IFNULL(rp.amount, 0) * (r.desired_servings / r.base_servings) * rnr.includes_servings 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 IFNULL(rp.amount, 0) * (r.desired_servings / r.base_servings) * rnr.includes_servings 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 / r.base_servings) * rnr.includes_servings 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 +FROM recipes r +JOIN recipes_nestings_resolved rnr + ON r.id = rnr.recipe_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 / r.base_servings) * rnr.includes_servings 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 / r.base_servings) * rnr.includes_servings 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 +FROM recipes r +JOIN recipes_nestings_resolved rnr + ON r.id = rnr.recipe_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; + +DROP VIEW recipes_fulfillment_sum; +CREATE VIEW recipes_resolved +AS +SELECT + r.id AS recipe_id, + IFNULL(MIN(rpr.need_fulfilled), 1) AS need_fulfilled, + IFNULL(MIN(rpr.need_fulfilled_with_shopping_list), 1) AS need_fulfilled_with_shopping_list, + (SELECT COUNT(*) FROM recipes_pos_resolved WHERE recipe_id = r.id AND need_fulfilled = 0) AS missing_products_count, + SUM(rpr.costs) AS costs +FROM recipes r +LEFT JOIN recipes_pos_resolved rpr + ON r.id = rpr.recipe_id +GROUP BY r.id; diff --git a/public/viewjs/recipeform.js b/public/viewjs/recipeform.js index 4eea4642..3e5d04d7 100644 --- a/public/viewjs/recipeform.js +++ b/public/viewjs/recipeform.js @@ -235,14 +235,16 @@ $(document).on('click', '.recipe-include-edit-button', function (e) { var id = $(e.currentTarget).attr('data-recipe-include-id'); var recipeId = $(e.currentTarget).attr('data-recipe-included-recipe-id'); - console.log(recipeId); + var recipeServings = $(e.currentTarget).attr('data-recipe-included-recipe-servings'); + Grocy.Api.Put('objects/recipes/' + Grocy.EditObjectId, $('#recipe-form').serializeJSON(), function(result) { $("#recipe-include-editform-title").text(L("Edit included recipe")); $("#recipe-include-form").data("edit-mode", "edit"); $("#recipe-include-form").data("recipe-nesting-id", id); - $("#includes_recipe_id").val(recipeId); + Grocy.Components.RecipePicker.SetId(recipeId); + $("#includes_servings").val(recipeServings); $("#recipe-include-editform-modal").modal("show"); Grocy.FrontendHelpers.ValidateForm("recipe-include-form"); }, @@ -274,7 +276,7 @@ $("#recipe-include-add-button").on("click", function(e) { $("#recipe-include-editform-title").text(L("Add included recipe")); $("#recipe-include-form").data("edit-mode", "create"); - $("#includes_recipe_id").val(""); + Grocy.Components.RecipePicker.Clear(); $("#recipe-include-editform-modal").modal("show"); Grocy.FrontendHelpers.ValidateForm("recipe-include-form"); }, @@ -289,10 +291,17 @@ $('#save-recipe-include-button').on('click', function(e) { e.preventDefault(); + if (document.getElementById("recipe-include-form").checkValidity() === false) //There is at least one validation error + { + return false; + } + var nestingId = $("#recipe-include-form").data("recipe-nesting-id"); var editMode = $("#recipe-include-form").data("edit-mode"); - var jsonData = $('#recipe-include-form').serializeJSON(); + var jsonData = {}; + jsonData.includes_recipe_id = Grocy.Components.RecipePicker.GetValue(); + jsonData.servings = $("#includes_servings").val(); jsonData.recipe_id = Grocy.EditObjectId; if (editMode === 'create') diff --git a/services/RecipesService.php b/services/RecipesService.php index 647d1128..37ac81ab 100644 --- a/services/RecipesService.php +++ b/services/RecipesService.php @@ -14,15 +14,15 @@ class RecipesService extends BaseService protected $StockService; - public function GetRecipesFulfillment() + public function GetRecipesPosResolved() { - $sql = 'SELECT * from recipes_fulfillment'; + $sql = 'SELECT * FROM recipes_pos_resolved'; return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ); } - public function GetRecipesSumFulfillment() + public function GetRecipesResolved() { - $sql = 'SELECT * from recipes_fulfillment_sum'; + $sql = 'SELECT * FROM recipes_resolved'; return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ); } @@ -30,7 +30,7 @@ class RecipesService extends BaseService { $recipe = $this->Database->recipes($recipeId); - $recipePositions = $this->GetRecipesFulfillment(); + $recipePositions = $this->GetRecipesPosResolved(); foreach ($recipePositions as $recipePosition) { if($recipePosition->recipe_id == $recipeId && !in_array($recipePosition->product_id, $excludedProductIds)) diff --git a/views/recipeform.blade.php b/views/recipeform.blade.php index ca9dc031..f328b904 100644 --- a/views/recipeform.blade.php +++ b/views/recipeform.blade.php @@ -154,6 +154,7 @@ # {{ $L('Recipe') }} + {{ $L('Servings') }} @@ -161,7 +162,7 @@ @foreach($recipeNestings as $recipeNesting) - + @@ -171,6 +172,9 @@ {{ FindObjectInArrayByPropertyValue($recipes, 'id', $recipeNesting->includes_recipe_id)->name }} + + {{ $recipeNesting->servings }} + @endforeach @endif @@ -202,18 +206,20 @@ @@ -95,7 +95,7 @@ - +    @@ -126,7 +126,7 @@ {{ $L('Based on the prices of the last purchase per product') }}

- {{ $totalRecipeCosts }} + {{ $selectedRecipeTotalCosts }}

@@ -142,7 +142,7 @@

@endif - @php $selectedRecipeSubRecipePositionsFiltered = FindAllObjectsInArrayByPropertyValue($selectedRecipeSubRecipesPositions, 'recipe_id', $selectedRecipeSubRecipe->id); @endphp + @php $selectedRecipeSubRecipePositionsFiltered = FindAllObjectsInArrayByPropertyValue($selectedRecipeSubRecipesPositions, 'child_recipe_id', $selectedRecipeSubRecipe->id); @endphp @if(count($selectedRecipeSubRecipePositionsFiltered) > 0)
{{ $L('Ingredients') }}
@@ -154,8 +154,8 @@
{{ $selectedRecipePosition->ingredient_group }}
@endif
  • - @if($selectedRecipePosition->amount == round($selectedRecipePosition->amount)){{ round($selectedRecipePosition->amount) }}@else{{ $selectedRecipePosition->amount }}@endif {{ Pluralize($selectedRecipePosition->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', $selectedRecipePosition->qu_id)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', $selectedRecipePosition->qu_id)->name_plural) }} {{ FindObjectInArrayByPropertyValue($products, 'id', $selectedRecipePosition->product_id)->name }} - @if(FindObjectInArrayByPropertyValue($recipesFulfillment, 'recipe_pos_id', $selectedRecipePosition->id)->need_fulfilled == 1) {{ $L('Enough in stock') }} @else {{ $L('Not enough in stock, #1 missing, #2 already on shopping list', FindObjectInArrayByPropertyValue($recipesFulfillment, 'recipe_pos_id', round($selectedRecipePosition->id)->missing_amount), round(FindObjectInArrayByPropertyValue($recipesFulfillment, 'recipe_pos_id', $selectedRecipePosition->id)->amount_on_shopping_list)) }} @endif + @if($selectedRecipePosition->recipe_amount == round($selectedRecipePosition->recipe_amount)){{ round($selectedRecipePosition->recipe_amount) }}@else{{ $selectedRecipePosition->recipe_amount }}@endif {{ Pluralize($selectedRecipePosition->recipe_amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', $selectedRecipePosition->qu_id)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', $selectedRecipePosition->qu_id)->name_plural) }} {{ FindObjectInArrayByPropertyValue($products, 'id', $selectedRecipePosition->product_id)->name }} + @if(FindObjectInArrayByPropertyValue($selectedRecipeSubRecipesPositions, 'recipe_pos_id', $selectedRecipePosition->id)->need_fulfilled == 1) {{ $L('Enough in stock') }} @else {{ $L('Not enough in stock, #1 missing, #2 already on shopping list', round(FindObjectInArrayByPropertyValue($selectedRecipeSubRecipesPositions, 'recipe_pos_id', $selectedRecipePosition->id)->missing_amount), round(FindObjectInArrayByPropertyValue($selectedRecipeSubRecipesPositions, 'recipe_pos_id', $selectedRecipePosition->id)->amount_on_shopping_list)) }} @endif @if(!empty($selectedRecipePosition->note))
    {!! nl2br($selectedRecipePosition->note) !!}
    @@ -178,19 +178,19 @@

    @endif - @if($selectedRecipePositions->count() > 0) + @if($selectedRecipePositionsResolved->count() > 0)
    {{ $L('Ingredients') }}
      @php $lastGroup = 'undefined'; @endphp - @foreach($selectedRecipePositions as $selectedRecipePosition) + @foreach($selectedRecipePositionsResolved as $selectedRecipePosition) @if($lastGroup != $selectedRecipePosition->ingredient_group)
      {{ $selectedRecipePosition->ingredient_group }}
      @endif
    • - @if($selectedRecipePosition->amount == round($selectedRecipePosition->amount)){{ round($selectedRecipePosition->amount) }}@else{{ $selectedRecipePosition->amount }}@endif {{ Pluralize($selectedRecipePosition->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', $selectedRecipePosition->qu_id)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', $selectedRecipePosition->qu_id)->name_plural) }} {{ FindObjectInArrayByPropertyValue($products, 'id', $selectedRecipePosition->product_id)->name }} - @if(FindObjectInArrayByPropertyValue($recipesFulfillment, 'recipe_pos_id', $selectedRecipePosition->id)->need_fulfilled == 1) {{ $L('Enough in stock') }} @else {{ $L('Not enough in stock, #1 missing, #2 already on shopping list', round(FindObjectInArrayByPropertyValue($recipesFulfillment, 'recipe_pos_id', $selectedRecipePosition->id)->missing_amount), round(FindObjectInArrayByPropertyValue($recipesFulfillment, 'recipe_pos_id', $selectedRecipePosition->id)->amount_on_shopping_list)) }} @endif + @if($selectedRecipePosition->recipe_amount == round($selectedRecipePosition->recipe_amount)){{ round($selectedRecipePosition->recipe_amount) }}@else{{ $selectedRecipePosition->recipe_amount }}@endif {{ Pluralize($selectedRecipePosition->recipe_amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', $selectedRecipePosition->qu_id)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', $selectedRecipePosition->qu_id)->name_plural) }} {{ FindObjectInArrayByPropertyValue($products, 'id', $selectedRecipePosition->product_id)->name }} + @if(FindObjectInArrayByPropertyValue($recipePositionsResolved, 'recipe_pos_id', $selectedRecipePosition->id)->need_fulfilled == 1) {{ $L('Enough in stock') }} @else {{ $L('Not enough in stock, #1 missing, #2 already on shopping list', round(FindObjectInArrayByPropertyValue($recipePositionsResolved, 'recipe_pos_id', $selectedRecipePosition->id)->missing_amount), round(FindObjectInArrayByPropertyValue($recipePositionsResolved, 'recipe_pos_id', $selectedRecipePosition->id)->amount_on_shopping_list)) }} @endif @if(!empty($selectedRecipePosition->note))
      {!! nl2br($selectedRecipePosition->note) !!}
      @@ -212,7 +212,7 @@
  • - @foreach($recipesFulfillment as $recipePos) + @foreach($recipePositionsResolved as $recipePos) @if(in_array($recipePos->recipe_id, $includedRecipeIdsAbsolute) && $recipePos->missing_amount > 0)