From 0585e80c704871c2fc3185b85773f3a14c2c2b84 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Mon, 26 Dec 2022 11:11:55 +0100 Subject: [PATCH] Implemented "Default quantity unit consume" (closes #1845) --- changelog/70_UNRELEASED_xxxx.xx.xx.md | 8 +- controllers/StockController.php | 4 +- grocy.openapi.json | 5 +- localization/strings.pot | 6 ++ migrations/0210.sql | 135 ++++++++++++++++++++++++++ public/viewjs/consume.js | 8 +- public/viewjs/productform.js | 11 ++- services/StockService.php | 4 +- views/productform.blade.php | 22 ++++- views/stockoverview.blade.php | 26 ++--- 10 files changed, 204 insertions(+), 25 deletions(-) create mode 100644 migrations/0210.sql diff --git a/changelog/70_UNRELEASED_xxxx.xx.xx.md b/changelog/70_UNRELEASED_xxxx.xx.xx.md index ed25f162..ca002c49 100644 --- a/changelog/70_UNRELEASED_xxxx.xx.xx.md +++ b/changelog/70_UNRELEASED_xxxx.xx.xx.md @@ -14,6 +14,10 @@ - The product option "Factor purchase to stock quantity unit" was removed - => Use normal product specific QU conversions instead, if needed - An existing "Factor purchase to stock quantity unit" was automatically migrated to a product specific QU conversion +- New product option "Default quantity unit consume" + - Will be used/selected as the default quantity unit on the consume page + - The product's "Quick consume amount" is now displayed related to this quantity unit ("quick consume/open buttons" on the stock overview page) + - Defaults to the product's "Quantity unit stock" (so no changed behavior when not configured) - Fixed that hiding the "Purchased date" column (table options) on the stock entries page didn't work - Fixed that the consumed amount was wrong, when consuming multiple substituted subproducts at once and when multiple/different conversion factors were involved @@ -63,8 +67,10 @@ - ⚠️ **Breaking changes**: - The product property `qu_factor_purchase_to_stock` was removed (existing factors were migrated to normal product specific QU conversions, see above) - - The endpoint `/stock/products/{productId}` returns a new field/property `qu_conversion_factor_purchase_to_stock` for convenience (contains the conversion factor of the corresponding QU conversion from the product's qu_id_purchase to qu_id_stock) - Numbers are now returned as numbers (so technically without quotes around them, were strings for nearly all endpoints before) +- Endpoint `/stock/products/{productId}`: + - Added a new field/property `qu_conversion_factor_purchase_to_stock` for convenience (contains the conversion factor of the corresponding QU conversion from the product's qu_id_purchase to qu_id_stock) + - Added a new field/property `default_quantity_unit_consume` (contains the quantity unit object of the product's "Default quantity unit consume") - The following entities are now also available via the endpoint `/objects/{entity}` (only listing, no edit) - `quantity_unit_conversions_resolved` (returns all final/resolved conversion factors per product and any directly or indirectly related quantity units) - The endpoint `/batteries` now also returns the corresponding battery object (as field/property `battery`) diff --git a/controllers/StockController.php b/controllers/StockController.php index 20a2defb..26ec8527 100644 --- a/controllers/StockController.php +++ b/controllers/StockController.php @@ -163,7 +163,7 @@ class StockController extends BaseController 'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'), 'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), 'quantityunitsStock' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), - 'quantityunitsPurchase' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), + 'referencedQuantityunits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'), 'productgroups' => $this->getDatabase()->product_groups()->orderBy('name', 'COLLATE NOCASE'), 'userfields' => $this->getUserfieldsService()->GetFields('products'), @@ -182,7 +182,7 @@ class StockController extends BaseController 'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'), 'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), 'quantityunitsStock' => $this->getDatabase()->quantity_units()->where('id IN (SELECT to_qu_id FROM quantity_unit_conversions_resolved WHERE product_id = :1) OR NOT EXISTS(SELECT 1 FROM stock_log WHERE product_id = :1)', $product->id)->orderBy('name', 'COLLATE NOCASE'), - 'quantityunitsPurchase' => $this->getDatabase()->quantity_units()->where('id IN (SELECT to_qu_id FROM quantity_unit_conversions_resolved WHERE product_id = :1)', $product->id)->orderBy('name', 'COLLATE NOCASE'), + 'referencedQuantityunits' => $this->getDatabase()->quantity_units()->where('id IN (SELECT to_qu_id FROM quantity_unit_conversions_resolved WHERE product_id = :1)', $product->id)->orderBy('name', 'COLLATE NOCASE'), 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'), 'productgroups' => $this->getDatabase()->product_groups()->orderBy('name', 'COLLATE NOCASE'), 'userfields' => $this->getUserfieldsService()->GetFields('products'), diff --git a/grocy.openapi.json b/grocy.openapi.json index fb39ff97..68a81ccc 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -4732,10 +4732,13 @@ "product_barcodes": { "$ref": "#/components/schemas/ProductBarcode" }, + "quantity_unit_stock": { + "$ref": "#/components/schemas/QuantityUnit" + }, "default_quantity_unit_purchase": { "$ref": "#/components/schemas/QuantityUnit" }, - "quantity_unit_stock": { + "default_quantity_unit_consume": { "$ref": "#/components/schemas/QuantityUnit" }, "last_purchased": { diff --git a/localization/strings.pot b/localization/strings.pot index d356a034..7d90e980 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -2344,3 +2344,9 @@ msgstr "" msgid "Product specifc QU conversions" msgstr "" + +msgid "Default quantity unit consume" +msgstr "" + +msgid "This is the default quantity unit used when consuming this product" +msgstr "" diff --git a/migrations/0210.sql b/migrations/0210.sql new file mode 100644 index 00000000..4b76a37d --- /dev/null +++ b/migrations/0210.sql @@ -0,0 +1,135 @@ +ALTER TABLE products +ADD qu_id_consume INTEGER; + +UPDATE products +SET qu_id_consume = qu_id_stock; + +CREATE TRIGGER default_qu_id_consume AFTER INSERT ON products +BEGIN + UPDATE products + SET qu_id_consume = qu_id_stock + WHERE id = NEW.id + AND IFNULL(qu_id_consume, 0) = 0; +END; + +DROP TRIGGER default_qu_conversion; +CREATE TRIGGER default_qu_conversions AFTER INSERT ON products +BEGIN + /* + Automatically create a default (product specific 1 to 1) QU conversion when a product + with qu_stock != qu_purchase was created and when no default QU conversion applies + */ + INSERT INTO quantity_unit_conversions + (from_qu_id, to_qu_id, factor, product_id) + SELECT p.qu_id_purchase, p.qu_id_stock, 1, p.id + FROM products p + WHERE p.id = NEW.id + AND p.qu_id_stock != qu_id_purchase + AND NOT EXISTS(SELECT 1 FROM quantity_unit_conversions_resolved WHERE product_id = p.id AND from_qu_id = p.qu_id_stock AND to_qu_id = p.qu_id_purchase); + + /* + Automatically create a default (product specific 1 to 1) QU conversion when a product + with qu_stock != qu_consume was created and when no default QU conversion applies + */ + INSERT INTO quantity_unit_conversions + (from_qu_id, to_qu_id, factor, product_id) + SELECT p.qu_id_consume, p.qu_id_stock, 1, p.id + FROM products p + WHERE p.id = NEW.id + AND p.qu_id_stock != qu_id_consume + AND NOT EXISTS(SELECT 1 FROM quantity_unit_conversions_resolved WHERE product_id = p.id AND from_qu_id = p.qu_id_stock AND to_qu_id = p.qu_id_consume); +END; + +DROP VIEW products_view; +CREATE VIEW products_view +AS +SELECT + p.*, + CASE WHEN (SELECT 1 FROM products WHERE parent_product_id = p.id) NOTNULL THEN 1 ELSE 0 END AS has_sub_products, + IFNULL(quc_purchase.factor, 1.0) AS qu_factor_purchase_to_stock, + IFNULL(quc_consume.factor, 1.0) AS qu_factor_consume_to_stock +FROM products p +LEFT JOIN quantity_unit_conversions_resolved quc_purchase + ON p.id = quc_purchase.product_id + AND p.qu_id_purchase = quc_purchase.from_qu_id + AND p.qu_id_stock = quc_purchase.to_qu_id +LEFT JOIN quantity_unit_conversions_resolved quc_consume + ON p.id = quc_consume.product_id + AND p.qu_id_consume = quc_consume.from_qu_id + AND p.qu_id_stock = quc_consume.to_qu_id; + +DROP VIEW uihelper_stock_current_overview; +CREATE VIEW uihelper_stock_current_overview +AS +SELECT + p.id, + sc.amount_opened AS amount_opened, + p.tare_weight AS tare_weight, + p.enable_tare_weight_handling AS enable_tare_weight_handling, + sc.amount AS amount, + sc.value as value, + sc.product_id AS product_id, + sc.best_before_date AS best_before_date, + EXISTS(SELECT id FROM stock_missing_products WHERE id = sc.product_id) AS product_missing, + p.name AS product_name, + pg.name AS product_group_name, + EXISTS(SELECT * FROM shopping_list WHERE shopping_list.product_id = sc.product_id) AS on_shopping_list, + qu_stock.name AS qu_stock_name, + qu_stock.name_plural AS qu_stock_name_plural, + qu_purchase.name AS qu_purchase_name, + qu_purchase.name_plural AS qu_purchase_name_plural, + qu_consume.name AS qu_consume_name, + qu_consume.name_plural AS qu_consume_name_plural, + sc.is_aggregated_amount, + sc.amount_opened_aggregated, + sc.amount_aggregated, + p.calories AS product_calories, + sc.amount * p.calories AS calories, + sc.amount_aggregated * p.calories AS calories_aggregated, + p.quick_consume_amount, + p.quick_consume_amount / p.qu_factor_consume_to_stock AS quick_consume_amount_qu_consume, + p.due_type, + plp.purchased_date AS last_purchased, + plp.price AS last_price, + pap.price as average_price, + p.min_stock_amount, + pbcs.barcodes AS product_barcodes, + p.description AS product_description, + l.name AS product_default_location_name, + p_parent.id AS parent_product_id, + p_parent.name AS parent_product_name, + p.picture_file_name AS product_picture_file_name, + p.no_own_stock AS product_no_own_stock, + p.qu_factor_purchase_to_stock AS product_qu_factor_purchase_to_stock +FROM ( + SELECT * + FROM stock_current + WHERE best_before_date IS NOT NULL + UNION + SELECT m.id, 0, 0, 0, null, 0, 0, 0, p.due_type + FROM stock_missing_products m + JOIN products p + ON m.id = p.id + WHERE m.id NOT IN (SELECT product_id FROM stock_current) + ) sc +JOIN products_view p + ON sc.product_id = p.id +JOIN locations l + ON p.location_id = l.id +JOIN quantity_units qu_stock + ON p.qu_id_stock = qu_stock.id +JOIN quantity_units qu_purchase + ON p.qu_id_purchase = qu_purchase.id +JOIN quantity_units qu_consume + ON p.qu_id_consume = qu_consume.id +LEFT JOIN product_groups pg + ON p.product_group_id = pg.id +LEFT JOIN products_last_purchased plp + ON sc.product_id = plp.product_id +LEFT JOIN products_average_price pap + ON sc.product_id = pap.product_id +LEFT JOIN product_barcodes_comma_separated pbcs + ON sc.product_id = pbcs.product_id +LEFT JOIN products p_parent + ON p.parent_product_id = p_parent.id +WHERE p.hide_on_stock_overview = 0; diff --git a/public/viewjs/consume.js b/public/viewjs/consume.js index 871ff035..81073d38 100644 --- a/public/viewjs/consume.js +++ b/public/viewjs/consume.js @@ -106,7 +106,7 @@ $("#display_amount").removeAttr("max"); if (BoolVal(Grocy.UserSettings.stock_default_consume_amount_use_quick_consume_amount)) { - $('#display_amount').val(productDetails.product.quick_consume_amount); + $('#display_amount').val(productDetails.product.quick_consume_amount * $("#qu_id option:selected").attr("data-qu-factor")); } else { @@ -196,7 +196,7 @@ $('#save-mark-as-open-button').on('click', function(e) if (BoolVal(Grocy.UserSettings.stock_default_consume_amount_use_quick_consume_amount)) { - $('#display_amount').val(productDetails.product.quick_consume_amount); + $('#display_amount').val(productDetails.product.quick_consume_amount * $("#qu_id option:selected").attr("data-qu-factor")); } else { @@ -365,10 +365,10 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e) current_productDetails = productDetails; Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_stock.id); - Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.quantity_unit_stock.id); + Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.default_quantity_unit_consume.id); if (BoolVal(Grocy.UserSettings.stock_default_consume_amount_use_quick_consume_amount)) { - $('#display_amount').val(productDetails.product.quick_consume_amount); + $('#display_amount').val(productDetails.product.quick_consume_amount * $("#qu_id option:selected").attr("data-qu-factor")); } else { diff --git a/public/viewjs/productform.js b/public/viewjs/productform.js index e84f4ae6..b003af32 100644 --- a/public/viewjs/productform.js +++ b/public/viewjs/productform.js @@ -358,15 +358,22 @@ $(document).on('click', '.barcode-delete-button', function(e) $('#qu_id_stock').change(function(e) { - // Preset QU purchase with stock QU if unset + // Preset qu_id_purchase and qu_id_consume by qu_id_stock if unset var quIdStock = $('#qu_id_stock'); var quIdPurchase = $('#qu_id_purchase'); + var quIdConsume = $('#qu_id_consume'); if (quIdPurchase[0].selectedIndex === 0 && quIdStock[0].selectedIndex !== 0) { quIdPurchase[0].selectedIndex = quIdStock[0].selectedIndex; - Grocy.FrontendHelpers.ValidateForm('product-form'); } + + if (quIdConsume[0].selectedIndex === 0 && quIdStock[0].selectedIndex !== 0) + { + quIdConsume[0].selectedIndex = quIdStock[0].selectedIndex; + } + + Grocy.FrontendHelpers.ValidateForm('product-form'); }); $(window).on("message", function(e) diff --git a/services/StockService.php b/services/StockService.php index 69178058..fbb36337 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -747,6 +747,7 @@ class StockService extends BaseService $nextDueDate = $this->getDatabase()->stock()->where('product_id', $productId)->min('best_before_date'); $quPurchase = $this->getDatabase()->quantity_units($product->qu_id_purchase); $quStock = $this->getDatabase()->quantity_units($product->qu_id_stock); + $quConsume = $this->getDatabase()->quantity_units($product->qu_id_consume); $location = $this->getDatabase()->locations($product->location_id); $averageShelfLifeDays = intval($this->getDatabase()->stock_average_product_shelf_life()->where('id', $productId)->fetch()->average_shelf_life_days); $currentPrice = $this->getDatabase()->products_current_price()->where('product_id', $productId)->fetch()->price; @@ -785,8 +786,9 @@ class StockService extends BaseService 'stock_amount_opened' => $stockCurrentRow->amount_opened, 'stock_amount_aggregated' => $stockCurrentRow->amount_aggregated, 'stock_amount_opened_aggregated' => $stockCurrentRow->amount_opened_aggregated, - 'default_quantity_unit_purchase' => $quPurchase, 'quantity_unit_stock' => $quStock, + 'default_quantity_unit_purchase' => $quPurchase, + 'default_quantity_unit_consume' => $quConsume, 'last_price' => $lastPrice, 'avg_price' => $avgPrice, 'oldest_price' => $currentPrice, // Deprecated diff --git a/views/productform.blade.php b/views/productform.blade.php index ee70e6b1..c1fcf3ee 100644 --- a/views/productform.blade.php +++ b/views/productform.blade.php @@ -395,7 +395,7 @@ id="qu_id_purchase" name="qu_id_purchase"> - @foreach($quantityunitsPurchase as $quantityunit) + @foreach($referencedQuantityunits as $quantityunit) @@ -403,6 +403,26 @@
{{ $__t('A quantity unit is required') }}
+ +
+ + + +
{{ $__t('A quantity unit is required') }}
+
diff --git a/views/stockoverview.blade.php b/views/stockoverview.blade.php index 23d6ffb1..3eb370fd 100755 --- a/views/stockoverview.blade.php +++ b/views/stockoverview.blade.php @@ -192,12 +192,12 @@ href="#" data-toggle="tooltip" data-placement="left" - title="{{ $__t('Consume %1$s of %2$s', floatval($currentStockEntry->quick_consume_amount) . ' ' . $currentStockEntry->qu_unit_name, $currentStockEntry->product_name) }}" + title="{{ $__t('Consume %1$s of %2$s', floatval($currentStockEntry->quick_consume_amount_qu_consume) . ' ' . $currentStockEntry->qu_consume_name, $currentStockEntry->product_name) }}" data-product-id="{{ $currentStockEntry->product_id }}" data-product-name="{{ $currentStockEntry->product_name }}" - data-product-qu-name="{{ $currentStockEntry->qu_unit_name }}" + data-product-qu-name="{{ $currentStockEntry->qu_stock_name }}" data-consume-amount="{{ $currentStockEntry->quick_consume_amount }}"> - {{ $currentStockEntry->quick_consume_amount }} + {{ $currentStockEntry->quick_consume_amount_qu_consume }} {{ $__t('All') }} @@ -217,12 +217,12 @@ href="#" data-toggle="tooltip" data-placement="left" - title="{{ $__t('Mark %1$s of %2$s as open', floatval($currentStockEntry->quick_consume_amount) . ' ' . $currentStockEntry->qu_unit_name, $currentStockEntry->product_name) }}" + title="{{ $__t('Mark %1$s of %2$s as open', floatval($currentStockEntry->quick_consume_amount_qu_consume) . ' ' . $currentStockEntry->qu_consume_name, $currentStockEntry->product_name) }}" data-product-id="{{ $currentStockEntry->product_id }}" data-product-name="{{ $currentStockEntry->product_name }}" - data-product-qu-name="{{ $currentStockEntry->qu_unit_name }}" + data-product-qu-name="{{ $currentStockEntry->qu_stock_name }}" data-open-amount="{{ $currentStockEntry->quick_consume_amount }}"> - {{ $currentStockEntry->quick_consume_amount }} + {{ $currentStockEntry->quick_consume_amount_qu_consume }} @endif