diff --git a/changelog/77_UNRELEASED_xxxx-xx-xx.md b/changelog/77_UNRELEASED_xxxx-xx-xx.md
index 2f8afee9..cdd17003 100644
--- a/changelog/77_UNRELEASED_xxxx-xx-xx.md
+++ b/changelog/77_UNRELEASED_xxxx-xx-xx.md
@@ -19,7 +19,9 @@
- => Quick video demo (using a Barcode Laser Scanner): https://www.youtube.com/watch?v=-moXPA-VvGc
- => Quick video demo (using Browser Camera Barcode Scanning): https://www.youtube.com/watch?v=veezFX4X1JU
- Optimized that when moving a product to a freezer location (so when freezing it) the due date will no longer be replaced when the product option "Default due days after freezing" is set to `0`
+- Added a new column "Product picture" on the products list (master data) page (hidden by default)
- Fixed that a once set quantity unit on a product barcode could not be removed on edit
+- Fixed that when consuming a specific stock entry which is opened, and which originated from a before partly opened stock entry, the unopened one was wrongly consume instead
### Shopping list
@@ -27,7 +29,7 @@
### Recipes
-- xxx
+- Fixed that calories/costs of recipe ingredients were wrong when the ingredient option "Only check if any amount is in stock" was set and the on the ingredient used quantity unit was different from the product's QU stock
### Meal plan
diff --git a/migrations/0031.php b/migrations/0031.php
index 27a562a8..9a6f81d0 100644
--- a/migrations/0031.php
+++ b/migrations/0031.php
@@ -2,10 +2,7 @@
// This is executed inside DatabaseMigrationService class/context
-use Grocy\Services\LocalizationService;
-
$localizationService = $this->getLocalizationService();
-
$db = $this->getDatabaseService()->GetDbConnection();
if ($db->quantity_units()->count() === 0)
diff --git a/migrations/0063.php b/migrations/0063.php
index 06c6b0fd..5fdae6cc 100644
--- a/migrations/0063.php
+++ b/migrations/0063.php
@@ -2,10 +2,7 @@
// This is executed inside DatabaseMigrationService class/context
-use Grocy\Services\LocalizationService;
-
$localizationService = $this->getLocalizationService();
-
$db = $this->getDatabaseService()->GetDbConnection();
$defaultShoppingList = $db->shopping_lists()->where('id = 1')->fetch();
diff --git a/migrations/0144.php b/migrations/0144.php
index 36a0ebb7..844208c1 100644
--- a/migrations/0144.php
+++ b/migrations/0144.php
@@ -2,6 +2,4 @@
// This is executed inside DatabaseMigrationService class/context
-use Grocy\Services\StockService;
-
$this->getStockService()->CompactStockEntries();
diff --git a/migrations/0241.sql b/migrations/0241.sql
new file mode 100644
index 00000000..2470f535
--- /dev/null
+++ b/migrations/0241.sql
@@ -0,0 +1,135 @@
+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) 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, 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) * CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN IFNULL(qucr.factor, 1.0) ELSE 1 END * (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.product_id != p_effective.id THEN IFNULL(qucr.factor, 1.0) ELSE 1.0 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 * CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN IFNULL(qucr.factor, 1.0) ELSE 1 END / 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_effective.calories, 0) * CASE WHEN rp.product_id != p_effective.id THEN IFNULL(qucr.factor, 1.0) ELSE 1.0 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,
+ IFNULL(pcs.product_id_effective, rp.product_id) AS product_id_effective,
+ p.name AS product_name
+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_substitutions pcs
+ ON rp.product_id = pcs.parent_product_id
+LEFT JOIN products_current_price pcp
+ ON IFNULL(pcs.product_id_effective, rp.product_id) = pcp.product_id
+LEFT JOIN products p_effective
+ ON IFNULL(pcs.product_id_effective, rp.product_id) = p_effective.id
+LEFT JOIN cache__quantity_unit_conversions_resolved qucr
+ ON IFNULL(pcs.product_id_effective, rp.product_id) = qucr.product_id
+ AND CASE WHEN rp.product_id != p_effective.id THEN p.qu_id_stock ELSE rp.qu_id END = qucr.from_qu_id
+ AND IFNULL(p_effective.qu_id_stock, 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) 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) * CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN IFNULL(qucr.factor, 1.0) ELSE 1 END * (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.product_id != p_effective.id THEN IFNULL(qucr.factor, 1.0) ELSE 1.0 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 * CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN IFNULL(qucr.factor, 1.0) ELSE 1 END / 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_effective.calories, 0) * CASE WHEN rp.product_id != p_effective.id THEN IFNULL(qucr.factor, 1.0) ELSE 1.0 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,
+ IFNULL(pcs.product_id_effective, rp.product_id) AS product_id_effective,
+ p.name AS product_name
+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_substitutions pcs
+ ON rp.product_id = pcs.parent_product_id
+LEFT JOIN products_current_price pcp
+ ON IFNULL(pcs.product_id_effective, rp.product_id) = pcp.product_id
+LEFT JOIN products p_effective
+ ON IFNULL(pcs.product_id_effective, rp.product_id) = p_effective.id
+LEFT JOIN cache__quantity_unit_conversions_resolved qucr
+ ON IFNULL(pcs.product_id_effective, rp.product_id) = qucr.product_id
+ AND CASE WHEN rp.product_id != p_effective.id THEN p.qu_id_stock ELSE rp.qu_id END = qucr.from_qu_id
+ AND IFNULL(p_effective.qu_id_stock, p.qu_id_stock) = qucr.to_qu_id
+WHERE rp.not_check_stock_fulfillment = 1;
diff --git a/migrations/0242.php b/migrations/0242.php
new file mode 100644
index 00000000..1714fa42
--- /dev/null
+++ b/migrations/0242.php
@@ -0,0 +1,22 @@
+getDatabaseService();
+
+$sql = 'SELECT s1.id
+FROM stock s1
+WHERE IFNULL(s1.open, 0) = 1
+ AND EXISTS (
+ SELECT 1
+ FROM stock s2
+ WHERE s2.stock_id = s1.stock_id
+ AND IFNULL(s2.open, 0) = 0
+ )';
+
+$rows = $db->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
+foreach ($rows as $row)
+{
+ $db->ExecuteDbStatement('UPDATE stock SET stock_id = \'' . uniqid() . '\' WHERE id = ' . $row->id);
+}
diff --git a/public/css/grocy.css b/public/css/grocy.css
index 21c1af09..72e48df0 100755
--- a/public/css/grocy.css
+++ b/public/css/grocy.css
@@ -16,14 +16,14 @@ body {
font-size: 0.8em;
}
-a:not(.btn):not(.nav-link):not(.dropdown-item) {
+a:not(.btn):not(.nav-link):not(.dropdown-item):not(.list-group-item) {
color: inherit;
text-decoration: underline;
text-decoration-style: dotted;
text-underline-offset: 0.2rem;
}
-a:not(.btn):not(.nav-link):not(.dropdown-item):hover {
+a:not(.btn):not(.nav-link):not(.dropdown-item):not(.list-group-item):hover {
text-decoration: underline;
}
diff --git a/public/css/grocy_night_mode.css b/public/css/grocy_night_mode.css
index ab65cb46..f7549d38 100644
--- a/public/css/grocy_night_mode.css
+++ b/public/css/grocy_night_mode.css
@@ -26,7 +26,8 @@ body.night-mode,
color: #c1c1c1;
}
-.night-mode .table {
+.night-mode .table,
+.night-mode .list-group-item {
color: #c1c1c1;
}
diff --git a/public/viewjs/consume.js b/public/viewjs/consume.js
index 84c725ce..42ff5a05 100644
--- a/public/viewjs/consume.js
+++ b/public/viewjs/consume.js
@@ -226,7 +226,6 @@ var sumValue = 0;
$("#location_id").on('change', function(e)
{
var locationId = $(e.target).val();
-
$("#specific_stock_entry").find("option").remove().end().append("");
if ($("#use_specific_stock_entry").is(":checked"))
{
@@ -293,21 +292,19 @@ function OnLocationChange(locationId, stockId)
if (stockEntry.location_id == locationId)
{
- if ($("#specific_stock_entry option[value='" + stockEntry.stock_id + "']").length == 0)
+ var noteTxt = "";
+ if (stockEntry.note)
{
- var noteTxt = "";
- if (stockEntry.note)
- {
- noteTxt = " " + stockEntry.note;
- }
-
- $("#specific_stock_entry").append($("