Also relate the shopping list amount to QU stock

This commit is contained in:
Bernd Bestel 2020-11-13 17:30:57 +01:00
parent ab68a51ba7
commit b0b3322266
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
14 changed files with 249 additions and 80 deletions

View File

@ -3,6 +3,7 @@
### New feature: Use any product related quantity unit anywhere ### New feature: Use any product related quantity unit anywhere
- Finally it's possible to use any product related quantity unit on any page - Finally it's possible to use any product related quantity unit on any page
- Products still have one quantity unit stock and one (default) quantity unit purchase, but any QU, which has a direct or indirect conversion for that product, can be used to pick an amount - Products still have one quantity unit stock and one (default) quantity unit purchase, but any QU, which has a direct or indirect conversion for that product, can be used to pick an amount
_- (Because the stock quantity unit is now the base for everything, it cannot be changed after the product was once added to stock (for now, maybe there will be a possibilty to change it in a future release))_
### New feature: Prefill purchase data by barcodes ### New feature: Prefill purchase data by barcodes
- Imagine you buy for example eggs in different pack sizes and they have different barcodes - Imagine you buy for example eggs in different pack sizes and they have different barcodes
@ -127,8 +128,9 @@
- Korean (demo available at https://ko.demo.grocy.info) - Korean (demo available at https://ko.demo.grocy.info)
### API improvements/fixes ### API improvements/fixes
- Breaking changes: - **Breaking changes**:
- All prices are now related to the products **stock** quantity unit (instead of the purchase QU) - All prices are now related to the products stock quantity unit (instead of the products purchase QU)
- All (product) amounts are now related to the products stock quantity unit (was related to the products purchase QU for the shopping list before)
- The product object no longer has a field `barcodes` with a comma separated barcode list, instead barcodes are now stored in a separate table/entity `product_barcodes` (use the existing "Generic entity interactions" endpoints to access them) - The product object no longer has a field `barcodes` with a comma separated barcode list, instead barcodes are now stored in a separate table/entity `product_barcodes` (use the existing "Generic entity interactions" endpoints to access them)
- For better integration (apps), it's now possible to show a QR-Code for API keys (thanks @fipwmaqzufheoxq92ebc) - For better integration (apps), it's now possible to show a QR-Code for API keys (thanks @fipwmaqzufheoxq92ebc)
- New QR-Code button on the "Manage API keys"-page (top right corner settings menu), the QR-Codes contains `<API-Url>|<API-Key>` - New QR-Code button on the "Manage API keys"-page (top right corner settings menu), the QR-Codes contains `<API-Url>|<API-Key>`

View File

@ -322,6 +322,7 @@ class StockController extends BaseController
'productGroups' => $this->getDatabase()->product_groups()->orderBy('name'), 'productGroups' => $this->getDatabase()->product_groups()->orderBy('name'),
'shoppingLists' => $this->getDatabase()->shopping_lists()->orderBy('name'), 'shoppingLists' => $this->getDatabase()->shopping_lists()->orderBy('name'),
'selectedShoppingListId' => $listId, 'selectedShoppingListId' => $listId,
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved(),
'userfields' => $this->getUserfieldsService()->GetFields('products'), 'userfields' => $this->getUserfieldsService()->GetFields('products'),
'userfieldValues' => $this->getUserfieldsService()->GetAllValues('products') 'userfieldValues' => $this->getUserfieldsService()->GetAllValues('products')
]); ]);

View File

