mirror of
https://github.com/grocy/grocy.git
synced 2025-04-29 01:32:38 +00:00
Squashed commit
Improved locale number display on stockoverview page Fixed choresoverview chore execution color highlighting Highlight recipe ingredients based on the new due score (references #1813) Reworked current price handling views (mostly needed for recipes)
This commit is contained in:
parent
fbb84277bf
commit
cd60c239af
@ -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)
|
||||
|
@ -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": {
|
||||
|
170
migrations/0179.sql
Normal file
170
migrations/0179.sql
Normal file
@ -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;
|
@ -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");
|
||||
|
@ -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,
|
||||
|
@ -478,12 +478,14 @@
|
||||
$selectedRecipePosition->recipe_amount = $selectedRecipePosition->recipe_amount * $productQuConversion->factor;
|
||||
}
|
||||
@endphp
|
||||
@if(!empty($selectedRecipePosition->recipe_variable_amount))
|
||||
{{ $selectedRecipePosition->recipe_variable_amount }}
|
||||
@else
|
||||
<span class="locale-number locale-number-quantity-amount">@if($selectedRecipePosition->recipe_amount == round($selectedRecipePosition->recipe_amount, 2)){{ round($selectedRecipePosition->recipe_amount, 2) }}@else{{ $selectedRecipePosition->recipe_amount }}@endif</span>
|
||||
@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 }}
|
||||
<span class="@if($selectedRecipePosition->due_score == 20) text-danger @elseif($selectedRecipePosition->due_score == 10) text-secondary @elseif($selectedRecipePosition->due_score == 1) text-warning @endif">
|
||||
@if(!empty($selectedRecipePosition->recipe_variable_amount))
|
||||
{{ $selectedRecipePosition->recipe_variable_amount }}
|
||||
@else
|
||||
<span class="locale-number locale-number-quantity-amount">@if($selectedRecipePosition->recipe_amount == round($selectedRecipePosition->recipe_amount, 2)){{ round($selectedRecipePosition->recipe_amount, 2) }}@else{{ $selectedRecipePosition->recipe_amount }}@endif</span>
|
||||
@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 }}
|
||||
</span>
|
||||
@if(GROCY_FEATURE_FLAG_STOCK)
|
||||
<span class="d-print-none">
|
||||
@if($selectedRecipePosition->need_fulfilled == 1)<i class="fas fa-check text-success"></i>@elseif($selectedRecipePosition->need_fulfilled_with_shopping_list == 1)<i class="fas fa-exclamation text-warning"></i>@else<i class="fas fa-times text-danger"></i>@endif
|
||||
|
@ -330,8 +330,12 @@
|
||||
<span class="pl-1 text-secondary">
|
||||
<i class="fas fa-custom-sigma-sign"></i> <span id="product-{{ $currentStockEntry->product_id }}-amount-aggregated"
|
||||
class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->amount_aggregated }}</span> {{ $__n($currentStockEntry->amount_aggregated, $currentStockEntry->qu_unit_name, $currentStockEntry->qu_unit_name_plural, true) }}
|
||||
@if($currentStockEntry->amount_opened_aggregated > 0)<span id="product-{{ $currentStockEntry->product_id }}-opened-amount-aggregated"
|
||||
class="small font-italic">{{ $__t('%s opened', $currentStockEntry->amount_opened_aggregated) }}</span>@endif
|
||||
@if($currentStockEntry->amount_opened_aggregated > 0)
|
||||
<span id="product-{{ $currentStockEntry->product_id }}-opened-amount-aggregated"
|
||||
class="small font-italic">
|
||||
{!! $__t('%s opened', '<span class="locale-number locale-number-quantity-amount">' . $currentStockEntry->amount_opened_aggregated . '</span>') !!}
|
||||
</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
@if(boolval($userSettings['show_icon_on_stock_overview_page_when_product_is_on_shopping_list']))
|
||||
|
Loading…
x
Reference in New Issue
Block a user