From cd60c239af71b6e37aa80498c023fb2bbcf0d1f6 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Thu, 31 Mar 2022 22:52:38 +0200 Subject: [PATCH] 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) --- changelog/67_UNRELEASED_xxxx-xx-xx.md | 4 +- grocy.openapi.json | 4 - migrations/0179.sql | 170 ++++++++++++++++++++++++++ public/viewjs/choresoverview.js | 8 +- services/StockService.php | 7 -- views/recipes.blade.php | 14 ++- views/stockoverview.blade.php | 8 +- 7 files changed, 194 insertions(+), 21 deletions(-) create mode 100644 migrations/0179.sql 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']))