Fixed default consume rule ORDER BY handling related to stock_next_use (fixes #1979)

This commit is contained in:
Bernd Bestel
2022-08-26 11:15:15 +02:00
parent d883474f03
commit c0d0b8fc90
5 changed files with 128 additions and 12 deletions

View File

@@ -26,6 +26,7 @@
### Recipes ### Recipes
- Fixed that headlines in the recipe description (preparation text) were removed on saving - Fixed that headlines in the recipe description (preparation text) were removed on saving
- Fixed that the default consume rule was not always applied correctly when a recipe consumed a substituted ingredient (so when having a parent product in the recipe which is currently not in stock itself)
### Meal plan ### Meal plan

View File

@@ -648,7 +648,7 @@ class StockApiController extends BaseApiController
$allowSubproductSubstitution = true; $allowSubproductSubstitution = true;
} }
return $this->FilteredApiResponse($response, $this->getStockService()->GetProductStockEntries($args['productId'], false, $allowSubproductSubstitution, true), $request->getQueryParams()); return $this->FilteredApiResponse($response, $this->getStockService()->GetProductStockEntries($args['productId'], false, $allowSubproductSubstitution), $request->getQueryParams());
} }
public function LocationStockEntries(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function LocationStockEntries(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)

122
migrations/0200.sql Normal file
View File

@@ -0,0 +1,122 @@
DROP VIEW stock_next_use;
CREATE VIEW stock_next_use
AS
/*
The default consume rule is:
Opened first, then first due first, then first in first out
Apart from that products at their default consume location should be consumed first
This orders the stock entries by that
=> Highest "priority" per product = the stock entry to use next
=> ORDER BY clause = ORDER BY priority DESC, open DESC, best_before_date ASC, purchased_date ASC
*/
SELECT
(ROW_NUMBER() OVER(PARTITION BY s.product_id ORDER BY CASE WHEN IFNULL(p.default_consume_location_id, -1) = s.location_id THEN 0 ELSE 1 END ASC, s.open DESC, s.best_before_date ASC, s.purchased_date ASC)) * -1 AS priority,
s.*
FROM stock s
JOIN products p
ON p.id = s.product_id
ORDER BY CASE WHEN IFNULL(p.default_consume_location_id, -1) = s.location_id THEN 0 ELSE 1 END ASC, s.open DESC, s.best_before_date ASC, s.purchased_date ASC;
CREATE TRIGGER stock_next_use_INS INSTEAD OF INSERT ON stock_next_use
BEGIN
INSERT INTO stock
(product_id, amount, best_before_date, purchased_date, stock_id,
price, open, opened_date, location_id, shopping_location_id, note)
VALUES
(NEW.product_id, NEW.amount, NEW.best_before_date, NEW.purchased_date, NEW.stock_id,
NEW.price, NEW.open, NEW.opened_date, NEW.location_id, NEW.shopping_location_id, NEW.note);
END;
CREATE TRIGGER stock_next_use_UPD INSTEAD OF UPDATE ON stock_next_use
BEGIN
UPDATE stock
SET product_id = NEW.product_id,
amount = NEW.amount,
best_before_date = NEW.best_before_date,
purchased_date = NEW.purchased_date,
stock_id = NEW.stock_id,
price = NEW.price,
open = NEW.open,
opened_date = NEW.opened_date,
location_id = NEW.location_id,
shopping_location_id = NEW.shopping_location_id,
note = NEW.note
WHERE id = NEW.id;
END;
CREATE TRIGGER stock_next_use_DEL INSTEAD OF DELETE ON stock_next_use
BEGIN
DELETE FROM stock
WHERE id = OLD.id;
END;
DROP VIEW products_current_substitutions;
CREATE VIEW products_current_substitutions
AS
/*
When a parent product is not in-stock itself,
any sub product (the next based on the default consume rule) should be used
This view lists all parent products and in the column "product_id_effective" either itself,
when the corresponding parent product is currently in-stock itself, or otherwise the next sub product to use
*/
SELECT
-1, -- Dummy
p_sub.id AS parent_product_id,
CASE WHEN p_sub.has_sub_products = 1 THEN
CASE WHEN IFNULL(sc.amount, 0) = 0 THEN -- Parent product itself is currently not in stock => use the next sub product
(
SELECT x_snu.product_id
FROM products_resolved x_pr
JOIN stock_next_use x_snu
ON x_pr.sub_product_id = x_snu.product_id
WHERE x_pr.parent_product_id = p_sub.id
AND x_pr.parent_product_id != x_pr.sub_product_id
ORDER BY x_snu.priority DESC, x_snu.open DESC, x_snu.best_before_date ASC, x_snu.purchased_date ASC
LIMIT 1
)
ELSE -- Parent product itself is currently in stock => use it
p_sub.id
END
END AS product_id_effective
FROM products_view p
JOIN products_resolved pr
ON p.id = pr.parent_product_id
JOIN products_view p_sub
ON pr.sub_product_id = p_sub.id
JOIN stock_current sc
ON p_sub.id = sc.product_id
WHERE p_sub.has_sub_products = 1;
DROP VIEW products_current_price;
CREATE VIEW products_current_price
AS
/*
Current price per product,
based on the stock entry to use next,
or on the last price if the product is currently not in stock
*/
SELECT
-1 AS id, -- Dummy,
p.id AS product_id,
IFNULL(snu.price, plp.price) AS price
FROM products p
LEFT JOIN (
SELECT
product_id,
MAX(priority),
price -- Bare column, ref https://www.sqlite.org/lang_select.html#bare_columns_in_an_aggregate_query
FROM stock_next_use
GROUP BY product_id
ORDER BY priority DESC, open DESC, best_before_date ASC, purchased_date ASC
) snu
ON p.id = snu.product_id
LEFT JOIN products_last_purchased plp
ON p.id = plp.product_id;

View File

@@ -87,7 +87,7 @@ class RecipesService extends BaseService
{ {
if ($recipePosition->only_check_single_unit_in_stock == 0) if ($recipePosition->only_check_single_unit_in_stock == 0)
{ {
$this->getStockService()->ConsumeProduct($recipePosition->product_id, $recipePosition->recipe_amount, false, StockService::TRANSACTION_TYPE_CONSUME, 'default', $recipeId, null, $transactionId, true, true); $this->getStockService()->ConsumeProduct($recipePosition->product_id_effective, $recipePosition->recipe_amount, false, StockService::TRANSACTION_TYPE_CONSUME, 'default', $recipeId, null, $transactionId, true, true);
} }
} }

View File

@@ -821,7 +821,7 @@ class StockService extends BaseService
return $returnData; return $returnData;
} }
public function GetProductStockEntries($productId, $excludeOpened = false, $allowSubproductSubstitution = false, $ordered = true) public function GetProductStockEntries($productId, $excludeOpened = false, $allowSubproductSubstitution = false)
{ {
$sqlWhereProductId = 'product_id = ' . $productId; $sqlWhereProductId = 'product_id = ' . $productId;
if ($allowSubproductSubstitution) if ($allowSubproductSubstitution)
@@ -835,14 +835,7 @@ class StockService extends BaseService
$sqlWhereAndOpen = 'AND open = 0'; $sqlWhereAndOpen = 'AND open = 0';
} }
$result = $this->getDatabase()->stock_next_use()->where($sqlWhereProductId . ' ' . $sqlWhereAndOpen); return $this->getDatabase()->stock_next_use()->where($sqlWhereProductId . ' ' . $sqlWhereAndOpen);
if ($ordered)
{
return $result->orderBy('product_id', 'ASC')->orderBy('priority', 'DESC');
}
return $result;
} }
public function GetLocationStockEntries($locationId) public function GetLocationStockEntries($locationId)
@@ -857,7 +850,7 @@ class StockService extends BaseService
public function GetProductStockEntriesForLocation($productId, $locationId, $excludeOpened = false, $allowSubproductSubstitution = false) public function GetProductStockEntriesForLocation($productId, $locationId, $excludeOpened = false, $allowSubproductSubstitution = false)
{ {
$stockEntries = $this->GetProductStockEntries($productId, $excludeOpened, $allowSubproductSubstitution, true); $stockEntries = $this->GetProductStockEntries($productId, $excludeOpened, $allowSubproductSubstitution);
return FindAllObjectsInArrayByPropertyValue($stockEntries, 'location_id', $locationId); return FindAllObjectsInArrayByPropertyValue($stockEntries, 'location_id', $locationId);
} }