diff --git a/changelog/65_UNRELEASED_xxxx-xx-xx.md b/changelog/65_UNRELEASED_xxxx-xx-xx.md index e86a3bac..2396cfcf 100644 --- a/changelog/65_UNRELEASED_xxxx-xx-xx.md +++ b/changelog/65_UNRELEASED_xxxx-xx-xx.md @@ -10,6 +10,7 @@ - Fixed that when undoing a product opened transaction and when the product has "Default due days after opened", the original due date wasn't restored - Fixed that "Track date only"-chores were shown as overdue on the due day on the chores overview page - Fixed that dropdown filters for tables maybe did not work after reordering columns +- Fixed that "Label per unit" stock entry labels (on purchase) weren't unique per unit ### API - Fixed that backslashes were not allowed in API query filters diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php index e6dad21d..013b1032 100644 --- a/controllers/StockApiController.php +++ b/controllers/StockApiController.php @@ -140,13 +140,14 @@ class StockApiController extends BaseApiController { $transactionType = $requestBody['transactiontype']; } - $runPrinterWebhook = false; - if (array_key_exists('print_stock_label', $requestBody) && intval($requestBody['print_stock_label'])) + + $stockLabelType = 0; + if (array_key_exists('stock_label_type', $requestBody) && is_numeric($requestBody['stock_label_type'])) { - $runPrinterWebhook = intval($requestBody['print_stock_label']); + $stockLabelType = intval($requestBody['stock_label_type']); } - $transactionId = $this->getStockService()->AddProduct($args['productId'], $requestBody['amount'], $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId, $shoppingLocationId, $unusedTransactionId, $runPrinterWebhook); + $transactionId = $this->getStockService()->AddProduct($args['productId'], $requestBody['amount'], $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId, $shoppingLocationId, $unusedTransactionId, $stockLabelType); $args['transactionId'] = $transactionId; return $this->StockTransactions($request, $response, $args); diff --git a/grocy.openapi.json b/grocy.openapi.json index fa92de2c..b170a7ea 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -1917,9 +1917,10 @@ "format": "integer", "description": "If omitted, no store will be affected" }, - "print_stock_label": { - "type": "boolean", - "description": "True when the stock entry label should be printed" + "stock_label_type": { + "type": "number", + "format": "integer", + "description": "`1` = No label, `2` = Single label, `3` = Label per unit" } }, "example": { diff --git a/migrations/0155.sql b/migrations/0155.sql index e03fb0b3..555b2640 100644 --- a/migrations/0155.sql +++ b/migrations/0155.sql @@ -2,6 +2,7 @@ PRAGMA legacy_alter_table = ON; ALTER TABLE products RENAME TO products_old; -- Remove allow_label_per_unit column +-- Rename default_print_stock_label column to default_stock_label_type CREATE TABLE products ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, name TEXT NOT NULL UNIQUE, @@ -28,13 +29,13 @@ CREATE TABLE products ( due_type TINYINT NOT NULL DEFAULT 1 CHECK(due_type IN (1, 2)), quick_consume_amount REAL NOT NULL DEFAULT 1, hide_on_stock_overview TINYINT NOT NULL DEFAULT 0 CHECK(hide_on_stock_overview IN (0, 1)), - default_print_stock_label INTEGER NOT NULL DEFAULT 0, + default_stock_label_type INTEGER NOT NULL DEFAULT 0, should_not_be_frozen TINYINT NOT NULL DEFAULT 0 CHECK(should_not_be_frozen IN (0, 1)), row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) ); INSERT INTO products - (id, name, description, product_group_id, active, location_id, shopping_location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, default_best_before_days, default_best_before_days_after_open, default_best_before_days_after_freezing, default_best_before_days_after_thawing, picture_file_name, enable_tare_weight_handling, tare_weight, not_check_stock_fulfillment_for_recipes, parent_product_id, calories, cumulate_min_stock_amount_of_sub_products, due_type, quick_consume_amount, hide_on_stock_overview, default_print_stock_label, should_not_be_frozen, row_created_timestamp) + (id, name, description, product_group_id, active, location_id, shopping_location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, default_best_before_days, default_best_before_days_after_open, default_best_before_days_after_freezing, default_best_before_days_after_thawing, picture_file_name, enable_tare_weight_handling, tare_weight, not_check_stock_fulfillment_for_recipes, parent_product_id, calories, cumulate_min_stock_amount_of_sub_products, due_type, quick_consume_amount, hide_on_stock_overview, default_stock_label_type, should_not_be_frozen, row_created_timestamp) SELECT id, name, description, product_group_id, active, location_id, shopping_location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, default_best_before_days, default_best_before_days_after_open, default_best_before_days_after_freezing, default_best_before_days_after_thawing, picture_file_name, enable_tare_weight_handling, tare_weight, not_check_stock_fulfillment_for_recipes, parent_product_id, calories, cumulate_min_stock_amount_of_sub_products, due_type, quick_consume_amount, hide_on_stock_overview, default_print_stock_label, should_not_be_frozen, row_created_timestamp FROM products_old; diff --git a/migrations/0156.sql b/migrations/0156.sql new file mode 100644 index 00000000..70473a3f --- /dev/null +++ b/migrations/0156.sql @@ -0,0 +1,21 @@ +DROP VIEW stock_splits; +CREATE VIEW stock_splits +AS + +/* + Helper view which shows splitted stock rows which could be compacted + (a stock_id starting with "x" indicates that this entry shouldn't be compacted) +*/ + +SELECT + product_id, + SUM(amount) AS total_amount, + MIN(stock_id) AS stock_id_to_keep, + MAX(id) AS id_to_keep, + GROUP_CONCAT(id) AS id_group, + GROUP_CONCAT(stock_id) AS stock_id_group, + id -- Dummy +FROM stock +WHERE stock_id NOT LIKE 'x%' +GROUP BY product_id, best_before_date, purchased_date, price, open, opened_date, location_id, shopping_location_id +HAVING COUNT(*) > 1; diff --git a/public/viewjs/consume.js b/public/viewjs/consume.js index fdf114e1..7f499e25 100644 --- a/public/viewjs/consume.js +++ b/public/viewjs/consume.js @@ -211,8 +211,6 @@ var sumValue = 0; $("#location_id").on('change', function(e) { var locationId = $(e.target).val(); - sumValue = 0; - var stockId = null; $("#specific_stock_entry").find("option").remove().end().append(""); if ($("#use_specific_stock_entry").is(":checked")) @@ -222,7 +220,7 @@ $("#location_id").on('change', function(e) if (GetUriParam("embedded") !== undefined) { - stockId = GetUriParam('stockId'); + OnLocationChange(locationId, GetUriParam('stockId')); } else { @@ -232,13 +230,36 @@ $("#location_id").on('change', function(e) var gc = $("#product_id").attr("barcode").split(":"); if (gc.length == 4) { - stockId = gc[3]; + Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries?query[]=stock_id=' + gc[3], + function(stockEntries) + { + OnLocationChange(stockEntries[0].location_id, gc[3]); + }, + function(xhr) + { + console.error(xhr); + } + ); } } + else + { + OnLocationChange(locationId, null); + } } +}); + +function OnLocationChange(locationId, stockId) +{ + sumValue = 0; if (locationId) { + if ($("#location_id").val() != locationId) + { + $("#location_id").val(locationId); + } + Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries?include_sub_products=true', function(stockEntries) { @@ -294,7 +315,7 @@ $("#location_id").on('change', function(e) } ); } -}); +} Grocy.Components.ProductPicker.GetPicker().on('change', function(e) { diff --git a/public/viewjs/purchase.js b/public/viewjs/purchase.js index f4331d52..605b7718 100644 --- a/public/viewjs/purchase.js +++ b/public/viewjs/purchase.js @@ -23,7 +23,7 @@ $('#save-purchase-button').on('click', function(e) { var jsonData = {}; jsonData.amount = jsonForm.amount; - jsonData.print_stock_label = jsonForm.print_stock_label + jsonData.stock_label_type = jsonForm.stock_label_type if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) { @@ -121,22 +121,41 @@ $('#save-purchase-button').on('click', function(e) { if (Grocy.Webhooks.labelprinter !== undefined) { - var post_data = {}; - post_data.product = productDetails.product.name; - post_data.grocycode = 'grcy:p:' + jsonForm.product_id + ":" + result[0].stock_id - if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) + if (jsonForm.stock_label_type == 1) // Single label { - post_data.due_date = __t('DD') + ': ' + result[0].best_before_date - } - - if (jsonForm.print_stock_label > 0) - { - var reps = 1; - if (jsonForm.print_stock_label == 2) + var webhookData = {}; + webhookData.product = productDetails.product.name; + webhookData.grocycode = 'grcy:p:' + jsonForm.product_id + ":" + result[0].stock_id; + if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) { - reps = Math.floor(jsonData.amount); + webhookData.due_date = __t('DD') + ': ' + result[0].best_before_date } - Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, post_data, reps); + + Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, webhookData); + } + else if (jsonForm.stock_label_type == 2) // Label per unit + { + Grocy.Api.Get('stock/transactions/' + result[0].transaction_id, + function(stockEntries) + { + stockEntries.forEach(stockEntry => + { + var webhookData = {}; + webhookData.product = productDetails.product.name; + webhookData.grocycode = 'grcy:p:' + jsonForm.product_id + ":" + stockEntry.stock_id; + if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) + { + webhookData.due_date = __t('DD') + ': ' + result[0].best_before_date + } + + Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, webhookData); + }); + }, + function(xhr) + { + console.error(xhr); + } + ); } } } @@ -187,7 +206,7 @@ $('#save-purchase-button').on('click', function(e) Grocy.Components.ProductCard.Refresh(jsonForm.product_id); if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABEL_PRINTER) { - $("#print_stock_label").val(0); + $("#stock_label_type").val(0); } $('#price-hint').text(""); @@ -294,7 +313,7 @@ if (Grocy.Components.ProductPicker !== undefined) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABEL_PRINTER) { - $("#print_stock_label").val(productDetails.product.default_print_stock_label); + $("#stock_label_type").val(productDetails.product.default_stock_label_type); } $("#display_amount").focus(); @@ -404,20 +423,23 @@ function PrefillBestBeforeDate(product, location) } } -Grocy.Components.LocationPicker.GetPicker().on('change', function(e) +if (Grocy.Components.LocationPicker !== undefined) { - if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRODUCT_FREEZING) + Grocy.Components.LocationPicker.GetPicker().on('change', function(e) { - Grocy.Api.Get('objects/locations/' + Grocy.Components.LocationPicker.GetValue(), - function(location) - { - PrefillBestBeforeDate(CurrentProductDetails.product, location); - }, - function(xhr) - { } - ); - } -}); + if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRODUCT_FREEZING) + { + Grocy.Api.Get('objects/locations/' + Grocy.Components.LocationPicker.GetValue(), + function(location) + { + PrefillBestBeforeDate(CurrentProductDetails.product, location); + }, + function(xhr) + { } + ); + } + }); +} $('#display_amount').val(parseFloat(Grocy.UserSettings.stock_default_purchase_amount)); RefreshLocaleNumberInput(); diff --git a/services/StockService.php b/services/StockService.php index 5b98d68f..77016102 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -114,7 +114,7 @@ class StockService extends BaseService } } - public function AddProduct(int $productId, float $amount, $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId = null, $shoppingLocationId = null, &$transactionId = null, $runWebhook = 0, $addExactAmount = false) + public function AddProduct(int $productId, float $amount, $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId = null, $shoppingLocationId = null, &$transactionId = null, $stockLabelType = 0, $addExactAmount = false) { if (!$this->ProductExists($productId)) { @@ -185,58 +185,102 @@ class StockService extends BaseService $transactionId = uniqid(); } - $stockId = uniqid(); - - $logRow = $this->getDatabase()->stock_log()->createRow([ - 'product_id' => $productId, - 'amount' => $amount, - 'best_before_date' => $bestBeforeDate, - 'purchased_date' => $purchasedDate, - 'stock_id' => $stockId, - 'transaction_type' => $transactionType, - 'price' => $price, - 'location_id' => $locationId, - 'transaction_id' => $transactionId, - 'shopping_location_id' => $shoppingLocationId, - 'user_id' => GROCY_USER_ID - ]); - $logRow->save(); - - $stockRow = $this->getDatabase()->stock()->createRow([ - 'product_id' => $productId, - 'amount' => $amount, - 'best_before_date' => $bestBeforeDate, - 'purchased_date' => $purchasedDate, - 'stock_id' => $stockId, - 'price' => $price, - 'location_id' => $locationId, - 'shopping_location_id' => $shoppingLocationId - ]); - $stockRow->save(); - - if (GROCY_FEATURE_FLAG_LABEL_PRINTER && GROCY_LABEL_PRINTER_RUN_SERVER && $runWebhook) + if ($stockLabelType == 2) { - $reps = 1; - if ($runWebhook == 2) + // Label per unit => single stock entry per unit + + for ($i = 1; $i <= $amount; $i++) { - // 2 == run $amount times - $reps = intval(floor($amount)); + $stockId = uniqid('x'); + $logRow = $this->getDatabase()->stock_log()->createRow([ + 'product_id' => $productId, + 'amount' => 1, + 'best_before_date' => $bestBeforeDate, + 'purchased_date' => $purchasedDate, + 'stock_id' => $stockId, + 'transaction_type' => $transactionType, + 'price' => $price, + 'location_id' => $locationId, + 'transaction_id' => $transactionId, + 'shopping_location_id' => $shoppingLocationId, + 'user_id' => GROCY_USER_ID + ]); + $logRow->save(); + + $stockRow = $this->getDatabase()->stock()->createRow([ + 'product_id' => $productId, + 'amount' => 1, + 'best_before_date' => $bestBeforeDate, + 'purchased_date' => $purchasedDate, + 'stock_id' => $stockId, + 'price' => $price, + 'location_id' => $locationId, + 'shopping_location_id' => $shoppingLocationId + ]); + $stockRow->save(); + + if (GROCY_FEATURE_FLAG_LABEL_PRINTER && GROCY_LABEL_PRINTER_RUN_SERVER) + { + $webhookData = array_merge([ + 'product' => $productDetails->product->name, + 'grocycode' => (string)(new Grocycode(Grocycode::PRODUCT, $productId, [$stockId])), + ], GROCY_LABEL_PRINTER_PARAMS); + + if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) + { + $webhookData['due_date'] = $this->getLocalizationService()->__t('DD') . ': ' . $bestBeforeDate; + } + + $runner = new WebhookRunner(); + $runner->run(GROCY_LABEL_PRINTER_WEBHOOK, $webhookData, GROCY_LABEL_PRINTER_HOOK_JSON); + } } + } + else + { + // No or single label => one stock entry - $webhookData = array_merge([ - 'product' => $productDetails->product->name, - 'grocycode' => (string)(new Grocycode(Grocycode::PRODUCT, $productId, [$stockId])), - ], GROCY_LABEL_PRINTER_PARAMS); + $stockId = uniqid(); + $logRow = $this->getDatabase()->stock_log()->createRow([ + 'product_id' => $productId, + 'amount' => $amount, + 'best_before_date' => $bestBeforeDate, + 'purchased_date' => $purchasedDate, + 'stock_id' => $stockId, + 'transaction_type' => $transactionType, + 'price' => $price, + 'location_id' => $locationId, + 'transaction_id' => $transactionId, + 'shopping_location_id' => $shoppingLocationId, + 'user_id' => GROCY_USER_ID + ]); + $logRow->save(); - if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) + $stockRow = $this->getDatabase()->stock()->createRow([ + 'product_id' => $productId, + 'amount' => $amount, + 'best_before_date' => $bestBeforeDate, + 'purchased_date' => $purchasedDate, + 'stock_id' => $stockId, + 'price' => $price, + 'location_id' => $locationId, + 'shopping_location_id' => $shoppingLocationId + ]); + $stockRow->save(); + + if (GROCY_FEATURE_FLAG_LABEL_PRINTER && GROCY_LABEL_PRINTER_RUN_SERVER) { - $webhookData['due_date'] = $this->getLocalizationService()->__t('DD') . ': ' . $bestBeforeDate; - } + $webhookData = array_merge([ + 'product' => $productDetails->product->name, + 'grocycode' => (string)(new Grocycode(Grocycode::PRODUCT, $productId, [$stockId])), + ], GROCY_LABEL_PRINTER_PARAMS); - $runner = new WebhookRunner(); + if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) + { + $webhookData['due_date'] = $this->getLocalizationService()->__t('DD') . ': ' . $bestBeforeDate; + } - for ($i = 0; $i < $reps; $i++) - { + $runner = new WebhookRunner(); $runner->run(GROCY_LABEL_PRINTER_WEBHOOK, $webhookData, GROCY_LABEL_PRINTER_HOOK_JSON); } } diff --git a/views/productform.blade.php b/views/productform.blade.php index 8cb0aba7..749617ca 100644 --- a/views/productform.blade.php +++ b/views/productform.blade.php @@ -447,23 +447,23 @@ @if(GROCY_FEATURE_FLAG_LABEL_PRINTER)