Squashed commit

Fixed some localization strings
Reviewed/optimized product deletion handling
Add option to hide products from the stock overview page (closes #906)
Prefill default_due_days also on the inventory page (closes #591)
Added DataTables accent chinese-string plugin (closes #872)
Show costs and calories per recipe ingredient (closes #1072)
Fixed user permission saving (fixes #1099)
User permissions should not have an effect for demo mode (closes #972)
Handle QU conversion when consuming a substituation (child) product (fixes #1118)
Consume/open any child product when the parent product is not in stock (closes #899)
Added a retry camera barcode scanning button to product picker workflow (closes #736)
This commit is contained in:
Bernd Bestel 2020-12-07 19:48:33 +01:00
parent 2bdb6ab2d4
commit cf34df5e3f
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
53 changed files with 387 additions and 210 deletions

View File

@ -56,7 +56,7 @@
- Various display/CSS improvements (thanks @Mik-)
- Prerequisites (PHP extensions, critical files/folders) will now be checked and properly reported if there are problems (thanks @Forceu)
- Improved the the overview pages on mobile devices (main column was hidden) (thanks @Mik-)
- The general search field now searches accent insensitive
- The general search field now searches accent insensitive (and table sorting is also accent insensitive)
- Fixed that all number inputs are always prefilled in the browser locale number format
- Optimized the handling of settings provided by `data/settingoverrides` files (thanks @dacto)
- Optimized the update script (`update.sh`) to create the backup tar archive before writing to it (was a problem on Btrfs file systems) (thanks @shane-kerr)

View File

@ -46,10 +46,13 @@
- The amount to be used for the "quick consume/open buttons" on the stock overview page can now be configured per product (new product option "Quick consume amount", defaults to 1)
- This "Quick consume amount" can optionally also be used as the default on the consume page (new stock setting / top right corner settings menu)
- Products can now be duplicated (new button on the products list page, all fields will be preset from the copied product, except the name)
- When consuming or opening a parent product, which is currently not in stock, any in-stock sub product will now be consumed/opened (like already automatically done when consuming recipes)
- Optimized/clarified what the total/unit price is on the purchase page (thanks @kriddles)
- On the purchase page the amount field is now displayed above/before the due date for better `TAB` handling (thanks @kriddles)
- Changed that when `FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING` is disabled, products now get internally a due date of "never overdue" (aka `2999-12-31`) instead of today (thanks @kriddles)
- Products can now be hidden instead of deleted to prevent problems / missing information on existing references (new checkbox on the product edit page) (thanks @kriddles)
- Products can now be disabled to keep the history/journal, but hide it everywhere, without deleting it (new product option "Active", deleting a product now explicitly also deletes its journal and all other references) (thanks @kriddles for the initial work on this)
- Products can now be hidden from the stock overview page (new product option "Show on stock overview page", enabled by default, so no changed behavior when not configured)
- The due date is now also prefilled on the inventory page based on the products "Default due days" (was only done on the purchase page before)
- On the stock journal page, it's now visible if a consume-booking was spoiled
- It's now tracked who made a stock change (currently logged in user, visible on the stock journal page) (thanks @fipwmaqzufheoxq92ebc)
- Product edit page improvements ("Save & continue" button, deleting and adding a product picuture is now possible in one go) (thanks @Ma27)
@ -60,6 +63,7 @@
- When clicking the product name on the shopping list, the product card will now be displayed (like on the stock overview page) (thanks @kriddles)
- On the product card there is now also a button to jump directly to the stock entries of the corresponding product (thanks @kriddles)
- The product picker workflows can now also be started by `ENTER` (additionally to `TAB`)
- Added a "retry camera barcode scan" button (button with camera icon, shortcut `C`) to the product picker workflow dialog
- Added more filters on the stock journal page
- Added a grouped/summarized stock journal (new button "Journal summary" at the top of the stock journal page) (thanks @fipwmaqzufheoxq92ebc)
- Provides an overview of summarized transactions per product, transaction type and user + summarized amount
@ -91,12 +95,14 @@
- Changed that recipe costs are now based on the costs of the products picked by the default consume rule "First due first, then first in first out" (thanks @kriddles)
- Recipe costs were based on the last purchase price per product before, so this now better reflects the current real costs
- Improved the recipe add workflow (a recipe called "New recipe" is now not automatically created when starting to add a recipe) (thanks @zsarnett)
- On the recipe page, the calories and costs per ingredient are now shown to get a better overview of how much each ingredient contributed
- Fixed that images on the recipe gallery view were not scaled correctly on larger screens (thanks @zsarnett)
- Fixed that decimal ingredient amounts maybe resulted in wrong conversions or truncated decimal places if your locale does not use a dot as the decimal separator (thanks @m-byte)
- Fixed that a recipe cannot be included in itself (because this will cause an infinite loop) (thanks @fipwmaqzufheoxq92ebc)
- Fixed that when editing a recipe ingredient the checkbox "Disable stock fulfillment checking for this ingredient" was not initaliased with the saved value
- Fixed that the status filter ("Enough in stock", etc.) on the recipes page did not filter recipes on the gallery tab (thanks @fipwmaqzufheoxq92ebc)
- Fixed that consuming a recipe ingredient with tare weight handling enabled consumed a wrong amount (thanks @fipwmaqzufheoxq92ebc)
- Fixed that consuming a parent product recipe ingredient did not consider quantity unit conversion when effectively consuming a child product
### Meal plan fixes
- Fixed that for products the quantity unit purchase was displayed instead of the products quantity unit stock (thanks @BenoitAnastay)
@ -201,6 +207,11 @@
- The stock journal (entity `stock_log`) is now also available via the endpoint `/objects/{entity}` (=> `/objects/stock_log`)
- Performance improvements of the `/stock/products/*` endpoints (thanks @fipwmaqzufheoxq92ebc)
- The endpoint `/stock/products/{productId}/locations` now also has an optional query parameter `include_sub_products` to optionally also return locations of sub products of the given product
- The following endpoints now have an optional request body parameter `allow_subproduct_substitution` to consume/open any child product when the given product is a parent product and currently not in stock
- `/stock/products/{productId}/consume`
- `/stock/products/by-barcode/{barcode}/consume`
- `/stock/products/{productId}/open`
- `/stock/products/by-barcode/{barcode}/open`
- Fixed that the endpoint `/objects/{entity}/{objectId}` always returned successfully, even when the given object not exists (now returns `404` when the object is not found) (thanks @fipwmaqzufheoxq92ebc)
- Fixed that the endpoint `/stock/volatile` didn't include products which expire today (thanks @fipwmaqzufheoxq92ebc)
- Fixed that the endpoint `/objects/{entity}` did not include Userfields for Userentities (so the effective endpoint `/objects/userobjects`)

View File

@ -206,7 +206,7 @@ class GenericEntityApiController extends BaseApiController
private function IsEntityWithEditRequiresAdmin($entity)
{
return !in_array($entity, $this->getOpenApiSpec()->components->internalSchemas->EntityEditRequiresAdmin->enum);
return in_array($entity, $this->getOpenApiSpec()->components->internalSchemas->EntityEditRequiresAdmin->enum);
}
private function IsEntityWithPreventedListing($entity)

View File

@ -238,8 +238,6 @@ class StockApiController extends BaseApiController
$requestBody = $this->GetParsedAndFilteredRequestBody($request);
$result = null;
try
{
if ($requestBody === null)
@ -253,57 +251,55 @@ class StockApiController extends BaseApiController
}
$spoiled = false;
if (array_key_exists('spoiled', $requestBody))
{
$spoiled = $requestBody['spoiled'];
}
$transactionType = StockService::TRANSACTION_TYPE_CONSUME;
if (array_key_exists('transaction_type', $requestBody) && !empty($requestBody['transactiontype']))
{
$transactionType = $requestBody['transactiontype'];
}
$specificStockEntryId = 'default';
if (array_key_exists('stock_entry_id', $requestBody) && !empty($requestBody['stock_entry_id']))
{
$specificStockEntryId = $requestBody['stock_entry_id'];
}
$locationId = null;
if (array_key_exists('location_id', $requestBody) && !empty($requestBody['location_id']) && is_numeric($requestBody['location_id']))
{
$locationId = $requestBody['location_id'];
}
$recipeId = null;
if (array_key_exists('recipe_id', $requestBody) && is_numeric($requestBody['recipe_id']))
{
$recipeId = $requestBody['recipe_id'];
}
$consumeExact = false;
if (array_key_exists('exact_amount', $requestBody))
{
$consumeExact = $requestBody['exact_amount'];
}
$transactionId = null;
$bookingId = $this->getStockService()->ConsumeProduct($args['productId'], $requestBody['amount'], $spoiled, $transactionType, $specificStockEntryId, $recipeId, $locationId, $transactionId, false, $consumeExact);
$allowSubproductSubstitution = false;
if (array_key_exists('allow_subproduct_substitution', $requestBody))
{
$allowSubproductSubstitution = $requestBody['allow_subproduct_substitution'];
}
$transactionId = null;
$bookingId = $this->getStockService()->ConsumeProduct($args['productId'], $requestBody['amount'], $spoiled, $transactionType, $specificStockEntryId, $recipeId, $locationId, $transactionId, $allowSubproductSubstitution, $consumeExact);
return $this->ApiResponse($response, $this->getDatabase()->stock_log($bookingId));
}
catch (\Exception $ex)
{
$result = $this->GenericErrorResponse($response, $ex->getMessage());
return $this->GenericErrorResponse($response, $ex->getMessage());
}
return $result;
}
public function ConsumeProductByBarcode(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
@ -510,13 +506,20 @@ class StockApiController extends BaseApiController
}
$specificStockEntryId = 'default';
if (array_key_exists('stock_entry_id', $requestBody) && !empty($requestBody['stock_entry_id']))
{
$specificStockEntryId = $requestBody['stock_entry_id'];
}
$bookingId = $this->getStockService()->OpenProduct($args['productId'], $requestBody['amount'], $specificStockEntryId);
$allowSubproductSubstitution = false;
if (array_key_exists('allow_subproduct_substitution', $requestBody))
{
$allowSubproductSubstitution = $requestBody['allow_subproduct_substitution'];
}
$transactionId = null;
$bookingId = $this->getStockService()->OpenProduct($args['productId'], $requestBody['amount'], $specificStockEntryId, $transactionId, $allowSubproductSubstitution);
return $this->ApiResponse($response, $this->getDatabase()->stock_log($bookingId));
}
catch (\Exception $ex)

View File

@ -207,8 +207,17 @@ class StockController extends BaseController
public function ProductsList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
if (isset($request->getQueryParams()['include_disabled']))
{
$products = $this->getDatabase()->products()->orderBy('name', 'COLLATE NOCASE');
}
else
{
$products = $this->getDatabase()->products()->where('active = 1')->orderBy('name', 'COLLATE NOCASE');
}
return $this->renderPage($response, 'products', [
'products' => $this->getDatabase()->products()->orderBy('name', 'COLLATE NOCASE'),
'products' => $products,
'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'),
'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'productGroups' => $this->getDatabase()->product_groups()->orderBy('name', 'COLLATE NOCASE'),

View File

@ -84,10 +84,6 @@ class User
public function hasPermission(string $permission): bool
{
// global $PERMISSION_CACHE;
// if(isset($PERMISSION_CACHE[$permission]))
// return $PERMISSION_CACHE[$permission];
return $this->getPermissions()->where('permission_name', $permission)->fetch() !== null;
}

View File

@ -152,14 +152,24 @@ class UsersApiController extends BaseApiController
try
{
User::checkPermission($request, User::PERMISSION_ADMIN);
$requestBody = $this->GetParsedAndFilteredRequestBody($request);
$requestBody = $request->getParsedBody();
$db = $this->getDatabase();
$db->user_permissions()
->where('user_id', $args['userId'])
->delete();
$perms = [];
if (GROCY_MODE === 'demo' || GROCY_MODE === 'prerelease')
{
// For demo mode always all users have and keep the ADMIN permission
$perms[] = [
'user_id' => $args['userId'],
'permission_id' => 1
];
}
else
{
foreach ($requestBody['permissions'] as $perm_id)
{
$perms[] = [
@ -167,7 +177,7 @@ class UsersApiController extends BaseApiController
'permission_id' => $perm_id
];
}
}
$db->insert('user_permissions', $perms, 'batch');
return $this->EmptyApiResponse($response);

View File

@ -1835,6 +1835,10 @@
"exact_amount": {
"type": "boolean",
"description": "For tare weight handling enabled products, `true` when the given is the absolute amount to be consumed, not the amount including the container weight"
},
"allow_subproduct_substitution": {
"type": "boolean",
"description": "`True` when any in-stock sub product should be used when the given product is a parent product and currently not in-stock"
}
},
"example": {
@ -2054,6 +2058,10 @@
"stock_entry_id": {
"type": "string",
"description": "A specific stock entry id to open, if used, the amount has to be 1"
},
"allow_subproduct_substitution": {
"type": "boolean",
"description": "`True` when any in-stock sub product should be used when the given product is a parent product and currently not in-stock"
}
},
"example": {
@ -2262,6 +2270,10 @@
"exact_amount": {
"type": "boolean",
"description": "For tare weight handling enabled products, `true` when the given is the absolute amount to be consumed, not the amount including the container weight"
},
"allow_subproduct_substitution": {
"type": "boolean",
"description": "`True` when any in-stock sub product should be used when the given product is a parent product and currently not in-stock"
}
},
"example": {
@ -2476,6 +2488,10 @@
"stock_entry_id": {
"type": "string",
"description": "A specific stock entry id to open, if used, the amount has to be 1"
},
"allow_subproduct_substitution": {
"type": "boolean",
"description": "`True` when any in-stock sub product should be used when the given product is a parent product and currently not in-stock"
}
},
"example": {

View File

@ -751,7 +751,7 @@ msgstr ""
msgid "Image of product %s"
msgstr ""
msgid "Delete not possible"
msgid "Deletion not possible"
msgstr ""
msgid "Equipment"
@ -1906,19 +1906,19 @@ msgstr ""
msgid "For purchases this amount of days will be added to today for the due date suggestion"
msgstr ""
msgid "-1 means that this product wille be never overdue"
msgid "-1 means that this product will be never overdue"
msgstr ""
msgid "Default due days"
msgstr ""
msgid "When this product was marked as opened, the expiry date will be replaced by today + this amount of days (a value of 0 disables this)"
msgid "When this product was marked as opened, the due date will be replaced by today + this amount of days (a value of 0 disables this)"
msgstr ""
msgid "Default due days after opened"
msgstr ""
msgid "On moving this product to a freezer location (so when freezing it), the expiry date will be replaced by today + this amount of days"
msgid "On moving this product to a freezer location (so when freezing it), the due date will be replaced by today + this amount of days"
msgstr ""
msgid "Default due days after freezing"
@ -1996,3 +1996,15 @@ msgstr ""
msgid "Use the products \"Quick consume amount\""
msgstr ""
msgid "Disabled"
msgstr ""
msgid "This also removes any stock amount, the journal and all other references of this product - consider disabling this product instead, if you want to keep that and just hide it."
msgstr ""
msgid "Show disabled products"
msgstr ""
msgid "Show on stock overview page"
msgstr ""

View File

@ -70,6 +70,7 @@ CREATE TABLE products (
cumulate_min_stock_amount_of_sub_products TINYINT DEFAULT 0,
due_type TINYINT NOT NULL DEFAULT 1 CHECK(due_type IN (1, 2)),
quick_consume_amount REAL NOT NULL DEFAULT 1,
show_on_stock_overview TINYINT NOT NULL DEFAULT 1 CHECK(show_on_stock_overview IN (0, 1)),
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
);

View File

@ -37,7 +37,8 @@ FROM (
WHERE m.id NOT IN (SELECT product_id FROM stock_current)
) sc
LEFT JOIN products p
ON sc.product_id = p.id;
ON sc.product_id = p.id
WHERE p.show_on_stock_overview = 1;
CREATE VIEW uihelper_stock_current_overview
AS
@ -78,4 +79,5 @@ FROM (
WHERE m.id NOT IN (SELECT product_id FROM stock_current)
) sc
LEFT JOIN products p
ON sc.product_id = p.id;
ON sc.product_id = p.id
WHERE p.show_on_stock_overview = 1;

25
migrations/0120.sql Normal file
View File

@ -0,0 +1,25 @@
CREATE TRIGGER cascade_product_removal AFTER DELETE ON products
BEGIN
DELETE FROM stock_log
WHERE product_id = OLD.id;
DELETE FROM product_barcodes
WHERE product_id = OLD.id;
DELETE FROM quantity_unit_conversions
WHERE product_id = OLD.id;
DELETE FROM recipes_pos
WHERE product_id = OLD.id;
UPDATE recipes
SET product_id = NULL
WHERE product_id = OLD.id;
DELETE FROM meal_plan
WHERE product_id = OLD.id
AND type = 'product';
DELETE FROM shopping_list
WHERE product_id = OLD.id;
END;

View File

@ -745,7 +745,10 @@ $.extend(true, $.fn.dataTable.defaults, {
{
return JSON.parse(Grocy.UserSettings[settingKey]);
}
}
},
'columnDefs': [
{ type: 'chinese-string', targets: '_all' }
]
});
// serializeJSON defaults

View File

@ -3,7 +3,7 @@
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
],
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#batteries-table tbody').removeClass("d-none");
batteriesTable.columns.adjust().draw();

View File

@ -4,7 +4,7 @@
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#batteries-journal-table tbody').removeClass("d-none");
batteriesJournalTable.columns.adjust().draw();

View File

@ -3,7 +3,7 @@
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#batteries-overview-table tbody').removeClass("d-none");
batteriesOverviewTable.columns.adjust().draw();

View File

@ -3,7 +3,7 @@
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#chores-table tbody').removeClass("d-none");
choresTable.columns.adjust().draw();

View File

@ -4,7 +4,7 @@
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#chores-journal-table tbody').removeClass("d-none");
choresJournalTable.columns.adjust().draw();

View File

@ -3,7 +3,7 @@
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#chores-overview-table tbody').removeClass("d-none");
choresOverviewTable.columns.adjust().draw();

View File

@ -172,19 +172,7 @@ $('#product_id_text_input').on('blur', function(e)
addProductWorkflowsAdditionalCssClasses = "d-none";
}
Grocy.Components.ProductPicker.PopupOpen = true;
bootbox.dialog({
message: __t('"%s" could not be resolved to a product, how do you want to proceed?', input),
title: __t('Create or assign product'),
onEscape: function()
{
Grocy.Components.ProductPicker.PopupOpen = false;
Grocy.Components.ProductPicker.SetValue('');
},
size: 'large',
backdrop: true,
closeButton: false,
buttons: {
var buttons = {
cancel: {
label: __t('Cancel'),
className: 'btn-secondary responsive-button',
@ -221,7 +209,35 @@ $('#product_id_text_input').on('blur', function(e)
window.location.href = U('/product/new?flow=InplaceNewProductWithBarcode&barcode=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(Grocy.CurrentUrlRelative + "?flow=InplaceAddBarcodeToExistingProduct&barcode=" + input));
}
}
};
if (!Grocy.FeatureFlags.DISABLE_BROWSER_BARCODE_CAMERA_SCANNING)
{
buttons.retrycamerascanning = {
label: '<strong>C</strong> <i class="fas fa-camera"></i>',
className: 'btn-primary responsive-button retry-camera-scanning-button',
callback: function()
{
Grocy.Components.ProductPicker.PopupOpen = false;
Grocy.Components.ProductPicker.SetValue('');
$("#barcodescanner-start-button").click();
}
};
}
Grocy.Components.ProductPicker.PopupOpen = true;
bootbox.dialog({
message: __t('"%s" could not be resolved to a product, how do you want to proceed?', input),
title: __t('Create or assign product'),
onEscape: function()
{
Grocy.Components.ProductPicker.PopupOpen = false;
Grocy.Components.ProductPicker.SetValue('');
},
size: 'large',
backdrop: true,
closeButton: false,
buttons: buttons
}).on('keypress', function(e)
{
if (e.key === 'B' || e.key === 'b')
@ -236,6 +252,10 @@ $('#product_id_text_input').on('blur', function(e)
{
$('.add-new-product-with-barcode-dialog-button').not(".d-none").click();
}
if (e.key === 'c' || e.key === 'C')
{
$('.retry-camera-scanning-button').not(".d-none").click();
}
});
}
}
@ -249,7 +269,6 @@ $(document).on("Grocy.BarcodeScanned", function(e, barcode, target)
}
// Don't know why the blur event does not fire immediately ... this works...
Grocy.Components.ProductPicker.GetInputElement().focusout();
Grocy.Components.ProductPicker.GetInputElement().focus();
Grocy.Components.ProductPicker.GetInputElement().blur();

View File

@ -11,6 +11,7 @@
jsonData.amount = jsonForm.amount;
jsonData.exact_amount = $('#consume-exact-amount').is(':checked');
jsonData.spoiled = $('#spoiled').is(':checked');
jsonData.allow_subproduct_substitution = true;
if ($("#use_specific_stock_entry").is(":checked"))
{
@ -28,7 +29,6 @@
}
var bookingResponse = null;
Grocy.Api.Get('stock/products/' + jsonForm.product_id,
function(productDetails)
{
@ -146,6 +146,7 @@ $('#save-mark-as-open-button').on('click', function(e)
jsonData = {};
jsonData.amount = jsonForm.amount;
jsonData.allow_subproduct_substitution = true;
if ($("#use_specific_stock_entry").is(":checked"))
{
@ -215,7 +216,7 @@ $("#location_id").on('change', function(e)
if (locationId)
{
Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries',
Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries?include_sub_products=true',
function(stockEntries)
{
stockEntries.forEach(stockEntry =>
@ -234,7 +235,7 @@ $("#location_id").on('change', function(e)
text: __t("Amount: %1$s; Due on %2$s; Bought on %3$s", stockEntry.amount, moment(stockEntry.best_before_date).format("YYYY-MM-DD"), moment(stockEntry.purchased_date).format("YYYY-MM-DD")) + "; " + openTxt
}));
sumValue = sumValue + parseFloat(stockEntry.amount);
sumValue = sumValue + parseFloat(stockEntry.amount_aggregated);
if (stockEntry.stock_id == stockId)
{
@ -302,7 +303,7 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
$(".input-group-productamountpicker").trigger("change");
$("#location_id").find("option").remove().end().append("<option></option>");
Grocy.Api.Get("stock/products/" + productId + '/locations',
Grocy.Api.Get("stock/products/" + productId + '/locations?include_sub_products=true',
function(stockLocations)
{
var setDefault = 0;
@ -369,7 +370,7 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
$("#tare-weight-handling-info").addClass("d-none");
}
if ((parseFloat(productDetails.stock_amount) || 0) === 0)
if ((parseFloat(productDetails.stock_amount_aggregated) || 0) === 0)
{
Grocy.Components.ProductAmountPicker.Reset();
Grocy.Components.ProductPicker.Clear();
@ -448,14 +449,14 @@ $("#specific_stock_entry").on("change", function(e)
if ($(e.target).val() == "")
{
sumValue = 0;
Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries',
Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries?include_sub_products=true',
function(stockEntries)
{
stockEntries.forEach(stockEntry =>
{
if (stockEntry.location_id == $("#location_id").val() || stockEntry.location_id == "")
{
sumValue = sumValue + parseFloat(stockEntry.amount);
sumValue = sumValue + parseFloat(stockEntry.amount_aggregated);
}
});
$("#display_amount").attr("max", sumValue);

View File

@ -2,9 +2,8 @@
'order': [[0, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 },
{ 'orderData': 2, 'targets': 1 }
],
{ 'searchable': false, "targets": 0 }
].concat($.fn.dataTable.defaults.columnDefs),
select: {
style: 'single',
selector: 'tr td:not(:first-child)'

View File

@ -159,6 +159,24 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
Grocy.Components.LocationPicker.SetId(productDetails.location.id);
}
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
{
if (productDetails.product.default_best_before_days.toString() !== '0')
{
if (productDetails.product.default_best_before_days == -1)
{
if (!$("#datetimepicker-shortcut").is(":checked"))
{
$("#datetimepicker-shortcut").click();
}
}
else
{
Grocy.Components.DateTimePicker.SetValue(moment().add(productDetails.product.default_best_before_days, 'days').format('YYYY-MM-DD'));
}
}
}
$('#display_amount').val(productDetails.stock_amount);
RefreshLocaleNumberInput();
$(".input-group-productamountpicker").trigger("change");

View File

@ -3,7 +3,7 @@
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#locations-table tbody').removeClass("d-none");
locationsTable.columns.adjust().draw();

View File

@ -3,7 +3,7 @@
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#apikeys-table tbody').removeClass("d-none");
apiKeysTable.columns.adjust().draw();

View File

@ -291,7 +291,7 @@ var quConversionsTable = $('#qu-conversions-table-products').DataTable({
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 },
{ 'visible': false, 'targets': 4 }
],
].concat($.fn.dataTable.defaults.columnDefs),
'rowGroup': {
dataSrc: 4
}
@ -305,7 +305,7 @@ var barcodeTable = $('#barcode-table').DataTable({
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#barcode-table tbody').removeClass("d-none");
barcodeTable.columns.adjust().draw();

View File

@ -3,7 +3,7 @@
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#productgroups-table tbody').removeClass("d-none");
groupsTable.columns.adjust().draw();

View File

@ -3,7 +3,7 @@
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#products-table tbody').removeClass("d-none");
productsTable.columns.adjust().draw();
@ -36,6 +36,7 @@ $("#clear-filter-button").on("click", function()
$("#product-group-filter").val("all");
productsTable.column(7).search("").draw();
productsTable.search("").draw();
$("#show-disabled-products").prop('checked', false);
});
if (typeof GetUriParam("product-group") !== "undefined")
@ -49,15 +50,8 @@ $(document).on('click', '.product-delete-button', function(e)
var objectName = $(e.currentTarget).attr('data-product-name');
var objectId = $(e.currentTarget).attr('data-product-id');
Grocy.Api.Get('stock/products/' + objectId,
function(productDetails)
{
var stockAmount = productDetails.stock_amount || '0';
if (stockAmount.toString() == "0")
{
bootbox.confirm({
message: __t('Are you sure you want to deactivate this product "%s"?', objectName),
message: __t('Are you sure to delete product "%s"?', objectName) + '<br><br>' + __t('This also removes any stock amount, the journal and all other references of this product - consider disabling this product instead, if you want to keep that and just hide it.'),
closeButton: false,
buttons: {
confirm: {
@ -75,7 +69,7 @@ $(document).on('click', '.product-delete-button', function(e)
{
jsonData = {};
jsonData.active = 0;
Grocy.Api.Put('objects/products/' + objectId, jsonData,
Grocy.Api.Delete('objects/products/' + objectId, {},
function(result)
{
window.location.href = U('/products');
@ -88,19 +82,21 @@ $(document).on('click', '.product-delete-button', function(e)
}
}
});
});
$("#show-disabled-products").change(function()
{
if (this.checked)
{
window.location.href = U('/products?include_disabled');
}
else
{
bootbox.alert({
title: __t('Deactivation not possible'),
message: __t('This product cannot be deactivated because it is in stock, please remove the stock amount first.') + '<br><br>' + __t('Stock amount') + ': ' + stockAmount + ' ' + __n(stockAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural),
closeButton: false
});
window.location.href = U('/products');
}
},
function(xhr)
{
console.error(xhr);
}
);
});
if (GetUriParam('include_disabled'))
{
$("#show-disabled-products").prop('checked', true);
}

View File

@ -139,7 +139,7 @@ var quConversionsTable = $('#qu-conversions-table').DataTable({
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#qu-conversions-table tbody').removeClass("d-none");
quConversionsTable.columns.adjust().draw();

View File

@ -3,7 +3,7 @@
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#quantityunits-table tbody').removeClass("d-none");
quantityUnitsTable.columns.adjust().draw();

View File

@ -80,7 +80,7 @@ var recipesPosTables = $('#recipes-pos-table').DataTable({
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 },
{ 'visible': false, 'targets': 4 }
],
].concat($.fn.dataTable.defaults.columnDefs),
'rowGroup': {
dataSrc: 4
}
@ -93,7 +93,7 @@ var recipesIncludesTables = $('#recipes-includes-table').DataTable({
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#recipes-includes-table tbody').removeClass("d-none");
recipesIncludesTables.columns.adjust().draw();

View File

@ -2,9 +2,8 @@
'order': [[1, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 },
{ 'orderData': 2, 'targets': 1 }
],
{ 'searchable': false, "targets": 0 }
].concat($.fn.dataTable.defaults.columnDefs),
select: {
style: 'single',
selector: 'tr td:not(:first-child)'

View File

@ -7,7 +7,7 @@ var shoppingListTable = $('#shoppinglist-table').DataTable({
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 },
{ 'visible': false, 'targets': 3 }
],
].concat($.fn.dataTable.defaults.columnDefs),
'rowGroup': {
dataSrc: 3,
startRender: function(rows, group)

View File

@ -3,7 +3,7 @@ var locationsTable = $('#shoppinglocations-table').DataTable({
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#shoppinglocations-table tbody').removeClass("d-none");
locationsTable.columns.adjust().draw();

View File

@ -3,7 +3,7 @@
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
],
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#stockentries-table tbody').removeClass("d-none");
stockEntriesTable.columns.adjust().draw();

View File

@ -4,7 +4,7 @@
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#stock-journal-table tbody').removeClass("d-none");
stockJournalTable.columns.adjust().draw();

View File

@ -4,7 +4,7 @@ var journalSummaryTable = $('#stock-journal-summary-table').DataTable({
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#stock-journal-summary-table tbody').removeClass("d-none");
journalSummaryTable.columns.adjust().draw();

View File

@ -11,7 +11,7 @@
{ 'visible': false, 'targets': 4 },
{ 'visible': false, 'targets': 9 },
{ 'visible': false, 'targets': 10 }
],
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#stock-overview-table tbody').removeClass("d-none");
@ -106,7 +106,7 @@ $(document).on('click', '.product-consume-button', function(e)
var originalTotalStockAmount = $(e.currentTarget).attr('data-original-total-stock-amount');
var wasSpoiled = $(e.currentTarget).hasClass("product-consume-button-spoiled");
Grocy.Api.Post('stock/products/' + productId + '/consume', { 'amount': consumeAmount, 'spoiled': wasSpoiled },
Grocy.Api.Post('stock/products/' + productId + '/consume', { 'amount': consumeAmount, 'spoiled': wasSpoiled, 'allow_subproduct_substitution': true },
function(bookingResponse)
{
Grocy.Api.Get('stock/products/' + productId,
@ -162,7 +162,7 @@ $(document).on('click', '.product-open-button', function(e)
var amount = $(e.currentTarget).attr('data-open-amount');
var button = $(e.currentTarget);
Grocy.Api.Post('stock/products/' + productId + '/open', { 'amount': amount },
Grocy.Api.Post('stock/products/' + productId + '/open', { 'amount': amount, 'allow_subproduct_substitution': true },
function(bookingResponse)
{
Grocy.Api.Get('stock/products/' + productId,

View File

@ -3,7 +3,7 @@
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#taskcategories-table tbody').removeClass("d-none");
categoriesTable.columns.adjust().draw();

View File

@ -4,7 +4,7 @@
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 },
{ 'visible': false, 'targets': 3 }
],
].concat($.fn.dataTable.defaults.columnDefs),
'rowGroup': {
dataSrc: 3
}

View File

@ -3,7 +3,7 @@
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#userentities-table tbody').removeClass("d-none");
userentitiesTable.columns.adjust().draw();

View File

@ -3,7 +3,7 @@
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#userfields-table tbody').removeClass("d-none");
userfieldsTable.columns.adjust().draw();

View File

@ -3,7 +3,7 @@
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#userobjects-table tbody').removeClass("d-none");
userobjectsTable.columns.adjust().draw();

View File

@ -24,14 +24,15 @@ $('#permission-save').click(
{
return $(this).data('perm-id');
}).toArray();
Grocy.Api.Put('users/' + Grocy.EditObjectId + '/permissions', {
'permissions': permission_list,
}, function(result)
Grocy.Api.Put('users/' + Grocy.EditObjectId + '/permissions', { 'permissions': permission_list },
function(result)
{
toastr.success(__t("Permissions saved"));
}, function(xhr)
},
function(xhr)
{
toastr.error(__t(JSON.parse(xhr.response).error_message));
toastr.error(JSON.parse(xhr.response).error_message);
}
);
}
@ -51,5 +52,3 @@ if (Grocy.EditObjectId == Grocy.UserId)
}
})
}
check_hierachy($("input.permission-cb[name=ADMIN]").is(":checked"), "ADMIN");

View File

@ -3,7 +3,7 @@
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#users-table tbody').removeClass("d-none");
usersTable.columns.adjust().draw();

View File

@ -242,12 +242,11 @@ class StockService extends BaseService
throw new \Exception('Location does not exist');
}
// Tare weight handling
$productDetails = (object)$this->GetProductDetails($productId);
// Tare weight handling
// The given amount is the new total amount including the container weight (gross)
// The amount to be posted needs to be the absolute value of the given amount - stock amount - tare weight
$productDetails = (object) $this->GetProductDetails($productId);
if ($productDetails->product->enable_tare_weight_handling == 1)
{
if ($consumeExactAmount)
@ -265,11 +264,13 @@ class StockService extends BaseService
if ($transactionType === self::TRANSACTION_TYPE_CONSUME || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION)
{
if ($locationId === null)
{ // Consume from any location
{
// Consume from any location
$potentialStockEntries = $this->GetProductStockEntries($productId, false, $allowSubproductSubstitution);
}
else
{ // Consume only from the supplied location
{
// Consume only from the supplied location
$potentialStockEntries = $this->GetProductStockEntriesForLocation($productId, $locationId, false, $allowSubproductSubstitution);
}
@ -297,8 +298,20 @@ class StockService extends BaseService
break;
}
if ($allowSubproductSubstitution && $stockEntry->product_id != $productId)
{
// A sub product will be used -> use QU conversions
$subProduct = $this->getDatabase()->products($stockEntry->product_id);
$conversion = $this->getDatabase()->quantity_unit_conversions_resolved()->where('product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3', $stockEntry->product_id, $productDetails->product->qu_id_stock, $subProduct->qu_id_stock)->fetch();
if ($conversion != null)
{
$amount = $amount * floatval($conversion->factor);
}
}
if ($amount >= $stockEntry->amount)
{ // Take the whole stock entry
{
// Take the whole stock entry
$logRow = $this->getDatabase()->stock_log()->createRow([
'product_id' => $stockEntry->product_id,
'amount' => $stockEntry->amount * -1,
@ -321,7 +334,8 @@ class StockService extends BaseService
$amount -= $stockEntry->amount;
}
else
{ // Stock entry amount is > than needed amount -> split the stock entry resp. update the amount
{
// Stock entry amount is > than needed amount -> split the stock entry resp. update the amount
$restStockAmount = $stockEntry->amount - $amount;
$logRow = $this->getDatabase()->stock_log()->createRow([
@ -665,7 +679,7 @@ class StockService extends BaseService
$sqlWhereProductId = 'product_id = ' . $productId;
if ($allowSubproductSubstitution)
{
$sqlWhereProductId = 'product_id IN (SELECT sub_product_id FROM products_resolved WHERE parent_product_id = ' . $productId . ')';
$sqlWhereProductId = '(product_id IN (SELECT sub_product_id FROM products_resolved WHERE parent_product_id = ' . $productId . ') OR product_id = ' . $productId . ')';
}
$sqlWhereAndOpen = 'AND open IN (0, 1)';
@ -697,7 +711,7 @@ class StockService extends BaseService
$sqlWhereProductId = 'product_id = ' . $productId;
if ($allowSubproductSubstitution)
{
$sqlWhereProductId = 'product_id IN (SELECT sub_product_id FROM products_resolved WHERE parent_product_id = ' . $productId . ')';
$sqlWhereProductId = '(product_id IN (SELECT sub_product_id FROM products_resolved WHERE parent_product_id = ' . $productId . ') OR product_id = ' . $productId . ')';
}
return $this->getDatabase()->stock_current_locations()->where($sqlWhereProductId);
@ -768,15 +782,16 @@ class StockService extends BaseService
return null;
}
public function OpenProduct(int $productId, float $amount, $specificStockEntryId = 'default', &$transactionId = null)
public function OpenProduct(int $productId, float $amount, $specificStockEntryId = 'default', &$transactionId = null, $allowSubproductSubstitution = false)
{
if (!$this->ProductExists($productId))
{
throw new \Exception('Product does not exist or is inactive');
}
$productStockAmountUnopened = $this->getDatabase()->stock()->where('product_id = :1 AND open = 0', $productId)->sum('amount');
$potentialStockEntries = $this->GetProductStockEntries($productId, true);
$productDetails = (object)$this->GetProductDetails($productId);
$productStockAmountUnopened = floatval($productDetails->stock_amount_aggregated) - floatval($productDetails->stock_amount_opened_aggregated);
$potentialStockEntries = $this->GetProductStockEntries($productId, true, $allowSubproductSubstitution);
$product = $this->getDatabase()->products($productId);
if ($product->enable_tare_weight_handling == 1)
@ -807,14 +822,25 @@ class StockService extends BaseService
}
$newBestBeforeDate = $stockEntry->best_before_date;
if ($product->default_best_before_days_after_open > 0)
{
$newBestBeforeDate = date('Y-m-d', strtotime('+' . $product->default_best_before_days_after_open . ' days'));
}
if ($allowSubproductSubstitution && $stockEntry->product_id != $productId)
{
// A sub product will be used -> use QU conversions
$subProduct = $this->getDatabase()->products($stockEntry->product_id);
$conversion = $this->getDatabase()->quantity_unit_conversions_resolved()->where('product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3', $stockEntry->product_id, $product->qu_id_stock, $subProduct->qu_id_stock)->fetch();
if ($conversion != null)
{
$amount = $amount * floatval($conversion->factor);
}
}
if ($amount >= $stockEntry->amount)
{ // Mark the whole stock entry as opened
{
// Mark the whole stock entry as opened
$logRow = $this->getDatabase()->stock_log()->createRow([
'product_id' => $stockEntry->product_id,
'amount' => $stockEntry->amount,
@ -840,7 +866,8 @@ class StockService extends BaseService
$amount -= $stockEntry->amount;
}
else
{ // Stock entry amount is > than needed amount -> split the stock entry
{
// Stock entry amount is > than needed amount -> split the stock entry
$restStockAmount = $stockEntry->amount - $amount;
$newStockRow = $this->getDatabase()->stock()->createRow([

View File

@ -3,7 +3,8 @@
name="{{ $perm->permission_name }}"
class="permission-cb"
data-perm-id="{{ $perm->permission_id }}"
@if($perm->has_permission) checked @endif autocomplete="off">
@if($perm->has_permission) checked @endif
@if(isset($permParent) && $permParent->has_permission) disabled @endif>
{{ $__t($perm->permission_name) }}
</label>
<div id="permission-sub-{{ $perm->permission_name }}">
@ -11,7 +12,8 @@
@foreach($perm->uihelper_user_permissionsList(array('user_id' => $user->id))->via('parent') as $p)
<li>
@include('components.userpermission_select', array(
'perm' => $p
'perm' => $p,
'permParent' => $perm
))
</li>
@endforeach

View File

@ -677,6 +677,7 @@
<script src="{{ $U('/node_modules/datatables.net-select/js/dataTables.select.min.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/node_modules/datatables.net-select-bs4/js/select.bootstrap4.min.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/node_modules/datatables.net-plugins/filtering/type-based/accent-neutralise.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/node_modules/datatables.net-plugins/sorting/chinese-string.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/node_modules/timeago/jquery.timeago.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/node_modules', true) }}/timeago/locales/jquery.timeago.{{ $__t('timeago_locale') }}.js?v={{ $version }}"></script>
<script src="{{ $U('/node_modules/toastr/build/toastr.min.js?v=', true) }}{{ $version }}"></script>

View File

@ -75,6 +75,19 @@
</div>
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input @if($mode=='create'
)
checked
@elseif($mode=='edit'
&&
$product->show_on_stock_overview == 1) checked @endif class="form-check-input custom-control-input" type="checkbox" id="show_on_stock_overview" name="show_on_stock_overview" value="1">
<label class="form-check-label custom-control-label"
for="show_on_stock_overview">{{ $__t('Show on stock overview page') }}</label>
</div>
</div>
@php $prefillById = ''; if($mode=='edit') { $prefillById = $product->parent_product_id; } @endphp
@php
$hint = '';
@ -211,7 +224,7 @@
'label' => 'Default due days',
'min' => -1,
'value' => $value,
'hint' => $__t('For purchases this amount of days will be added to today for the due date suggestion') . ' (' . $__t('-1 means that this product wille be never overdue') . ')'
'hint' => $__t('For purchases this amount of days will be added to today for the due date suggestion') . ' (' . $__t('-1 means that this product will be never overdue') . ')'
))
@if(GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING)
@ -221,7 +234,7 @@
'label' => 'Default due days after opened',
'min' => 0,
'value' => $value,
'hint' => $__t('When this product was marked as opened, the expiry date will be replaced by today + this amount of days (a value of 0 disables this)')
'hint' => $__t('When this product was marked as opened, the due date will be replaced by today + this amount of days (a value of 0 disables this)')
))
@else
<input type="hidden"
@ -376,7 +389,7 @@
'label' => 'Default due days after freezing',
'min' => -1,
'value' => $value,
'hint' => $__t('On moving this product to a freezer location (so when freezing it), the expiry date will be replaced by today + this amount of days')
'hint' => $__t('On moving this product to a freezer location (so when freezing it), the due date will be replaced by today + this amount of days')
))
@php if($mode == 'edit') { $value = $product->default_best_before_days_after_thawing; } else { $value = 0; } @endphp

View File

@ -71,6 +71,17 @@
</select>
</div>
</div>
<div class="col-xs-12 col-md-6 col-xl-3">
<div class="form-check custom-control custom-checkbox">
<input class="form-check-input custom-control-input"
type="checkbox"
id="show-disabled-products">
<label class="form-check-label custom-control-label"
for="show-disabled-products">
{{ $__t('Show disabled products') }}
</label>
</div>
</div>
<div class="col">
<div class="float-right">
<a id="clear-filter-button"
@ -124,7 +135,7 @@
title="{{ $__t('Copy this item') }}">
<i class="fas fa-copy"></i>
</a>
<a class="btn btn-danger btn-sm product-delete-button @if($product->active == 0) disabled @endif"
<a class="btn btn-danger btn-sm product-delete-button"
href="#"
data-product-id="{{ $product->id }}"
data-product-name="{{ $product->name }}"
@ -134,9 +145,12 @@
</a>
</td>
<td>
@if($product->active == 0) (deactivated) @endif {{ $product->name }}@if(!empty($product->picture_file_name)) <i class="fas fa-image text-muted"
{{ $product->name }}
@if(!empty($product->picture_file_name))
<i class="fas fa-image text-muted"
data-toggle="tooltip"
title="{{ $__t('This product has a picture') }}"></i>@endif
title="{{ $__t('This product has a picture') }}"></i>
@endif
</td>
<td class="@if(!GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) d-none @endif">
{{ FindObjectInArrayByPropertyValue($locations, 'id', $product->location_id)->name }}

View File

@ -391,7 +391,7 @@
@endif
<li class="list-group-item px-0 @if($hasIngredientGroups && $hasProductGroups) ml-4 @elseif($hasIngredientGroups || $hasProductGroups) ml-2 @else ml-0 @endif">
@if($selectedRecipePosition->product_active == 0)
<div class="small text-muted font-italic">{{ $__t('Deactivated Product') }}</div>
<div class="small text-muted font-italic">{{ $__t('Disabled') }}</div>
@endif
@php
$product = FindObjectInArrayByPropertyValue($products, 'id', $selectedRecipePosition->product_id);
@ -411,7 +411,8 @@
{{ $__n($selectedRecipePosition->recipe_amount, FindObjectInArrayByPropertyValue($quantityUnits, 'id', $selectedRecipePosition->qu_id)->name, FindObjectInArrayByPropertyValue($quantityUnits, 'id', $selectedRecipePosition->qu_id)->name_plural) }} {{ FindObjectInArrayByPropertyValue($products, 'id', $selectedRecipePosition->product_id)->name }}
@if($selectedRecipePosition->need_fulfilled == 1)<i class="fas fa-check text-success"></i>@elseif($selectedRecipePosition->need_fulfilled_with_shopping_list == 1)<i class="fas fa-exclamation text-warning"></i>@else<i class="fas fa-times text-danger"></i>@endif
<span class="timeago-contextual">@if(FindObjectInArrayByPropertyValue($recipePositionsResolved, 'recipe_pos_id', $selectedRecipePosition->id)->need_fulfilled == 1) {{ $__t('Enough in stock') }} @else {{ $__t('Not enough in stock (not included in costs), %1$s missing, %2$s already on shopping list', round(FindObjectInArrayByPropertyValue($recipePositionsResolved, 'recipe_pos_id', $selectedRecipePosition->id)->missing_amount, 2), round(FindObjectInArrayByPropertyValue($recipePositionsResolved, 'recipe_pos_id', $selectedRecipePosition->id)->amount_on_shopping_list, 2)) }} @endif</span>
@if($selectedRecipePosition->need_fulfilled == 1 && GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) <span class="float-right font-italic ml-2 locale-number locale-number-currency">{{ $selectedRecipePosition->costs }}</span> @endif
<span class="float-right font-italic"><span class="locale-number locale-number-quantity-amount">{{ $selectedRecipePosition->calories }} {{ $__t('Calories') }}</span></span>
@if(!empty($selectedRecipePosition->recipe_variable_amount))
<div class="small text-muted font-italic">{{ $__t('Variable amount') }}</div>
@endif

View File

@ -178,7 +178,7 @@
<tr id="product-{{ $currentStockEntry->product_id }}-row"
class="@if(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('-1 days')) && $currentStockEntry->amount > 0) @if($currentStockEntry->due_type == 1) table-secondary @else table-danger @endif @elseif(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('+' . $nextXDays . ' days')) && $currentStockEntry->amount > 0) table-warning @elseif ($currentStockEntry->product_missing) table-info @endif">
<td class="fit-content border-right">
<a class="permission-STOCK_CONSUME btn btn-success btn-sm product-consume-button @if($currentStockEntry->amount < $currentStockEntry->quick_consume_amount || $currentStockEntry->enable_tare_weight_handling == 1) disabled @endif"
<a class="permission-STOCK_CONSUME btn btn-success btn-sm product-consume-button @if($currentStockEntry->amount_aggregated < $currentStockEntry->quick_consume_amount || $currentStockEntry->enable_tare_weight_handling == 1) disabled @endif"
href="#"
data-toggle="tooltip"
data-placement="left"
@ -190,7 +190,7 @@
<i class="fas fa-utensils"></i> <span class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->quick_consume_amount }}</span>
</a>
<a id="product-{{ $currentStockEntry->product_id }}-consume-all-button"
class="permission-STOCK_CONSUME btn btn-danger btn-sm product-consume-button @if($currentStockEntry->amount == 0) disabled @endif"
class="permission-STOCK_CONSUME btn btn-danger btn-sm product-consume-button @if($currentStockEntry->amount_aggregated == 0) disabled @endif"
href="#"
data-toggle="tooltip"
data-placement="right"
@ -203,7 +203,7 @@
<i class="fas fa-utensils"></i> {{ $__t('All') }}
</a>
@if(GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING)
<a class="btn btn-success btn-sm product-open-button @if($currentStockEntry->amount < $currentStockEntry->quick_consume_amount || $currentStockEntry->amount == $currentStockEntry->amount_opened || $currentStockEntry->enable_tare_weight_handling == 1) disabled @endif"
<a class="btn btn-success btn-sm product-open-button @if($currentStockEntry->amount_aggregated < $currentStockEntry->quick_consume_amount || $currentStockEntry->amount_aggregated == $currentStockEntry->amount_opened_aggregated || $currentStockEntry->enable_tare_weight_handling == 1) disabled @endif"
href="#"
data-toggle="tooltip"
data-placement="left"
@ -251,7 +251,7 @@
<span class="dropdown-item-icon"><i class="fas fa-list"></i></span> <span class="dropdown-item-text">{{ $__t('Inventory') }}</span>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item product-consume-button product-consume-button-spoiled permission-STOCK_CONSUME @if($currentStockEntry->amount < 1) disabled @endif"
<a class="dropdown-item product-consume-button product-consume-button-spoiled permission-STOCK_CONSUME @if($currentStockEntry->amount_aggregated < 1) disabled @endif"
type="button"
href="#"
data-product-id="{{ $currentStockEntry->product_id }}"