@ -3811,7 +3811,7 @@
"product_barcodes": { "product_barcodes": {
"$ref": "#/components/schemas/ProductBarcodeDetailsResponse" "$ref": "#/components/schemas/ProductBarcodeDetailsResponse"
}, },
"quantity_unit_purchase": { "default_quantity_unit_purchase": {
"$ref": "#/components/schemas/QuantityUnit" "$ref": "#/components/schemas/QuantityUnit"
}, },
"quantity_unit_stock": { "quantity_unit_stock": {
@ -3898,7 +3898,7 @@
"last_used": null, "last_used": null,
"stock_amount": "2", "stock_amount": "2",
"stock_amount_opened": null, "stock_amount_opened": null,
"quantity_unit_purchase": { "default_quantity_unit_purchase": {
"id": "3", "id": "3",
"name": "Pack", "name": "Pack",
"description": null, "description": null,

View File

@ -181,15 +181,6 @@ msgstr ""
msgid "Min. stock amount" msgid "Min. stock amount"
msgstr "" msgstr ""
msgid "QU purchase"
msgstr ""
msgid "QU stock"
msgstr ""
msgid "QU factor"
msgstr ""
msgid "Description" msgid "Description"
msgstr "" msgstr ""
@ -205,7 +196,7 @@ msgstr ""
msgid "Default best before days" msgid "Default best before days"
msgstr "" msgstr ""
msgid "Quantity unit purchase" msgid "Default quantity unit purchase"
msgstr "" msgstr ""
msgid "Quantity unit stock" msgid "Quantity unit stock"
@ -1930,3 +1921,6 @@ msgstr ""
msgid "Show a QR-Code for this API key" msgid "Show a QR-Code for this API key"
msgstr "" msgstr ""
msgid "This is the default quantity unit used when adding a product to the shopping list"
msgstr ""

118
migrations/0117.sql Normal file
View File

@ -0,0 +1,118 @@
ALTER TABLE shopping_list
ADD qu_id INTEGER;
UPDATE shopping_list
SET qu_id = (SELECT qu_id_purchase FROM products WHERE id = product_id)
WHERE product_id IS NOT NULL;
UPDATE shopping_list
SET amount = IFNULL(ROUND(amount * (SELECT CASE WHEN IFNULL(qu_factor_purchase_to_stock, 0) = 0 THEN 1 ELSE qu_factor_purchase_to_stock END FROM products WHERE id = product_id), 2), amount)
WHERE product_id IS NOT NULL;
CREATE TRIGGER shopping_list_qu_id_default AFTER INSERT ON shopping_list
BEGIN
UPDATE shopping_list
SET qu_id = (SELECT qu_id_stock FROM products where id = product_id)
WHERE qu_id IS NULL
AND id = NEW.id;
END;
DROP VIEW recipes_pos_resolved;
CREATE VIEW recipes_pos_resolved
AS
-- Multiplication by 1.0 to force conversion to float (REAL)
SELECT
r.id AS recipe_id,
rp.id AS recipe_pos_id,
rp.product_id AS product_id,
rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) AS recipe_amount,
IFNULL(sc.amount_aggregated, 0) AS stock_amount,
CASE WHEN IFNULL(sc.amount_aggregated, 0) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END THEN 1 ELSE 0 END AS need_fulfilled,
CASE WHEN IFNULL(sc.amount_aggregated, 0) - CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END < 0 THEN ABS(IFNULL(sc.amount_aggregated, 0) - (CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END)) ELSE 0 END AS missing_amount,
IFNULL(sl.amount, 0) AS amount_on_shopping_list,
CASE WHEN IFNULL(sc.amount_aggregated, 0) + (CASE WHEN r.not_check_shoppinglist = 1 THEN 0 ELSE IFNULL(sl.amount, 0) END) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END THEN 1 ELSE 0 END AS need_fulfilled_with_shopping_list,
rp.qu_id,
(CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END) * rp.amount * pop.price * rp.price_factor AS costs,
CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos,
rp.ingredient_group,
pg.name as product_group,
rp.id, -- Just a dummy id column
r.type as recipe_type,
rnr.includes_recipe_id as child_recipe_id,
rp.note,
rp.variable_amount AS recipe_variable_amount,
rp.only_check_single_unit_in_stock,
rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) * IFNULL(p.calories, 0) AS calories,
p.active AS product_active
FROM recipes r
JOIN recipes_nestings_resolved rnr
ON r.id = rnr.recipe_id
JOIN recipes rnrr
ON rnr.includes_recipe_id = rnrr.id
JOIN recipes_pos rp
ON rnr.includes_recipe_id = rp.recipe_id
JOIN products p
ON rp.product_id = p.id
LEFT JOIN product_groups pg
ON p.product_group_id = pg.id
LEFT JOIN (
SELECT product_id, SUM(amount) AS amount
FROM shopping_list
GROUP BY product_id) sl
ON rp.product_id = sl.product_id
LEFT JOIN stock_current sc
ON rp.product_id = sc.product_id
LEFT JOIN products_oldest_stock_unit_price pop
ON rp.product_id = pop.product_id
WHERE rp.not_check_stock_fulfillment = 0
UNION
-- Just add all recipe positions which should not be checked against stock with fulfilled need
SELECT
r.id AS recipe_id,
rp.id AS recipe_pos_id,
rp.product_id AS product_id,
rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) AS recipe_amount,
IFNULL(sc.amount_aggregated, 0) AS stock_amount,
1 AS need_fulfilled,
0 AS missing_amount,
IFNULL(sl.amount, 0) AS amount_on_shopping_list,
1 AS need_fulfilled_with_shopping_list,
rp.qu_id,
(CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END) * rp.amount * IFNULL(pop.price, 0) * rp.price_factor AS costs,
CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos,
rp.ingredient_group,
pg.name as product_group,
rp.id, -- Just a dummy id column
r.type as recipe_type,
rnr.includes_recipe_id as child_recipe_id,
rp.note,
rp.variable_amount AS recipe_variable_amount,
rp.only_check_single_unit_in_stock,
rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) * IFNULL(p.calories, 0) AS calories,
p.active AS product_active
FROM recipes r
JOIN recipes_nestings_resolved rnr
ON r.id = rnr.recipe_id
JOIN recipes rnrr
ON rnr.includes_recipe_id = rnrr.id
JOIN recipes_pos rp
ON rnr.includes_recipe_id = rp.recipe_id
JOIN products p
ON rp.product_id = p.id
LEFT JOIN product_groups pg
ON p.product_group_id = pg.id
LEFT JOIN (
SELECT product_id, SUM(amount) AS amount
FROM shopping_list
GROUP BY product_id) sl
ON rp.product_id = sl.product_id
LEFT JOIN stock_current sc
ON rp.product_id = sc.product_id
LEFT JOIN products_oldest_stock_unit_price pop
ON rp.product_id = pop.product_id
WHERE rp.not_check_stock_fulfillment = 1;

