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
- 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
_- (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
- 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)
### API improvements/fixes
- Breaking changes:
- All prices are now related to the products **stock** quantity unit (instead of the purchase QU)
- **Breaking changes**:
- 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)
- 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>`

View File

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

View File

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

View File

@ -181,15 +181,6 @@ msgstr ""
msgid "Min. stock amount"
msgstr ""
msgid "QU purchase"
msgstr ""
msgid "QU stock"
msgstr ""
msgid "QU factor"
msgstr ""
msgid "Description"
msgstr ""
@ -205,7 +196,7 @@ msgstr ""
msgid "Default best before days"
msgstr ""
msgid "Quantity unit purchase"
msgid "Default quantity unit purchase"
msgstr ""
msgid "Quantity unit stock"
@ -1930,3 +1921,6 @@ msgstr ""
msgid "Show a QR-Code for this API key"
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> \
' + costsAndCaloriesPerServing + ' \
<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-info btn-xs edit-meal-plan-entry-button" href="#"><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-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="#" 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_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> \
</h5> \
</div>');
@ -258,8 +258,8 @@ var calendar = $("#calendar").fullCalendar({
<div> \
<h5 class="text-wrap text-break">' + mealPlanEntry.note + '<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-info btn-xs edit-meal-plan-entry-button" href="#"><i class="fas fa-edit"></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="#" data-toggle="tooltip" title="' + __t("Delete this item") + '"><i class="fas fa-edit"></i></a> \
</h5> \
</div>');
}

View File

@ -163,13 +163,14 @@ if (Grocy.Components.ProductPicker !== undefined)
function(productDetails)
{
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 }));
$(".input-group-productamountpicker").trigger("change");
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");
@ -282,7 +283,6 @@ if (Grocy.Components.ProductPicker !== undefined)
if (barcode.amount != null)
{
$("#display_amount").val(barcode.amount);
$(".input-group-productamountpicker").trigger("change");
$("#display_amount").select();
}
@ -296,6 +296,7 @@ if (Grocy.Components.ProductPicker !== undefined)
Grocy.Components.ShoppingLocationPicker.SetId(barcode.shopping_location_id);
}
$(".input-group-productamountpicker").trigger("change");
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();
var jsonData = $('#shoppinglist-form').serializeJSON();
if (!jsonData.product_id)
{
jsonData.amount = jsonData.display_amount;
}
delete jsonData.display_amount;
delete jsonData.qu_id;
Grocy.FrontendHelpers.BeginUiBusy("shoppinglist-form");
if (GetUriParam("updateexistingproduct") !== undefined)
@ -20,7 +26,7 @@
Grocy.Api.Get('stock/products/' + jsonData.product_id,
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("CloseAllModals"), Grocy.BaseUrl);
},
@ -50,18 +56,26 @@
{
if (GetUriParam("embedded") !== undefined)
{
Grocy.Api.Get('stock/products/' + jsonData.product_id,
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("ShoppingListChanged", $("#shopping_list_id").val().toString()), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
},
function(xhr)
{
console.error(xhr);
}
);
if (jsonData.product_id)
{
Grocy.Api.Get('stock/products/' + jsonData.product_id,
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.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("CloseAllModals"), Grocy.BaseUrl);
},
function(xhr)
{
console.error(xhr);
}
);
}
else
{
window.parent.postMessage(WindowMessageBag("ShoppingListChanged", $("#shopping_list_id").val().toString()), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
}
}
else
{
@ -82,18 +96,26 @@
{
if (GetUriParam("embedded") !== undefined)
{
Grocy.Api.Get('stock/products/' + jsonData.product_id,
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("ShoppingListChanged", $("#shopping_list_id").val().toString()), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
},
function(xhr)
{
console.error(xhr);
}
);
if (jsonData.product_id)
{
Grocy.Api.Get('stock/products/' + jsonData.product_id,
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.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("CloseAllModals"), Grocy.BaseUrl);
},
function(xhr)
{
console.error(xhr);
}
);
}
else
{
window.parent.postMessage(WindowMessageBag("ShoppingListChanged", $("#shopping_list_id").val().toString()), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
}
}
else
{
@ -120,11 +142,19 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
Grocy.Api.Get('stock/products/' + productId,
function(productDetails)
{
Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_purchase.id);
Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.quantity_unit_purchase.id);
if (!Grocy.ShoppingListItemFormInitialLoadDone)
{
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();
Grocy.FrontendHelpers.ValidateForm('shoppinglist-form');
Grocy.ShoppingListItemFormInitialLoadDone = true;
},
function(xhr)
{
@ -136,13 +166,17 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
Grocy.FrontendHelpers.ValidateForm('shoppinglist-form');
Grocy.Components.ProductPicker.GetInputElement().focus();
Grocy.Components.ProductPicker.GetPicker().trigger('change');
if (Grocy.EditMode === "edit")
{
Grocy.Components.ProductPicker.GetPicker().trigger('change');
}
if (Grocy.EditMode == "create")
{
Grocy.ShoppingListItemFormInitialLoadDone = true;
}
$('#display_amount').on('focus', function(e)
{
$(this).select();

View File

@ -24,11 +24,11 @@ class RecipesService extends BaseService
{
$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)
{
$toOrderAmount = round($recipePosition->missing_amount / $product->qu_factor_purchase_to_stock, 2);
$toOrderAmount = round($recipePosition->missing_amount, 2);
}
if ($toOrderAmount > 0)
@ -63,7 +63,6 @@ class RecipesService extends BaseService
}
$recipeRow = $this->getDatabase()->recipes()->where('id = :1', $recipeId)->fetch();
if (!empty($recipeRow->product_id))
{
$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)
{
$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();
@ -577,7 +577,7 @@ class StockService extends BaseService
'stock_amount_opened' => $stockCurrentRow->amount_opened,
'stock_amount_aggregated' => $stockCurrentRow->amount_aggregated,
'stock_amount_opened_aggregated' => $stockCurrentRow->amount_opened_aggregated,
'quantity_unit_purchase' => $quPurchase,
'default_quantity_unit_purchase' => $quPurchase,
'quantity_unit_stock' => $quStock,
'last_price' => $lastPrice,
'avg_price' => $avgPrice,

View File

@ -201,22 +201,6 @@
</select>
</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">
<label for="qu_id_stock">{{ $__t('Quantity unit stock') }}</label>
<i class="fas fa-question-circle"
@ -240,6 +224,25 @@
<div class="invalid-feedback">{{ $__t('A quantity unit is required') }}</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
@include('components.numberpicker', array(
'id' => 'qu_factor_purchase_to_stock',

View File

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

View File

@ -223,8 +223,8 @@
<i class="fas fa-trash"></i>
</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"
href="{{ $U('/purchase?embedded&flow=shoppinglistitemtostock&product=') }}{{ $listItem->product_id }}&amount={{ $listItem->amount }}&listitemid={{ $listItem->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>
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', $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>
</a>
</td>
@ -233,7 +233,20 @@
@if(!empty($listItem->product_id)) {{ FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->name }}<br>@endif<em>{!! nl2br($listItem->note) !!}</em>
</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 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
@ -339,7 +352,7 @@
@if(!empty($listItem->product_id)) {{ FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->name }}<br>@endif<em>{!! nl2br($listItem->note) !!}</em>
</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>
@include('components.userfields_tbody', array(

View File

@ -66,11 +66,15 @@
'prefillById' => $productId
))
</div>
@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(
'value' => $value,
'initialQuId' => $initialQuId,
'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">