diff --git a/changelog/81_UNRELEASED_xxxx-xx-xx.md b/changelog/81_UNRELEASED_xxxx-xx-xx.md index 67f22f48..0635e5c7 100644 --- a/changelog/81_UNRELEASED_xxxx-xx-xx.md +++ b/changelog/81_UNRELEASED_xxxx-xx-xx.md @@ -10,6 +10,11 @@ ### Stock +- Optimized product definition quantity unit handling: + - As long as a product was not once in stock, the product options "Default quantity unit purchase", "Default quantity unit consume" and "Quantity unit for prices" can now be changed to any other unit + - When necessary (means when no default quantity unit conversions apply), "1:1" product specific quantity unit conversion between the product's QU stock and the corresponding other unit will now be created automatically after editing a product (like already done when initially creating a product with different unit definitions) + - For convenience, when changing a product's QU stock and when all other unit properties ("Default quantity unit purchase" etc.) are the same, they will now be changed in tandem (like already done when creating a new product and initially setting the product's QU stock) + - (This should drastically improve the workflow of completing a product setup looked up via an external barcode lookup plugin) - Optimized the line plot markers color of the price history chart (product card) (thanks @DeepCoreSystem) - External barcode lookup plugin optimizations: - When an image URL without a file extension is returned, the file extension is now determined by the Content-Type header (if any) (thanks @jordy-u for the idea) diff --git a/controllers/StockController.php b/controllers/StockController.php index 408c29c7..09742aad 100644 --- a/controllers/StockController.php +++ b/controllers/StockController.php @@ -195,12 +195,13 @@ class StockController extends BaseController { if ($args['productId'] == 'new') { + $quantityunits = $this->getDatabase()->quantity_units()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'); + return $this->renderPage($response, 'productform', [ 'locations' => $this->getDatabase()->locations()->where('active = 1')->orderBy('name'), 'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'), - 'quantityunits' => $this->getDatabase()->quantity_units()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'), - 'quantityunitsStock' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), - 'referencedQuantityunits' => $this->getDatabase()->quantity_units()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'), + 'quantityunitsAll' => $quantityunits, + 'quantityunitsReferenced' => $quantityunits, 'shoppinglocations' => $this->getDatabase()->shopping_locations()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'), 'productgroups' => $this->getDatabase()->product_groups()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'), 'userfields' => $this->getUserfieldsService()->GetFields('products'), @@ -217,9 +218,8 @@ class StockController extends BaseController 'product' => $product, 'locations' => $this->getDatabase()->locations()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'), 'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'), - 'quantityunits' => $this->getDatabase()->quantity_units()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'), - 'quantityunitsStock' => $this->getDatabase()->quantity_units()->where('id IN (SELECT to_qu_id FROM cache__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'), - 'referencedQuantityunits' => $this->getDatabase()->quantity_units()->where('active = 1')->where('id IN (SELECT to_qu_id FROM cache__quantity_unit_conversions_resolved WHERE product_id = :1)', $product->id)->orderBy('name', 'COLLATE NOCASE'), + 'quantityunitsAll' => $this->getDatabase()->quantity_units()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'), + 'quantityunitsReferenced' => $this->getDatabase()->quantity_units()->where('id IN (SELECT to_qu_id FROM cache__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'), 'shoppinglocations' => $this->getDatabase()->shopping_locations()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'), 'productgroups' => $this->getDatabase()->product_groups()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'), 'userfields' => $this->getUserfieldsService()->GetFields('products'), diff --git a/localization/strings.pot b/localization/strings.pot index bdb68303..dc8d22d2 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -2470,3 +2470,6 @@ msgstr "" msgid "List actions" msgstr "" + +msgid "After this product was once in stock and when the desired quantity unit cannot be selected here, first create a corresponding unit conversion" +msgstr "" diff --git a/migrations/0255.sql b/migrations/0255.sql new file mode 100644 index 00000000..cdca7d16 --- /dev/null +++ b/migrations/0255.sql @@ -0,0 +1,66 @@ +DROP TRIGGER products_default_qu_conversions; +CREATE TRIGGER products_default_qu_conversions_INS AFTER INSERT ON products +BEGIN + -- Create product specific 1:1 conversions when QU stock != QU purchase/consume/price + -- and when no default QU conversion apply + + -- with qu_id_stock != qu_id_purchase + 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); + + -- with qu_id_stock != qu_id_consume + 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); + + -- with qu_id_stock != qu_id_price + INSERT INTO quantity_unit_conversions + (from_qu_id, to_qu_id, factor, product_id) + SELECT p.qu_id_price, p.qu_id_stock, 1, p.id + FROM products p + WHERE p.id = NEW.id + AND p.qu_id_stock != qu_id_price + 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_price); +END; + +CREATE TRIGGER products_default_qu_conversions_UPD AFTER UPDATE ON products +BEGIN + -- Create product specific 1:1 conversions when QU stock != QU purchase/consume/price + -- and when no default QU conversion apply + + -- with qu_id_stock != qu_id_purchase + 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); + + -- with qu_id_stock != qu_id_consume + 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); + + -- with qu_id_stock != qu_id_price + INSERT INTO quantity_unit_conversions + (from_qu_id, to_qu_id, factor, product_id) + SELECT p.qu_id_price, p.qu_id_stock, 1, p.id + FROM products p + WHERE p.id = NEW.id + AND p.qu_id_stock != qu_id_price + 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_price); +END; diff --git a/public/viewjs/productform.js b/public/viewjs/productform.js index 6991a44c..a2f3002d 100644 --- a/public/viewjs/productform.js +++ b/public/viewjs/productform.js @@ -352,29 +352,33 @@ $(document).on('click', '.barcode-delete-button', function(e) }); }); +var quIdStockBefore = $("#qu_id_stock").val(); $('#qu_id_stock').change(function(e) { - // Preset qu_id_purchase/qu_id_consume/qu_id_price by qu_id_stock if unset + // Preset qu_id_purchase / qu_id_consume / qu_id_price by qu_id_stock if unset or identical + var quIdStock = $('#qu_id_stock'); var quIdPurchase = $('#qu_id_purchase'); var quIdConsume = $('#qu_id_consume'); var quIdPrice = $('#qu_id_price'); - if (quIdPurchase[0].selectedIndex === 0 && quIdStock[0].selectedIndex !== 0) + if (quIdPurchase[0].selectedIndex === 0 && quIdStock[0].selectedIndex !== 0 || quIdStockBefore == quIdPurchase.val()) { quIdPurchase[0].selectedIndex = quIdStock[0].selectedIndex; } - if (quIdConsume[0].selectedIndex === 0 && quIdStock[0].selectedIndex !== 0) + if (quIdConsume[0].selectedIndex === 0 && quIdStock[0].selectedIndex !== 0 || quIdStockBefore == quIdConsume.val()) { quIdConsume[0].selectedIndex = quIdStock[0].selectedIndex; } - if (quIdPrice[0].selectedIndex === 0 && quIdStock[0].selectedIndex !== 0) + if (quIdPrice[0].selectedIndex === 0 && quIdStock[0].selectedIndex !== 0 || quIdStockBefore == quIdPrice.val()) { quIdPrice[0].selectedIndex = quIdStock[0].selectedIndex; } + quIdStockBefore = quIdStock.val(); + Grocy.FrontendHelpers.ValidateForm('product-form'); }); diff --git a/views/productform.blade.php b/views/productform.blade.php index b71436ec..da5c9f27 100644 --- a/views/productform.blade.php +++ b/views/productform.blade.php @@ -371,15 +371,19 @@