View File

@ -219,9 +219,9 @@ var calendar = $("#calendar").fullCalendar({
<h5 class="small timeago-contextual text-truncate">' + fulfillmentIconHtml + " " + fulfillmentInfoHtml + '</h5> \ <h5 class="small timeago-contextual text-truncate">' + fulfillmentIconHtml + " " + fulfillmentInfoHtml + '</h5> \
' + costsAndCaloriesPerServing + ' \ ' + costsAndCaloriesPerServing + ' \
<h5> \ <h5> \
<a class="ml-1 btn btn-outline-danger btn-xs remove-product-button" href="#"><i class="fas fa-trash"></i></a> \ <a class="ml-1 btn btn-outline-danger btn-xs remove-product-button" href="#" data-toggle="tooltip" title="' + __t("Delete this item") + '"><i class="fas fa-trash"></i></a> \
<a class="ml-1 btn btn-outline-info btn-xs edit-meal-plan-entry-button" href="#"><i class="fas fa-edit"></i></a> \ <a class="ml-1 btn btn-outline-info btn-xs edit-meal-plan-entry-button" href="#" data-toggle="tooltip" title="' + __t("Edit this item") + '"><i class="fas fa-edit"></i></a> \
<a class="ml-1 btn btn-outline-success btn-xs product-consume-button ' + productConsumeButtonDisabledClasses + '" href="#" data-toggle="tooltip" title="' + __t("Consume %1$s of %2$s", parseFloat(mealPlanEntry.product_amount).toLocaleString() + ' ' + __n(mealPlanEntry.product_amount, productDetails.quantity_unit_purchase.name, productDetails.quantity_unit_purchase.name_plural), productDetails.product.name) + '" data-product-id="' + productDetails.product.id.toString() + '" data-product-name="' + productDetails.product.name + '" data-product-amount="' + mealPlanEntry.product_amount + '"><i class="fas fa-utensils"></i></a> \ <a class="ml-1 btn btn-outline-success btn-xs product-consume-button ' + productConsumeButtonDisabledClasses + '" href="#" data-toggle="tooltip" title="' + __t("Consume %1$s of %2$s", parseFloat(mealPlanEntry.product_amount).toLocaleString() + ' ' + __n(mealPlanEntry.product_amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '" data-product-id="' + productDetails.product.id.toString() + '" data-product-name="' + productDetails.product.name + '" data-product-amount="' + mealPlanEntry.product_amount + '"><i class="fas fa-utensils"></i></a> \
<a class="ml-1 btn btn-outline-primary btn-xs show-as-dialog-link ' + productOrderMissingButtonDisabledClasses + '" href="' + U("/shoppinglistitem/new?embedded&updateexistingproduct&product=") + mealPlanEntry.product_id + '&amount=' + mealPlanEntry.product_amount + '" data-toggle="tooltip" title="' + __t("Add to shopping list") + '" data-product-id="' + productDetails.product.id.toString() + '" data-product-name="' + productDetails.product.name + '" data-product-amount="' + mealPlanEntry.product_amount + '"><i class="fas fa-cart-plus"></i></a> \ <a class="ml-1 btn btn-outline-primary btn-xs show-as-dialog-link ' + productOrderMissingButtonDisabledClasses + '" href="' + U("/shoppinglistitem/new?embedded&updateexistingproduct&product=") + mealPlanEntry.product_id + '&amount=' + mealPlanEntry.product_amount + '" data-toggle="tooltip" title="' + __t("Add to shopping list") + '" data-product-id="' + productDetails.product.id.toString() + '" data-product-name="' + productDetails.product.name + '" data-product-amount="' + mealPlanEntry.product_amount + '"><i class="fas fa-cart-plus"></i></a> \
</h5> \ </h5> \
</div>'); </div>');
@ -258,8 +258,8 @@ var calendar = $("#calendar").fullCalendar({
<div> \ <div> \
<h5 class="text-wrap text-break">' + mealPlanEntry.note + '<h5> \ <h5 class="text-wrap text-break">' + mealPlanEntry.note + '<h5> \
<h5> \ <h5> \
<a class="ml-1 btn btn-outline-danger btn-xs remove-note-button" href="#"><i class="fas fa-trash"></i></a> \ <a class="ml-1 btn btn-outline-danger btn-xs remove-note-button" href="#" data-toggle="tooltip" title="' + __t("Delete this item") + '"><i class="fas fa-trash"></i></a> \
<a class="ml-1 btn btn-outline-info btn-xs edit-meal-plan-entry-button" href="#"><i class="fas fa-edit"></i></a> \ <a class="ml-1 btn btn-outline-info btn-xs edit-meal-plan-entry-button" href="#" data-toggle="tooltip" title="' + __t("Delete this item") + '"><i class="fas fa-edit"></i></a> \
</h5> \ </h5> \
</div>'); </div>');
} }

