diff --git a/changelog/67_UNRELEASED_xxxx-xx-xx.md b/changelog/67_UNRELEASED_xxxx-xx-xx.md
index b574d6f8..8cf27d6b 100644
--- a/changelog/67_UNRELEASED_xxxx-xx-xx.md
+++ b/changelog/67_UNRELEASED_xxxx-xx-xx.md
@@ -5,7 +5,7 @@
### New feature: Notes and Userfields for stock entries
- Stock entries can now have notes
- - For example to distinguish between same, yet different products (e.g. having only a generic product "Chocolate" and note in that field what special one it is exactly this time)
+ - For example to distinguish between same, yet different products (e.g. having only a generic product "Chocolate" and note in that field what special one it is exactly this time - an alternative to have sub products)
- => New field on the purchase and inventory page
- => New column on the stock entries and stock journal page
- => Visible also in the "Use a specific stock item" dropdown on the consume and transfer page
@@ -22,6 +22,7 @@
- 10 points per overdue ingredient
- 20 points per expired ingredient
- (or else 0)
+- The corresponding ingredient is also highlighted in red/yellow/grey (same colors as on the stock overview page)
### Stock
@@ -89,3 +90,4 @@
- Added a new endpoint `GET /stock/locations/{locationId}/entries` to get all stock entries of a given location (similar to the already existing endpoint `GET /stock/products/{productId}/entries`)
- Endpoint `/recipes/{recipeId}/consume`: Fixed that consuming partially fulfilled recipes was possible, although an error was already returned in that case (and potentially some of the in-stock ingredients were consumed in fact)
+- Endpoint `/stock/products/{productId}`: The property/field `oldest_price` has been removed (as this had no real sense)
diff --git a/grocy.openapi.json b/grocy.openapi.json
index 243488f9..bc5dd46f 100644
--- a/grocy.openapi.json
+++ b/grocy.openapi.json
@@ -4756,9 +4756,6 @@
"avg_price": {
"type": "number"
},
- "oldest_price": {
- "type": "number"
- },
"last_shopping_location_id": {
"type": "integer"
},
@@ -4829,7 +4826,6 @@
},
"last_price": null,
"avg_price": null,
- "oldest_price": null,
"last_shopping_location_id": null,
"next_due_date": "2019-07-07",
"location": {
diff --git a/migrations/0179.sql b/migrations/0179.sql
new file mode 100644
index 00000000..07efeaa1
--- /dev/null
+++ b/migrations/0179.sql
@@ -0,0 +1,170 @@
+CREATE VIEW stock_next_use
+AS
+
+/*
+ The default consume rule is:
+ Opened first, then first due first, then first in first out
+
+ This orders the stock entries by that
+ => Lowest "priority" per product = the stock entry to use next
+*/
+
+SELECT
+ -1, -- Dummy
+ ROW_NUMBER() OVER(PARTITION BY product_id ORDER BY open DESC, best_before_date ASC, purchased_date ASC) AS priority,
+ product_id,
+ stock_id,
+ price
+FROM stock;
+
+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, -- Dummy,
+ p.id AS product_id,
+ IFNULL(snu.price, plp.price) AS price
+FROM products p
+LEFT JOIN (
+ SELECT
+ product_id,
+ MIN(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
+ ) snu
+ ON p.id = snu.product_id
+LEFT JOIN products_last_purchased plp
+ ON p.id = plp.product_id;
+
+DROP VIEW products_oldest_stock_unit_price;
+
+DROP VIEW recipes_pos_resolved;
+CREATE VIEW recipes_pos_resolved
+AS
+
+-- Multiplication by 1.0 to force conversion to float (REAL)
+
+SELECT
+ r.id AS recipe_id,
+ rp.id AS recipe_pos_id,
+ rp.product_id AS product_id,
+ CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END AS recipe_amount,
+ IFNULL(sc.amount_aggregated, 0) AS stock_amount,
+ CASE WHEN IFNULL(sc.amount_aggregated, 0) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 0.00000001 ELSE CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END END THEN 1 ELSE 0 END AS need_fulfilled,
+ CASE WHEN IFNULL(sc.amount_aggregated, 0) - CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 0.00000001 ELSE CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END END < 0 THEN ABS(IFNULL(sc.amount_aggregated, 0) - (CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END)) ELSE 0 END AS missing_amount,
+ IFNULL(sl.amount, 0) * p.qu_factor_purchase_to_stock AS amount_on_shopping_list,
+ CASE WHEN ROUND(IFNULL(sc.amount_aggregated, 0) + (CASE WHEN r.not_check_shoppinglist = 1 THEN 0 ELSE IFNULL(sl.amount, 0) END * p.qu_factor_purchase_to_stock), 2) >= ROUND(CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 0.00000001 ELSE CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END END, 2) THEN 1 ELSE 0 END AS need_fulfilled_with_shopping_list,
+ rp.qu_id,
+ (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) * rp.amount * IFNULL(pcp.price, 0) * rp.price_factor * CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN IFNULL(qucr.factor, 1) ELSE 1 END AS costs,
+ CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos,
+ rp.ingredient_group,
+ pg.name as product_group,
+ rp.id, -- Just a dummy id column
+ r.type as recipe_type,
+ rnr.includes_recipe_id as child_recipe_id,
+ rp.note,
+ rp.variable_amount AS recipe_variable_amount,
+ rp.only_check_single_unit_in_stock,
+ rp.amount / r.base_servings*1.0 * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) * IFNULL(p.calories, 0) * CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN IFNULL(qucr.factor, 1) ELSE 1 END AS calories,
+ p.active AS product_active,
+ CASE pvs.current_due_status
+ WHEN 'ok' THEN 0
+ WHEN 'due_soon' THEN 1
+ WHEN 'overdue' THEN 10
+ WHEN 'expired' THEN 20
+ END AS due_score
+FROM recipes r
+JOIN recipes_nestings_resolved rnr
+ ON r.id = rnr.recipe_id
+JOIN recipes rnrr
+ ON rnr.includes_recipe_id = rnrr.id
+JOIN recipes_pos rp
+ ON rnr.includes_recipe_id = rp.recipe_id
+JOIN products p
+ ON rp.product_id = p.id
+JOIN products_volatile_status pvs
+ ON rp.product_id = pvs.product_id
+LEFT JOIN product_groups pg
+ ON p.product_group_id = pg.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
+LEFT JOIN quantity_unit_conversions_resolved qucr
+ ON rp.product_id = qucr.product_id
+ AND rp.qu_id = qucr.from_qu_id
+ AND p.qu_id_stock = qucr.to_qu_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,
+ CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END AS recipe_amount,
+ IFNULL(sc.amount_aggregated, 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,
+ (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) * rp.amount * IFNULL(pcp.price, 0) * rp.price_factor * CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN IFNULL(qucr.factor, 1) ELSE 1 END AS costs,
+ CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos,
+ rp.ingredient_group,
+ pg.name as product_group,
+ rp.id, -- Just a dummy id column
+ r.type as recipe_type,
+ rnr.includes_recipe_id as child_recipe_id,
+ rp.note,
+ rp.variable_amount AS recipe_variable_amount,
+ rp.only_check_single_unit_in_stock,
+ rp.amount / r.base_servings*1.0 * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) * IFNULL(p.calories, 0) * CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN IFNULL(qucr.factor, 1) ELSE 1 END AS calories,
+ p.active AS product_active,
+ CASE pvs.current_due_status
+ WHEN 'ok' THEN 0
+ WHEN 'due_soon' THEN 1
+ WHEN 'overdue' THEN 10
+ WHEN 'expired' THEN 20
+ END AS due_score
+FROM recipes r
+JOIN recipes_nestings_resolved rnr
+ ON r.id = rnr.recipe_id
+JOIN recipes rnrr
+ ON rnr.includes_recipe_id = rnrr.id
+JOIN recipes_pos rp
+ ON rnr.includes_recipe_id = rp.recipe_id
+JOIN products p
+ ON rp.product_id = p.id
+JOIN products_volatile_status pvs
+ ON rp.product_id = pvs.product_id
+LEFT JOIN product_groups pg
+ ON p.product_group_id = pg.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
+LEFT JOIN quantity_unit_conversions_resolved qucr
+ ON rp.product_id = qucr.product_id
+ AND rp.qu_id = qucr.from_qu_id
+ AND p.qu_id_stock = qucr.to_qu_id
+WHERE rp.not_check_stock_fulfillment = 1;
diff --git a/public/viewjs/choresoverview.js b/public/viewjs/choresoverview.js
index 010d041f..fe7be3d0 100644
--- a/public/viewjs/choresoverview.js
+++ b/public/viewjs/choresoverview.js
@@ -123,17 +123,24 @@ $(document).on('click', '.track-chore-button', function(e)
{
var choreRow = $('#chore-' + choreId + '-row');
var nextXDaysThreshold = moment().add($("#info-due-soon-chores").data("next-x-days"), "days");
+ var todayThreshold = moment().endOf("day");
var now = moment();
var nextExecutionTime = moment(result.next_estimated_execution_time);
choreRow.removeClass("table-warning");
choreRow.removeClass("table-danger");
+ choreRow.removeClass("table-info");
$('#chore-' + choreId + '-due-filter-column').html("");
if (nextExecutionTime.isBefore(now))
{
choreRow.addClass("table-danger");
$('#chore-' + choreId + '-due-filter-column').html("overdue");
}
+ else if (nextExecutionTime.isSameOrBefore(todayThreshold))
+ {
+ choreRow.addClass("table-info");
+ $('#chore-' + choreId + '-due-filter-column').html("duetoday");
+ }
else if (nextExecutionTime.isBefore(nextXDaysThreshold))
{
choreRow.addClass("table-warning");
@@ -332,7 +339,6 @@ $("#reschedule-chore-clear-button").on("click", function(e)
);
});
-
if (GetUriParam("user") !== undefined)
{
$("#user-filter").val("xx" + GetUriParam("user") + "xx");
diff --git a/services/StockService.php b/services/StockService.php
index a987d26f..568f0f5b 100644
--- a/services/StockService.php
+++ b/services/StockService.php
@@ -696,7 +696,6 @@ class StockService extends BaseService
$lastPrice = null;
$lastShoppingLocation = null;
$avgPrice = null;
- $oldestPrice = null;
if ($productLastPurchased)
{
$lastPurchasedDate = $productLastPurchased->purchased_date;
@@ -707,11 +706,6 @@ class StockService extends BaseService
{
$avgPrice = $avgPriceRow->price;
}
- $oldestPriceRow = $this->getDatabase()->products_oldest_stock_unit_price()->where('product_id', $productId)->fetch();
- if ($oldestPriceRow)
- {
- $oldestPrice = $avgPriceRow->price;
- }
}
$product = $this->getDatabase()->products($productId);
@@ -746,7 +740,6 @@ class StockService extends BaseService
'quantity_unit_stock' => $quStock,
'last_price' => $lastPrice,
'avg_price' => $avgPrice,
- 'oldest_price' => $oldestPrice,
'last_shopping_location_id' => $lastShoppingLocation,
'default_shopping_location_id' => $product->shopping_location_id,
'next_due_date' => $nextDueDate,
diff --git a/views/recipes.blade.php b/views/recipes.blade.php
index 489587a4..edd9453f 100644
--- a/views/recipes.blade.php
+++ b/views/recipes.blade.php
@@ -478,12 +478,14 @@
$selectedRecipePosition->recipe_amount = $selectedRecipePosition->recipe_amount * $productQuConversion->factor;
}
@endphp
- @if(!empty($selectedRecipePosition->recipe_variable_amount))
- {{ $selectedRecipePosition->recipe_variable_amount }}
- @else
- @if($selectedRecipePosition->recipe_amount == round($selectedRecipePosition->recipe_amount, 2)){{ round($selectedRecipePosition->recipe_amount, 2) }}@else{{ $selectedRecipePosition->recipe_amount }}@endif
- @endif
- {{ $__n($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(!empty($selectedRecipePosition->recipe_variable_amount))
+ {{ $selectedRecipePosition->recipe_variable_amount }}
+ @else
+ @if($selectedRecipePosition->recipe_amount == round($selectedRecipePosition->recipe_amount, 2)){{ round($selectedRecipePosition->recipe_amount, 2) }}@else{{ $selectedRecipePosition->recipe_amount }}@endif
+ @endif
+ {{ $__n($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(GROCY_FEATURE_FLAG_STOCK)
@if($selectedRecipePosition->need_fulfilled == 1)@elseif($selectedRecipePosition->need_fulfilled_with_shopping_list == 1)@else@endif
diff --git a/views/stockoverview.blade.php b/views/stockoverview.blade.php
index ccda18bb..cab5d133 100755
--- a/views/stockoverview.blade.php
+++ b/views/stockoverview.blade.php
@@ -330,8 +330,12 @@
{{ $currentStockEntry->amount_aggregated }} {{ $__n($currentStockEntry->amount_aggregated, $currentStockEntry->qu_unit_name, $currentStockEntry->qu_unit_name_plural, true) }}
- @if($currentStockEntry->amount_opened_aggregated > 0){{ $__t('%s opened', $currentStockEntry->amount_opened_aggregated) }}@endif
+ @if($currentStockEntry->amount_opened_aggregated > 0)
+
+ {!! $__t('%s opened', '' . $currentStockEntry->amount_opened_aggregated . '') !!}
+
+ @endif
@endif
@if(boolval($userSettings['show_icon_on_stock_overview_page_when_product_is_on_shopping_list']))