diff --git a/changelog/67_UNRELEASED_xxxx-xx-xx.md b/changelog/67_UNRELEASED_xxxx-xx-xx.md index f554209f..2f4d2b90 100644 --- a/changelog/67_UNRELEASED_xxxx-xx-xx.md +++ b/changelog/67_UNRELEASED_xxxx-xx-xx.md @@ -39,6 +39,9 @@ - 1. The new default consume location, if the product currently has any stock there, otherwise - 2. The products default location, if the product currently has any stock there, otherwise - 3. The first location where the product currently has any stock +- Optimized quantity unit conversion handling: + - The option "Create inverse QU conversion" was removed when creating a QU conversion + - => Instead the corresponding inverse conversion is now always created/updated/deleted automatically - New product option "Disable own stock" (defaults to disabled) - When enabled, the corresponding product can't have own stock, means it will not be selectable on purchase (useful for parent products which are just used as a summary/total view of the child products) - The location content sheet can now optionally list also out of stock products (at the products default location, new checkbox "Show only in-stock products " at the top of the page, defaults to enabled) diff --git a/localization/strings.pot b/localization/strings.pot index 884fac3a..80120c36 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1601,9 +1601,6 @@ msgstr "" msgid "means %1$s per %2$s" msgstr "" -msgid "Create inverse QU conversion" -msgstr "" - msgid "Create recipe" msgstr "" diff --git a/migrations/0188.sql b/migrations/0188.sql new file mode 100644 index 00000000..2112e5ad --- /dev/null +++ b/migrations/0188.sql @@ -0,0 +1,108 @@ +-- Remove QU conversions which are already defined by the products qu_factor_purchase_to_stock +DELETE FROM quantity_unit_conversions +WHERE id IN ( + SELECT quc.id + FROM quantity_unit_conversions quc + JOIN products p + ON quc.product_id = p.id + WHERE (p.qu_id_purchase = quc.to_qu_id AND p.qu_id_stock = quc.from_qu_id) + OR (p.qu_id_purchase = quc.from_qu_id AND p.qu_id_stock = quc.to_qu_id) +); + +DROP TRIGGER quantity_unit_conversions_custom_unique_constraint_INS; +CREATE TRIGGER qu_conversions_custom_constraint_INS BEFORE INSERT ON quantity_unit_conversions +BEGIN + /* + Necessary because unique constraints don't include NULL values in SQLite, + and also because the constraint should include the products default conversion factor + */ +SELECT CASE WHEN(( + SELECT 1 + FROM quantity_unit_conversions + WHERE from_qu_id = NEW.from_qu_id + AND to_qu_id = NEW.to_qu_id + AND IFNULL(product_id, 0) = IFNULL(NEW.product_id, 0) + UNION + SELECT 1 + FROM products + WHERE id = NEW.product_id + AND qu_id_purchase = NEW.from_qu_id + AND qu_id_stock = NEW.to_qu_id + UNION + SELECT 1 + FROM products + WHERE id = NEW.product_id + AND qu_id_purchase = NEW.to_qu_id + AND qu_id_stock = NEW.from_qu_id + ) + NOTNULL) THEN RAISE(ABORT, "QU constraint violation") END; +END; + +DROP TRIGGER quantity_unit_conversions_custom_unique_constraint_UPD; +CREATE TRIGGER qu_conversions_custom_constraint_UPD BEFORE UPDATE ON quantity_unit_conversions +BEGIN + /* This contains practically the same logic as the trigger qu_conversions_custom_constraint_INS */ + + /* + Necessary because unique constraints don't include NULL values in SQLite, + and also because the constraint should include the products default conversion factor + */ +SELECT CASE WHEN(( + SELECT 1 + FROM quantity_unit_conversions + WHERE from_qu_id = NEW.from_qu_id + AND to_qu_id = NEW.to_qu_id + AND IFNULL(product_id, 0) = IFNULL(NEW.product_id, 0) + AND id != NEW.id + UNION + SELECT 1 + FROM products + WHERE id = NEW.product_id + AND qu_id_purchase = NEW.from_qu_id + AND qu_id_stock = NEW.to_qu_id + UNION + SELECT 1 + FROM products + WHERE id = NEW.product_id + AND qu_id_purchase = NEW.to_qu_id + AND qu_id_stock = NEW.from_qu_id + ) + NOTNULL) THEN RAISE(ABORT, "QU constraint violation") END; +END; + +CREATE TRIGGER qu_conversions_inverse_INS AFTER INSERT ON quantity_unit_conversions +BEGIN + /* + Create the inverse QU conversion + */ + + INSERT OR REPLACE INTO quantity_unit_conversions + (from_qu_id, to_qu_id, factor, product_id) + VALUES + (NEW.to_qu_id, NEW.from_qu_id, 1 / IFNULL(NEW.factor, 1), NEW.product_id); +END; + +CREATE TRIGGER qu_conversions_inverse_UPD AFTER UPDATE ON quantity_unit_conversions +BEGIN + /* + Update the inverse QU conversion + */ + + UPDATE quantity_unit_conversions + SET factor = 1 / IFNULL(NEW.factor, 1) + WHERE from_qu_id = NEW.to_qu_id + AND to_qu_id = NEW.from_qu_id + AND IFNULL(product_id, -1) = IFNULL(NEW.product_id, -1); +END; + +CREATE TRIGGER qu_conversions_inverse_DEL AFTER DELETE ON quantity_unit_conversions +BEGIN + /* + Delete the inverse QU conversion + */ + + DELETE FROM quantity_unit_conversions + WHERE from_qu_id = OLD.to_qu_id + AND to_qu_id = OLD.from_qu_id + AND IFNULL(product_id, -1) = IFNULL(OLD.product_id, -1); +END; diff --git a/public/viewjs/quantityunitconversionform.js b/public/viewjs/quantityunitconversionform.js index 10edc375..c32599d2 100644 --- a/public/viewjs/quantityunitconversionform.js +++ b/public/viewjs/quantityunitconversionform.js @@ -15,11 +15,6 @@ var jsonData = $('#quconversion-form').serializeJSON(); jsonData.from_qu_id = $("#from_qu_id").val(); Grocy.FrontendHelpers.BeginUiBusy("quconversion-form"); - if ($("#create_inverse").is(":checked")) - { - var inverse_to_qu_id = $("#from_qu_id").val(); - var inverse_from_qu_id = $("#to_qu_id").val(); - } if (Grocy.EditMode === 'create') { @@ -29,63 +24,22 @@ Grocy.EditObjectId = result.created_object_id; Grocy.Components.UserfieldsForm.Save(function() { - if ($("#create_inverse").is(":checked")) + if (typeof GetUriParam("qu-unit") !== "undefined") { - jsonData.to_qu_id = inverse_to_qu_id; - jsonData.from_qu_id = inverse_from_qu_id; - jsonData.factor = 1 / jsonData.factor; - - //Create Inverse - Grocy.Api.Post('objects/quantity_unit_conversions', jsonData, - function(result) - { - Grocy.EditObjectId = result.created_object_id; - Grocy.Components.UserfieldsForm.Save(function() - { - if (typeof GetUriParam("qu-unit") !== "undefined") - { - if (GetUriParam("embedded") !== undefined) - { - window.parent.postMessage(WindowMessageBag("Reload"), Grocy.BaseUrl); - } - else - { - window.location.href = U("/quantityunit/" + GetUriParam("qu-unit")); - } - } - else - { - window.parent.postMessage(WindowMessageBag("ProductQUConversionChanged"), U("/product/" + GetUriParam("product"))); - window.parent.postMessage(WindowMessageBag("CloseAllModals"), U("/product/" + GetUriParam("product"))); - } - }); - }, - function(xhr) - { - Grocy.FrontendHelpers.EndUiBusy("quconversion-form"); - Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) - } - ); - } - else - { - if (typeof GetUriParam("qu-unit") !== "undefined") + if (GetUriParam("embedded") !== undefined) { - if (GetUriParam("embedded") !== undefined) - { - window.parent.postMessage(WindowMessageBag("Reload"), Grocy.BaseUrl); - } - else - { - window.location.href = U("/quantityunit/" + GetUriParam("qu-unit")); - } + window.parent.postMessage(WindowMessageBag("Reload"), Grocy.BaseUrl); } else { - window.parent.postMessage(WindowMessageBag("ProductQUConversionChanged"), U("/product/" + GetUriParam("product"))); - window.parent.postMessage(WindowMessageBag("CloseAllModals"), U("/product/" + GetUriParam("product"))); + window.location.href = U("/quantityunit/" + GetUriParam("qu-unit")); } } + else + { + window.parent.postMessage(WindowMessageBag("ProductQUConversionChanged"), U("/product/" + GetUriParam("product"))); + window.parent.postMessage(WindowMessageBag("CloseAllModals"), U("/product/" + GetUriParam("product"))); + } }); }, function(xhr) @@ -152,20 +106,6 @@ $('#quconversion-form input').keydown(function(event) } }); -$("#create_inverse").on("change", function() -{ - var value = $(this).is(":checked"); - - if (value) - { - $('#qu-conversion-inverse-info').removeClass('d-none'); - } - else - { - $('#qu-conversion-inverse-info').addClass('d-none'); - } -}); - $('.input-group-qu').on('change', function(e) { var fromQuId = $("#from_qu_id").val(); @@ -186,12 +126,8 @@ $('.input-group-qu').on('change', function(e) { $('#qu-conversion-info').text(__t('This means 1 %1$s is the same as %2$s %3$s', $("#from_qu_id option:selected").text(), parseFloat((1 * factor)).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }), __n((1 * factor).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }), $("#to_qu_id option:selected").text(), $("#to_qu_id option:selected").data("plural-form"), true))); $('#qu-conversion-info').removeClass('d-none'); - - if (Grocy.EditMode === 'create') - { - $('#qu-conversion-inverse-info').text(__t('This means 1 %1$s is the same as %2$s %3$s', $("#to_qu_id option:selected").text(), parseFloat((1 / factor)).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }), __n((1 / factor).toString(), $("#from_qu_id option:selected").text(), $("#from_qu_id option:selected").data("plural-form"), true))); - $('#qu-conversion-inverse-info').removeClass('d-none'); - } + $('#qu-conversion-inverse-info').removeClass('d-none'); + $('#qu-conversion-inverse-info').text(__t('This means 1 %1$s is the same as %2$s %3$s', $("#to_qu_id option:selected").text(), parseFloat((1 / factor)).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }), __n((1 / factor).toString(), $("#from_qu_id option:selected").text(), $("#from_qu_id option:selected").data("plural-form"), true))); } else { diff --git a/services/DemoDataGeneratorService.php b/services/DemoDataGeneratorService.php index 4311b83f..d9da4768 100644 --- a/services/DemoDataGeneratorService.php +++ b/services/DemoDataGeneratorService.php @@ -111,9 +111,7 @@ class DemoDataGeneratorService extends BaseService UPDATE products SET qu_id_purchase = (SELECT MIN(id) FROM quantity_units) WHERE id IN (SELECT id FROM products WHERE qu_id_purchase NOT IN (SELECT id FROM quantity_units)); INSERT INTO quantity_unit_conversions (from_qu_id, to_qu_id, factor, product_id) VALUES (3, 12, 10, 10); - INSERT INTO quantity_unit_conversions (from_qu_id, to_qu_id, factor, product_id) VALUES (12, 3, 0.1, 10); INSERT INTO quantity_unit_conversions (from_qu_id, to_qu_id, factor, product_id) VALUES (3, 8, 1000, 22); - INSERT INTO quantity_unit_conversions (from_qu_id, to_qu_id, factor, product_id) VALUES (8, 3, 0.001, 22); INSERT INTO shopping_list (note, amount) VALUES ('{$this->__t_sql('Some good snacks')}', 1); INSERT INTO shopping_list (product_id, amount) VALUES (20, 1); diff --git a/views/quantityunitconversionform.blade.php b/views/quantityunitconversionform.blade.php index 60ae5c68..4db6a324 100644 --- a/views/quantityunitconversionform.blade.php +++ b/views/quantityunitconversionform.blade.php @@ -114,25 +114,12 @@ 'decimals' => $userSettings['stock_decimal_places_amounts'], 'value' => $value, 'additionalHtmlElements' => '

+

', 'additionalCssClasses' => 'input-group-qu locale-number-input locale-number-quantity-amount' )) -
-
- - -
- -
- @include('components.userfieldsform', array( 'userfields' => $userfields, 'entity' => 'quantity_unit_conversions'