Consume any subproduct when consuming a recipe ingredient which is not in stock (fixes #446)

This commit is contained in:
Bernd Bestel 2020-01-27 22:14:11 +01:00
parent dceed6759a
commit e84c7063d3
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
6 changed files with 41 additions and 20 deletions

View File

@ -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

View File

@ -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)

View File

@ -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": {

View File

@ -82,7 +82,7 @@ function SumArrayValue($array, $propertyName)
$sum = 0;
foreach($array as $object)
{
$sum += $object->{$propertyName};
$sum += floatval($object->{$propertyName});
}
return $sum;

View File

@ -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);
}
}

View File

@ -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)');