View File

@ -163,13 +163,14 @@ if (Grocy.Components.ProductPicker !== undefined)
function(productDetails) function(productDetails)
{ {
Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_stock.id); Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_stock.id);
Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.quantity_unit_purchase.id); Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.default_quantity_unit_purchase.id);
$('#display_amount').val(parseFloat(Grocy.UserSettings.stock_default_purchase_amount).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts })); $('#display_amount').val(parseFloat(Grocy.UserSettings.stock_default_purchase_amount).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }));
$(".input-group-productamountpicker").trigger("change"); $(".input-group-productamountpicker").trigger("change");
if (GetUriParam("flow") === "shoppinglistitemtostock") if (GetUriParam("flow") === "shoppinglistitemtostock")
{ {
$('#display_amount').val(parseFloat(GetUriParam("amount")).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts })); Grocy.Components.ProductAmountPicker.SetQuantityUnit(GetUriParam("quId"));
$('#display_amount').val(parseFloat(GetUriParam("amount") * $("#qu_id option:selected").attr("data-qu-factor")).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }));
} }
$(".input-group-productamountpicker").trigger("change"); $(".input-group-productamountpicker").trigger("change");
@ -282,7 +283,6 @@ if (Grocy.Components.ProductPicker !== undefined)
if (barcode.amount != null) if (barcode.amount != null)
{ {
$("#display_amount").val(barcode.amount); $("#display_amount").val(barcode.amount);
$(".input-group-productamountpicker").trigger("change");
$("#display_amount").select(); $("#display_amount").select();
} }
@ -296,6 +296,7 @@ if (Grocy.Components.ProductPicker !== undefined)
Grocy.Components.ShoppingLocationPicker.SetId(barcode.shopping_location_id); Grocy.Components.ShoppingLocationPicker.SetId(barcode.shopping_location_id);
} }
$(".input-group-productamountpicker").trigger("change");
Grocy.FrontendHelpers.ValidateForm('purchase-form'); Grocy.FrontendHelpers.ValidateForm('purchase-form');
} }
} }

View File

