Automatically create/update/delete inverse QU conversions (closes #1844)

This commit is contained in:
Bernd Bestel 2022-04-06 22:21:21 +02:00
parent cefc1b7b9c
commit 6ecf94073d
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
6 changed files with 124 additions and 95 deletions

View File

@ -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)

View File

@ -1601,9 +1601,6 @@ msgstr ""
msgid "means %1$s per %2$s"
msgstr ""
msgid "Create inverse QU conversion"
msgstr ""
msgid "Create recipe"
msgstr ""

108
migrations/0188.sql Normal file
View File

@ -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;

View File

@ -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
{

View File

@ -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);

View File

@ -114,25 +114,12 @@
'decimals' => $userSettings['stock_decimal_places_amounts'],
'value' => $value,
'additionalHtmlElements' => '<p id="qu-conversion-info"
class="form-text text-info d-none mb-0"></p>
<p id="qu-conversion-inverse-info"
class="form-text text-info d-none"></p>',
'additionalCssClasses' => 'input-group-qu locale-number-input locale-number-quantity-amount'
))
<div class="form-group @if($mode == 'edit') d-none @endif">
<div class="custom-control custom-checkbox">
<input checked
class="form-check-input custom-control-input"
type="checkbox"
id="create_inverse"
name="create_inverse:skip"
value="1">
<label class="form-check-label custom-control-label"
for="create_inverse">{{ $__t('Create inverse QU conversion') }}</label>
</div>
<span id="qu-conversion-inverse-info"
class="form-text text-info d-none"></span>
</div>
@include('components.userfieldsform', array(
'userfields' => $userfields,
'entity' => 'quantity_unit_conversions'