From e84c7063d35014441e9d9d4f936446c0661f8fa6 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Mon, 27 Jan 2020 22:14:11 +0100 Subject: [PATCH] Consume any subproduct when consuming a recipe ingredient which is not in stock (fixes #446) --- changelog/55_UNRELEASED_2020-01-31.md | 4 +++- controllers/StockApiController.php | 8 ++++++- grocy.openapi.json | 12 ++++++++-- helpers/extensions.php | 2 +- services/RecipesService.php | 3 ++- services/StockService.php | 32 +++++++++++++++------------ 6 files changed, 41 insertions(+), 20 deletions(-) diff --git a/changelog/55_UNRELEASED_2020-01-31.md b/changelog/55_UNRELEASED_2020-01-31.md index 91e0f56f..ec8a6254 100644 --- a/changelog/55_UNRELEASED_2020-01-31.md +++ b/changelog/55_UNRELEASED_2020-01-31.md @@ -41,6 +41,7 @@ - Fixed that when `FEATURE_FLAG_SHOPPINGLIST_MULTIPLE_LISTS` was set to `false`, the shopping list appeared empty after some actions ### Recipe improvements +- When consuming a recipe and if an ingredient is not in stock, but that product has any subproduct which is in stock, this gets now consumed (before consuming was not possible in that case) - When adding or editing a recipe ingredient, a dialog is now used instead of switching between pages (thanks @kriddles) ### Meal plan improvements/fixes @@ -60,9 +61,10 @@ ### API improvements/fixes - The endpoint `/stock` now includes also the product object itself (new field/property `product`) (thanks @gsacre) +- The endpoint `/stock/products/{productId}/entries` can now include stock entries of child products (if the given product is a parent product and in addition to the ones of the given product) - new query parameter `include_sub_products` (defaults to `false` so no changed behavior when not supplied) +- New endpoints for the new stock transfer & stock entry edit capabilities - Fixed that the route `/stock/barcodes/external-lookup/{barcode}` did not work, because the `barcode` argument was expected as a route argument but the route was missing it (thanks @Mikhail5555 and @beetle442002) - Fixed the response type description of the `/stock/volatile` endpoint -- New endpoints for the new stock transfer & stock entry edit capabilities ### General & other improvements/fixes - It's now possible to keep the screen on always or when a "fullscreen-card" (e. g. used for recipes) is displayed diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php index 5b0995c7..1f41ecc7 100644 --- a/controllers/StockApiController.php +++ b/controllers/StockApiController.php @@ -567,7 +567,13 @@ class StockApiController extends BaseApiController public function ProductStockEntries(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { - return $this->ApiResponse($this->StockService->GetProductStockEntries($args['productId'])); + $allowSubproductSubstitution = false; + if (isset($request->getQueryParams()['include_sub_products']) && boolval($request->getQueryParams()['include_sub_products'])) + { + $allowSubproductSubstitution = true; + } + + return $this->ApiResponse($this->StockService->GetProductStockEntries($args['productId'], false, $allowSubproductSubstitution)); } public function ProductStockLocations(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) diff --git a/grocy.openapi.json b/grocy.openapi.json index 89f982a0..194b4765 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -707,8 +707,7 @@ "schema": { "type": "integer" } - } - , + }, { "in": "query", "name": "best_fit_width", @@ -1351,6 +1350,15 @@ "schema": { "type": "integer" } + }, + { + "in": "query", + "name": "include_sub_products", + "required": false, + "description": "If sub products should be included (if the given product is a parent product and in addition to the ones of the given product)", + "schema": { + "type": "boolean" + } } ], "responses": { diff --git a/helpers/extensions.php b/helpers/extensions.php index 956d14c7..5a69eef7 100644 --- a/helpers/extensions.php +++ b/helpers/extensions.php @@ -82,7 +82,7 @@ function SumArrayValue($array, $propertyName) $sum = 0; foreach($array as $object) { - $sum += $object->{$propertyName}; + $sum += floatval($object->{$propertyName}); } return $sum; diff --git a/services/RecipesService.php b/services/RecipesService.php index c9c070f2..2cbd3865 100644 --- a/services/RecipesService.php +++ b/services/RecipesService.php @@ -67,12 +67,13 @@ class RecipesService extends BaseService throw new \Exception('Recipe does not exist'); } + $transactionId = uniqid(); $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->recipe_amount, false, StockService::TRANSACTION_TYPE_CONSUME, 'default', $recipeId); + $this->StockService->ConsumeProduct($recipePosition->product_id, $recipePosition->recipe_amount, false, StockService::TRANSACTION_TYPE_CONSUME, 'default', $recipeId, null, $transactionId, true); } } diff --git a/services/StockService.php b/services/StockService.php index 17e64ffe..563198be 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -184,24 +184,29 @@ class StockService extends BaseService return $this->Database->stock()->where('id', $entryId)->fetch(); } - public function GetProductStockEntries($productId, $excludeOpened = false) + public function GetProductStockEntries($productId, $excludeOpened = false, $allowSubproductSubstitution = false) { // In order of next use: // First expiring first, then first in first out + $sqlWhereProductId = 'product_id = :1'; + if ($allowSubproductSubstitution) + { + $sqlWhereProductId = '(product_id IN (SELECT sub_product_id FROM products_resolved WHERE parent_product_id = :1) OR product_id = :1)'; + } + + $sqlOpenAndWhere = 'AND open IN (0, 1)'; if ($excludeOpened) { - return $this->Database->stock()->where('product_id = :1 AND open = 0', $productId)->orderBy('best_before_date', 'ASC')->orderBy('purchased_date', 'ASC')->fetchAll(); - } - else - { - return $this->Database->stock()->where('product_id', $productId)->orderBy('best_before_date', 'ASC')->orderBy('purchased_date', 'ASC')->fetchAll(); + $sqlOpenAndWhere = 'AND open = 0'; } + + return $this->Database->stock()->where($sqlWhereProductId . ' ' . $sqlOpenAndWhere, $productId)->orderBy('best_before_date', 'ASC')->orderBy('purchased_date', 'ASC')->fetchAll(); } - public function GetProductStockEntriesForLocation($productId, $locationId, $excludeOpened = false) + public function GetProductStockEntriesForLocation($productId, $locationId, $excludeOpened = false, $allowSubproductSubstitution = false) { - $stockEntries = $this->GetProductStockEntries($productId, $excludeOpened); + $stockEntries = $this->GetProductStockEntries($productId, $excludeOpened, $allowSubproductSubstitution); return FindAllObjectsInArrayByPropertyValue($stockEntries, 'location_id', $locationId); } @@ -286,14 +291,14 @@ class StockService extends BaseService } } - public function ConsumeProduct(int $productId, float $amount, bool $spoiled, $transactionType, $specificStockEntryId = 'default', $recipeId = null, $locationId = null, &$transactionId = null) + public function ConsumeProduct(int $productId, float $amount, bool $spoiled, $transactionType, $specificStockEntryId = 'default', $recipeId = null, $locationId = null, &$transactionId = null, $allowSubproductSubstitution = false) { if (!$this->ProductExists($productId)) { throw new \Exception('Product does not exist'); } - if ($locationId !== null & !$this->LocationExists($locationId)) + if ($locationId !== null && !$this->LocationExists($locationId)) { throw new \Exception('Location does not exist'); } @@ -316,15 +321,14 @@ class StockService extends BaseService { if ($locationId === null) // Consume from any location { - $productStockAmount = $this->Database->stock()->where('product_id', $productId)->sum('amount'); - $potentialStockEntries = $this->GetProductStockEntries($productId); + $potentialStockEntries = $this->GetProductStockEntries($productId, false, $allowSubproductSubstitution); } else // Consume only from the supplied location { - $productStockAmount = $this->Database->stock()->where('product_id = :1 AND location_id = :2', $productId, $locationId)->sum('amount'); - $potentialStockEntries = $this->GetProductStockEntriesForLocation($productId, $locationId); + $potentialStockEntries = $this->GetProductStockEntriesForLocation($productId, $locationId, false, $allowSubproductSubstitution); } + $productStockAmount = SumArrayValue($potentialStockEntries, 'amount'); if ($amount > $productStockAmount) { throw new \Exception('Amount to be consumed cannot be > current stock amount (if supplied, at the desired location)');