@ -1,10 +1,16 @@
$('#save-shoppinglist-button').on('click', function(e) Grocy.ShoppingListItemFormInitialLoadDone = false;
$('#save-shoppinglist-button').on('click', function(e)
{ {
e.preventDefault(); e.preventDefault();
var jsonData = $('#shoppinglist-form').serializeJSON(); var jsonData = $('#shoppinglist-form').serializeJSON();
if (!jsonData.product_id)
{
jsonData.amount = jsonData.display_amount;
}
delete jsonData.display_amount; delete jsonData.display_amount;
delete jsonData.qu_id;
Grocy.FrontendHelpers.BeginUiBusy("shoppinglist-form"); Grocy.FrontendHelpers.BeginUiBusy("shoppinglist-form");
if (GetUriParam("updateexistingproduct") !== undefined) if (GetUriParam("updateexistingproduct") !== undefined)
@ -20,7 +26,7 @@
Grocy.Api.Get('stock/products/' + jsonData.product_id, Grocy.Api.Get('stock/products/' + jsonData.product_id,
function(productDetails) function(productDetails)
{ {
window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", __t("Added %1$s of %2$s to the shopping list \"%3$s\"", jsonData.product_amount + " " + __n(jsonData.product_amount, productDetails.quantity_unit_purchase.name, productDetails.quantity_unit_purchase.name_plural), productDetails.product.name, $("#shopping_list_id option:selected").text())), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", __t("Added %1$s of %2$s to the shopping list \"%3$s\"", jsonData.product_amount + " " + __n(jsonData.product_amount, productDetails.default_quantity_unit_purchase.name, productDetails.default_.name_plural), productDetails.product.name, $("#shopping_list_id option:selected").text())), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("ShoppingListChanged", $("#shopping_list_id").val().toString()), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ShoppingListChanged", $("#shopping_list_id").val().toString()), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
}, },
@ -49,11 +55,13 @@
function(result) function(result)
{ {
if (GetUriParam("embedded") !== undefined) if (GetUriParam("embedded") !== undefined)
{
if (jsonData.product_id)
{ {
Grocy.Api.Get('stock/products/' + jsonData.product_id, Grocy.Api.Get('stock/products/' + jsonData.product_id,
function(productDetails) function(productDetails)
{ {
window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", __t("Added %1$s of %2$s to the shopping list \"%3$s\"", jsonData.amount + " " + __n(jsonData.amount, productDetails.quantity_unit_purchase.name, productDetails.quantity_unit_purchase.name_plural), productDetails.product.name, $("#shopping_list_id option:selected").text())), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", __t("Added %1$s of %2$s to the shopping list \"%3$s\"", jsonData.amount + " " + __n(jsonData.amount, productDetails.default_quantity_unit_purchase.name, productDetails.default_quantity_unit_purchase.name_plural), productDetails.product.name, $("#shopping_list_id option:selected").text())), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("ShoppingListChanged", $("#shopping_list_id").val().toString()), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ShoppingListChanged", $("#shopping_list_id").val().toString()), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
}, },
@ -64,6 +72,12 @@
); );
} }
else else
{
window.parent.postMessage(WindowMessageBag("ShoppingListChanged", $("#shopping_list_id").val().toString()), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
}
}
else
{ {
window.location.href = U('/shoppinglist?list=' + $("#shopping_list_id").val().toString()); window.location.href = U('/shoppinglist?list=' + $("#shopping_list_id").val().toString());
} }
@ -81,11 +95,13 @@
function(result) function(result)
{ {
if (GetUriParam("embedded") !== undefined) if (GetUriParam("embedded") !== undefined)
{
if (jsonData.product_id)
{ {
Grocy.Api.Get('stock/products/' + jsonData.product_id, Grocy.Api.Get('stock/products/' + jsonData.product_id,
function(productDetails) function(productDetails)
{ {
window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", __t("Added %1$s of %2$s to the shopping list \"%3$s\"", jsonData.amount + " " + __n(jsonData.amount, productDetails.quantity_unit_purchase.name, productDetails.quantity_unit_purchase.name_plural), productDetails.product.name, $("#shopping_list_id option:selected").text())), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", __t("Added %1$s of %2$s to the shopping list \"%3$s\"", jsonData.amount + " " + __n(jsonData.amount, productDetails.default_quantity_unit_purchase.name, productDetails.default_quantity_unit_purchase.name_plural), productDetails.product.name, $("#shopping_list_id option:selected").text())), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("ShoppingListChanged", $("#shopping_list_id").val().toString()), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ShoppingListChanged", $("#shopping_list_id").val().toString()), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
}, },
@ -96,6 +112,12 @@
); );
} }
else else
{
window.parent.postMessage(WindowMessageBag("ShoppingListChanged", $("#shopping_list_id").val().toString()), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
}
}
else
{ {
window.location.href = U('/shoppinglist?list=' + $("#shopping_list_id").val().toString()); window.location.href = U('/shoppinglist?list=' + $("#shopping_list_id").val().toString());
} }
@ -120,11 +142,19 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
Grocy.Api.Get('stock/products/' + productId, Grocy.Api.Get('stock/products/' + productId,
function(productDetails) function(productDetails)
{ {
Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_purchase.id); if (!Grocy.ShoppingListItemFormInitialLoadDone)
Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.quantity_unit_purchase.id); {
Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_stock.id, true);
}
else
{
Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_stock.id);
Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.default_quantity_unit_purchase.id);
}
$('#display_amount').focus(); $('#display_amount').focus();
Grocy.FrontendHelpers.ValidateForm('shoppinglist-form'); Grocy.FrontendHelpers.ValidateForm('shoppinglist-form');
Grocy.ShoppingListItemFormInitialLoadDone = true;
}, },
function(xhr) function(xhr)
{ {
@ -136,13 +166,17 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
Grocy.FrontendHelpers.ValidateForm('shoppinglist-form'); Grocy.FrontendHelpers.ValidateForm('shoppinglist-form');
Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.Components.ProductPicker.GetInputElement().focus();
Grocy.Components.ProductPicker.GetPicker().trigger('change');
if (Grocy.EditMode === "edit") if (Grocy.EditMode === "edit")
{ {
Grocy.Components.ProductPicker.GetPicker().trigger('change'); Grocy.Components.ProductPicker.GetPicker().trigger('change');
} }
if (Grocy.EditMode == "create")
{
Grocy.ShoppingListItemFormInitialLoadDone = true;
}
$('#display_amount').on('focus', function(e) $('#display_amount').on('focus', function(e)
{ {
$(this).select(); $(this).select();

View File

@ -24,11 +24,11 @@ class RecipesService extends BaseService
{ {
$product = $this->getDataBase()->products($recipePosition->product_id); $product = $this->getDataBase()->products($recipePosition->product_id);
$toOrderAmount = round(($recipePosition->missing_amount - $recipePosition->amount_on_shopping_list) / $product->qu_factor_purchase_to_stock, 2); $toOrderAmount = round(($recipePosition->missing_amount - $recipePosition->amount_on_shopping_list), 2);
if ($recipe->not_check_shoppinglist == 1) if ($recipe->not_check_shoppinglist == 1)
{ {
$toOrderAmount = round($recipePosition->missing_amount / $product->qu_factor_purchase_to_stock, 2); $toOrderAmount = round($recipePosition->missing_amount, 2);
} }
if ($toOrderAmount > 0) if ($toOrderAmount > 0)
@ -63,7 +63,6 @@ class RecipesService extends BaseService
} }
$recipeRow = $this->getDatabase()->recipes()->where('id = :1', $recipeId)->fetch(); $recipeRow = $this->getDatabase()->recipes()->where('id = :1', $recipeId)->fetch();
if (!empty($recipeRow->product_id)) if (!empty($recipeRow->product_id))
{ {
$recipeResolvedRow = $this->getDatabase()->recipes_resolved()->where('recipe_id = :1', $recipeId)->fetch(); $recipeResolvedRow = $this->getDatabase()->recipes_resolved()->where('recipe_id = :1', $recipeId)->fetch();

View File

@ -26,7 +26,7 @@ class StockService extends BaseService
foreach ($missingProducts as $missingProduct) foreach ($missingProducts as $missingProduct)
{ {
$product = $this->getDatabase()->products()->where('id', $missingProduct->id)->fetch(); $product = $this->getDatabase()->products()->where('id', $missingProduct->id)->fetch();
$amountToAdd = round($missingProduct->amount_missing / $product->qu_factor_purchase_to_stock, 2); $amountToAdd = round($missingProduct->amount_missing, 2);
$alreadyExistingEntry = $this->getDatabase()->shopping_list()->where('product_id', $missingProduct->id)->fetch(); $alreadyExistingEntry = $this->getDatabase()->shopping_list()->where('product_id', $missingProduct->id)->fetch();
@ -577,7 +577,7 @@ class StockService extends BaseService
'stock_amount_opened' => $stockCurrentRow->amount_opened, 'stock_amount_opened' => $stockCurrentRow->amount_opened,
'stock_amount_aggregated' => $stockCurrentRow->amount_aggregated, 'stock_amount_aggregated' => $stockCurrentRow->amount_aggregated,
'stock_amount_opened_aggregated' => $stockCurrentRow->amount_opened_aggregated, 'stock_amount_opened_aggregated' => $stockCurrentRow->amount_opened_aggregated,
'quantity_unit_purchase' => $quPurchase, 'default_quantity_unit_purchase' => $quPurchase,
'quantity_unit_stock' => $quStock, 'quantity_unit_stock' => $quStock,
'last_price' => $lastPrice, 'last_price' => $lastPrice,
'avg_price' => $avgPrice, 'avg_price' => $avgPrice,

View File

@ -201,22 +201,6 @@
</select> </select>
</div> </div>
<div class="form-group">
<label for="qu_id_purchase">{{ $__t('Quantity unit purchase') }}</label>
<select required
class="form-control input-group-qu"
id="qu_id_purchase"
name="qu_id_purchase">
<option></option>
@foreach($quantityunits as $quantityunit)
<option @if($mode=='edit'
&&
$quantityunit->id == $product->qu_id_purchase) selected="selected" @endif value="{{ $quantityunit->id }}">{{ $quantityunit->name }}</option>
@endforeach
</select>
<div class="invalid-feedback">{{ $__t('A quantity unit is required') }}</div>
</div>
<div class="form-group"> <div class="form-group">
<label for="qu_id_stock">{{ $__t('Quantity unit stock') }}</label> <label for="qu_id_stock">{{ $__t('Quantity unit stock') }}</label>
<i class="fas fa-question-circle" <i class="fas fa-question-circle"
@ -240,6 +224,25 @@
<div class="invalid-feedback">{{ $__t('A quantity unit is required') }}</div> <div class="invalid-feedback">{{ $__t('A quantity unit is required') }}</div>
</div> </div>
<div class="form-group">
<label for="qu_id_purchase">{{ $__t('Default quantity unit purchase') }}</label>
<i class="fas fa-question-circle"
data-toggle="tooltip"
title="{{ $__t('This is the default quantity unit used when adding a product to the shopping list') }}"></i>
<select required
class="form-control input-group-qu"
id="qu_id_purchase"
name="qu_id_purchase">
<option></option>
@foreach($quantityunits as $quantityunit)
<option @if($mode=='edit'
&&
$quantityunit->id == $product->qu_id_purchase) selected="selected" @endif value="{{ $quantityunit->id }}">{{ $quantityunit->name }}</option>
@endforeach
</select>
<div class="invalid-feedback">{{ $__t('A quantity unit is required') }}</div>
</div>
@php if($mode == 'edit') { $value = $product->qu_factor_purchase_to_stock; } else { $value = 1; } @endphp @php if($mode == 'edit') { $value = $product->qu_factor_purchase_to_stock; } else { $value = 1; } @endphp
@include('components.numberpicker', array( @include('components.numberpicker', array(
'id' => 'qu_factor_purchase_to_stock', 'id' => 'qu_factor_purchase_to_stock',

View File

@ -98,8 +98,8 @@
<th>{{ $__t('Name') }}</th> <th>{{ $__t('Name') }}</th>
<th class="@if(!GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) d-none @endif">{{ $__t('Location') }}</th> <th class="@if(!GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) d-none @endif">{{ $__t('Location') }}</th>
<th>{{ $__t('Min. stock amount') }}</th> <th>{{ $__t('Min. stock amount') }}</th>
<th>{{ $__t('QU purchase') }}</th> <th>{{ $__t('Default quantity unit purchase') }}</th>
<th>{{ $__t('QU stock') }}</th> <th>{{ $__t('Quantity unit stock') }}</th>
<th>{{ $__t('Product group') }}</th> <th>{{ $__t('Product group') }}</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', array(

View File

@ -223,8 +223,8 @@
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
<a class="btn btn-sm btn-primary @if(!GROCY_FEATURE_FLAG_STOCK) d-none @endif @if(empty($listItem->product_id)) disabled @else shopping-list-stock-add-workflow-list-item-button @endif" <a class="btn btn-sm btn-primary @if(!GROCY_FEATURE_FLAG_STOCK) d-none @endif @if(empty($listItem->product_id)) disabled @else shopping-list-stock-add-workflow-list-item-button @endif"
href="{{ $U('/purchase?embedded&flow=shoppinglistitemtostock&product=') }}{{ $listItem->product_id }}&amount={{ $listItem->amount }}&listitemid={{ $listItem->id }}" href="{{ $U('/purchase?embedded&flow=shoppinglistitemtostock&product=') }}{{ $listItem->product_id }}&amount={{ $listItem->amount }}&listitemid={{ $listItem->id }}&quId={{ $listItem->qu_id }}"
@if(!empty($listItem->product_id)) data-toggle="tooltip" title="{{ $__t('Add %1$s of %2$s to stock', $listItem->amount . ' ' . $__n($listItem->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->qu_id_purchase)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->qu_id_purchase)->name_plural), FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->name, $listItem->amount) }}" @endif> @if(!empty($listItem->product_id)) data-toggle="tooltip" title="{{ $__t('Add %1$s of %2$s to stock', $listItem->amount . ' ' . $__n($listItem->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', $listItem->qu_id)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', $listItem->qu_id)->name_plural), FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->name, $listItem->amount) }}" @endif>
<i class="fas fa-box"></i> <i class="fas fa-box"></i>
</a> </a>
</td> </td>
@ -233,7 +233,20 @@
@if(!empty($listItem->product_id)) {{ FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->name }}<br>@endif<em>{!! nl2br($listItem->note) !!}</em> @if(!empty($listItem->product_id)) {{ FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->name }}<br>@endif<em>{!! nl2br($listItem->note) !!}</em>
</td> </td>
<td> <td>
<span class="locale-number locale-number-quantity-amount">{{ $listItem->amount }}</span> @if(!empty($listItem->product_id)){{ $__n($listItem->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->qu_id_purchase)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->qu_id_purchase)->name_plural) }}@endif @if(!empty($listItem->product_id))
@php
$listItem->amount_origin_qu = $listItem->amount;
$product = FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id);
$productQuConversions = FindAllObjectsInArrayByPropertyValue($quantityUnitConversionsResolved, 'product_id', $product->id);
$productQuConversions = FindAllObjectsInArrayByPropertyValue($productQuConversions, 'from_qu_id', $product->qu_id_stock);
$productQuConversion = FindObjectInArrayByPropertyValue($productQuConversions, 'to_qu_id', $listItem->qu_id);
if ($productQuConversion)
{
$listItem->amount = $listItem->amount * $productQuConversion->factor;
}
@endphp
@endif
<span class="locale-number locale-number-quantity-amount">{{ $listItem->amount }}</span> @if(!empty($listItem->product_id)){{ $__n($listItem->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', $listItem->qu_id)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', $listItem->qu_id)->name_plural) }}@endif
</td> </td>
<td class="d-none"> <td class="d-none">
@if(!empty(FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->product_group_id)) {{ FindObjectInArrayByPropertyValue($productGroups, 'id', FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->product_group_id)->name }} @else <span class="font-italic font-weight-light">{{ $__t('Ungrouped') }}</span> @endif @if(!empty(FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->product_group_id)) {{ FindObjectInArrayByPropertyValue($productGroups, 'id', FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->product_group_id)->name }} @else <span class="font-italic font-weight-light">{{ $__t('Ungrouped') }}</span> @endif
@ -339,7 +352,7 @@
@if(!empty($listItem->product_id)) {{ FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->name }}<br>@endif<em>{!! nl2br($listItem->note) !!}</em> @if(!empty($listItem->product_id)) {{ FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->name }}<br>@endif<em>{!! nl2br($listItem->note) !!}</em>
</td> </td>
<td> <td>
{{ $listItem->amount }} @if(!empty($listItem->product_id)){{ $__n($listItem->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->qu_id_purchase)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->qu_id_purchase)->name_plural) }}@endif <span class="locale-number locale-number-quantity-amount">{{ $listItem->amount }}</span> @if(!empty($listItem->product_id)){{ $__n($listItem->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', $listItem->qu_id)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', $listItem->qu_id)->name_plural) }}@endif
</td> </td>
@include('components.userfields_tbody', array( @include('components.userfields_tbody', array(

View File

@ -66,11 +66,15 @@
'prefillById' => $productId 'prefillById' => $productId
)) ))
</div> </div>
@php if($mode == 'edit') { $value = $listItem->amount; } else { $value = 1; } @endphp @php if($mode == 'edit') { $value = $listItem->amount; } else { $value = 1; } @endphp
@php if($mode == 'edit') { $initialQuId = $listItem->qu_id; } else { $initialQuId = ''; } @endphp
@include('components.productamountpicker', array( @include('components.productamountpicker', array(
'value' => $value, 'value' => $value,
'initialQuId' => $initialQuId,
'min' => '0.' . str_repeat('0', $userSettings['stock_decimal_places_amounts'] - 1) . '1', 'min' => '0.' . str_repeat('0', $userSettings['stock_decimal_places_amounts'] - 1) . '1',
'invalidFeedback' => $__t('The amount cannot be lower than %s', '0.' . str_repeat('0', $userSettings['stock_decimal_places_amounts'] - 1) . '1') 'invalidFeedback' => $__t('The amount cannot be lower than %s', '0.' . str_repeat('0', $userSettings['stock_decimal_places_amounts'] - 1) . '1'),
'isRequired' => false
)) ))
<div class="form-group"> <div class="form-group">