diff --git a/changelog/58_2.7.0_2020-04-16.md b/changelog/58_2.7.0_2020-04-16.md
index 2f0ee725..99001022 100644
--- a/changelog/58_2.7.0_2020-04-16.md
+++ b/changelog/58_2.7.0_2020-04-16.md
@@ -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)
diff --git a/changelog/60_UNRELEASED_2020-xx-xx.md b/changelog/60_UNRELEASED_2020-xx-xx.md
index 6740397f..7323d5c0 100644
--- a/changelog/60_UNRELEASED_2020-xx-xx.md
+++ b/changelog/60_UNRELEASED_2020-xx-xx.md
@@ -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`)
diff --git a/controllers/GenericEntityApiController.php b/controllers/GenericEntityApiController.php
index ca28539c..d1f4033c 100644
--- a/controllers/GenericEntityApiController.php
+++ b/controllers/GenericEntityApiController.php
@@ -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)
diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php
index 4ac52728..0843a1fc 100644
--- a/controllers/StockApiController.php
+++ b/controllers/StockApiController.php
@@ -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)
diff --git a/controllers/StockController.php b/controllers/StockController.php
index 73c7d8b7..2ff38b14 100644
--- a/controllers/StockController.php
+++ b/controllers/StockController.php
@@ -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'),
diff --git a/controllers/Users/User.php b/controllers/Users/User.php
index 8859c428..771e5c0a 100644
--- a/controllers/Users/User.php
+++ b/controllers/Users/User.php
@@ -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;
}
diff --git a/controllers/UsersApiController.php b/controllers/UsersApiController.php
index e9d436e1..280b647a 100644
--- a/controllers/UsersApiController.php
+++ b/controllers/UsersApiController.php
@@ -152,22 +152,32 @@ 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 = [];
-
- foreach ($requestBody['permissions'] as $perm_id)
+ 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' => $perm_id
+ 'permission_id' => 1
];
}
-
+ else
+ {
+ foreach ($requestBody['permissions'] as $perm_id)
+ {
+ $perms[] = [
+ 'user_id' => $args['userId'],
+ 'permission_id' => $perm_id
+ ];
+ }
+ }
$db->insert('user_permissions', $perms, 'batch');
return $this->EmptyApiResponse($response);
diff --git a/grocy.openapi.json b/grocy.openapi.json
index 205a498a..9f392169 100644
--- a/grocy.openapi.json
+++ b/grocy.openapi.json
@@ -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": {
diff --git a/localization/strings.pot b/localization/strings.pot
index 8a452a90..61a40133 100644
--- a/localization/strings.pot
+++ b/localization/strings.pot
@@ -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 ""
diff --git a/migrations/0103.sql b/migrations/0103.sql
index e5f2650c..5b41fd5b 100644
--- a/migrations/0103.sql
+++ b/migrations/0103.sql
@@ -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'))
);
diff --git a/migrations/0105.sql b/migrations/0105.sql
index e51fbdf0..19066995 100644
--- a/migrations/0105.sql
+++ b/migrations/0105.sql
@@ -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;
diff --git a/migrations/0110.sql b/migrations/0110.sql
index 8fd4fd8e..7a00e6f9 100644
--- a/migrations/0110.sql
+++ b/migrations/0110.sql
@@ -128,7 +128,7 @@ SELECT
SELECT pc.permission_name
FROM user_permissions_resolved pc
WHERE pc.user_id = u.id
- )
- ) AS has_permission,
- ph.parent AS parent
+ )
+ ) AS has_permission,
+ ph.parent AS parent
FROM users u, permission_hierarchy ph;
diff --git a/migrations/0120.sql b/migrations/0120.sql
new file mode 100644
index 00000000..7a469899
--- /dev/null
+++ b/migrations/0120.sql
@@ -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;
diff --git a/public/js/grocy.js b/public/js/grocy.js
index 35a70acc..7684c0af 100644
--- a/public/js/grocy.js
+++ b/public/js/grocy.js
@@ -745,7 +745,10 @@ $.extend(true, $.fn.dataTable.defaults, {
{
return JSON.parse(Grocy.UserSettings[settingKey]);
}
- }
+ },
+ 'columnDefs': [
+ { type: 'chinese-string', targets: '_all' }
+ ]
});
// serializeJSON defaults
diff --git a/public/viewjs/batteries.js b/public/viewjs/batteries.js
index de12f057..210006e0 100644
--- a/public/viewjs/batteries.js
+++ b/public/viewjs/batteries.js
@@ -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();
diff --git a/public/viewjs/batteriesjournal.js b/public/viewjs/batteriesjournal.js
index 6d11c157..1fd073fd 100644
--- a/public/viewjs/batteriesjournal.js
+++ b/public/viewjs/batteriesjournal.js
@@ -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();
diff --git a/public/viewjs/batteriesoverview.js b/public/viewjs/batteriesoverview.js
index 8a739318..f89ac36e 100644
--- a/public/viewjs/batteriesoverview.js
+++ b/public/viewjs/batteriesoverview.js
@@ -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();
diff --git a/public/viewjs/chores.js b/public/viewjs/chores.js
index 54afde41..5f29a2b6 100644
--- a/public/viewjs/chores.js
+++ b/public/viewjs/chores.js
@@ -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();
diff --git a/public/viewjs/choresjournal.js b/public/viewjs/choresjournal.js
index 5257edd2..b438bd62 100644
--- a/public/viewjs/choresjournal.js
+++ b/public/viewjs/choresjournal.js
@@ -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();
diff --git a/public/viewjs/choresoverview.js b/public/viewjs/choresoverview.js
index bbcf4b79..98e87bc0 100644
--- a/public/viewjs/choresoverview.js
+++ b/public/viewjs/choresoverview.js
@@ -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();
diff --git a/public/viewjs/components/productpicker.js b/public/viewjs/components/productpicker.js
index 87bf8c52..368554ea 100644
--- a/public/viewjs/components/productpicker.js
+++ b/public/viewjs/components/productpicker.js
@@ -172,6 +172,59 @@ $('#product_id_text_input').on('blur', function(e)
addProductWorkflowsAdditionalCssClasses = "d-none";
}
+ var buttons = {
+ cancel: {
+ label: __t('Cancel'),
+ className: 'btn-secondary responsive-button',
+ callback: function()
+ {
+ Grocy.Components.ProductPicker.PopupOpen = false;
+ Grocy.Components.ProductPicker.SetValue('');
+ }
+ },
+ addnewproduct: {
+ label: 'P ' + __t('Add as new product'),
+ className: 'btn-success add-new-product-dialog-button responsive-button ' + addProductWorkflowsAdditionalCssClasses,
+ callback: function()
+ {
+ Grocy.Components.ProductPicker.PopupOpen = false;
+ window.location.href = U('/product/new?flow=InplaceNewProductWithName&name=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(Grocy.CurrentUrlRelative + "?flow=InplaceNewProductWithName"));
+ }
+ },
+ addbarcode: {
+ label: 'B ' + __t('Add as barcode to existing product'),
+ className: 'btn-info add-new-barcode-dialog-button responsive-button',
+ callback: function()
+ {
+ Grocy.Components.ProductPicker.PopupOpen = false;
+ window.location.href = U(Grocy.CurrentUrlRelative + '?flow=InplaceAddBarcodeToExistingProduct&barcode=' + encodeURIComponent(input));
+ }
+ },
+ addnewproductwithbarcode: {
+ label: 'A ' + __t('Add as new product and prefill barcode'),
+ className: 'btn-warning add-new-product-with-barcode-dialog-button responsive-button ' + addProductWorkflowsAdditionalCssClasses,
+ callback: function()
+ {
+ Grocy.Components.ProductPicker.PopupOpen = false;
+ 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: 'C ',
+ 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),
@@ -184,44 +237,7 @@ $('#product_id_text_input').on('blur', function(e)
size: 'large',
backdrop: true,
closeButton: false,
- buttons: {
- cancel: {
- label: __t('Cancel'),
- className: 'btn-secondary responsive-button',
- callback: function()
- {
- Grocy.Components.ProductPicker.PopupOpen = false;
- Grocy.Components.ProductPicker.SetValue('');
- }
- },
- addnewproduct: {
- label: 'P ' + __t('Add as new product'),
- className: 'btn-success add-new-product-dialog-button responsive-button ' + addProductWorkflowsAdditionalCssClasses,
- callback: function()
- {
- Grocy.Components.ProductPicker.PopupOpen = false;
- window.location.href = U('/product/new?flow=InplaceNewProductWithName&name=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(Grocy.CurrentUrlRelative + "?flow=InplaceNewProductWithName"));
- }
- },
- addbarcode: {
- label: 'B ' + __t('Add as barcode to existing product'),
- className: 'btn-info add-new-barcode-dialog-button responsive-button',
- callback: function()
- {
- Grocy.Components.ProductPicker.PopupOpen = false;
- window.location.href = U(Grocy.CurrentUrlRelative + '?flow=InplaceAddBarcodeToExistingProduct&barcode=' + encodeURIComponent(input));
- }
- },
- addnewproductwithbarcode: {
- label: 'A ' + __t('Add as new product and prefill barcode'),
- className: 'btn-warning add-new-product-with-barcode-dialog-button responsive-button ' + addProductWorkflowsAdditionalCssClasses,
- callback: function()
- {
- Grocy.Components.ProductPicker.PopupOpen = false;
- window.location.href = U('/product/new?flow=InplaceNewProductWithBarcode&barcode=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(Grocy.CurrentUrlRelative + "?flow=InplaceAddBarcodeToExistingProduct&barcode=" + input));
- }
- }
- }
+ 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();
diff --git a/public/viewjs/consume.js b/public/viewjs/consume.js
index f00c5348..fa2dbcb6 100644
--- a/public/viewjs/consume.js
+++ b/public/viewjs/consume.js
@@ -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("");
- 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);
diff --git a/public/viewjs/equipment.js b/public/viewjs/equipment.js
index 77d102ef..a7f66f6a 100644
--- a/public/viewjs/equipment.js
+++ b/public/viewjs/equipment.js
@@ -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)'
diff --git a/public/viewjs/inventory.js b/public/viewjs/inventory.js
index 0e9a4dec..895dab30 100644
--- a/public/viewjs/inventory.js
+++ b/public/viewjs/inventory.js
@@ -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");
diff --git a/public/viewjs/locations.js b/public/viewjs/locations.js
index cc82df97..c8e2f784 100644
--- a/public/viewjs/locations.js
+++ b/public/viewjs/locations.js
@@ -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();
diff --git a/public/viewjs/manageapikeys.js b/public/viewjs/manageapikeys.js
index 167cb23d..f31d3954 100644
--- a/public/viewjs/manageapikeys.js
+++ b/public/viewjs/manageapikeys.js
@@ -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();
diff --git a/public/viewjs/productform.js b/public/viewjs/productform.js
index 436989bc..3d402fb4 100644
--- a/public/viewjs/productform.js
+++ b/public/viewjs/productform.js
@@ -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();
diff --git a/public/viewjs/productgroups.js b/public/viewjs/productgroups.js
index 0271b06f..3c9b85d0 100644
--- a/public/viewjs/productgroups.js
+++ b/public/viewjs/productgroups.js
@@ -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();
diff --git a/public/viewjs/products.js b/public/viewjs/products.js
index 81b7a008..67ac189c 100644
--- a/public/viewjs/products.js
+++ b/public/viewjs/products.js
@@ -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,58 +50,53 @@ $(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),
- closeButton: false,
- buttons: {
- confirm: {
- label: __t('Yes'),
- className: 'btn-success'
- },
- cancel: {
- label: __t('No'),
- className: 'btn-danger'
- }
- },
- callback: function(result)
- {
- if (result === true)
- {
- jsonData = {};
- jsonData.active = 0;
- Grocy.Api.Put('objects/products/' + objectId, jsonData,
- function(result)
- {
- window.location.href = U('/products');
- },
- function(xhr)
- {
- console.error(xhr);
- }
- );
- }
- }
- });
- }
- 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.') + '
' + __t('Stock amount') + ': ' + stockAmount + ' ' + __n(stockAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural),
- closeButton: false
- });
+ bootbox.confirm({
+ message: __t('Are you sure to delete product "%s"?', objectName) + '
' + __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: {
+ label: __t('Yes'),
+ className: 'btn-success'
+ },
+ cancel: {
+ label: __t('No'),
+ className: 'btn-danger'
}
},
- function(xhr)
+ callback: function(result)
{
- console.error(xhr);
+ if (result === true)
+ {
+ jsonData = {};
+ jsonData.active = 0;
+ Grocy.Api.Delete('objects/products/' + objectId, {},
+ function(result)
+ {
+ window.location.href = U('/products');
+ },
+ function(xhr)
+ {
+ console.error(xhr);
+ }
+ );
+ }
}
- );
+ });
});
+
+$("#show-disabled-products").change(function()
+{
+ if (this.checked)
+ {
+ window.location.href = U('/products?include_disabled');
+ }
+ else
+ {
+ window.location.href = U('/products');
+ }
+});
+
+if (GetUriParam('include_disabled'))
+{
+ $("#show-disabled-products").prop('checked', true);
+}
diff --git a/public/viewjs/quantityunitform.js b/public/viewjs/quantityunitform.js
index 45594380..f61a4ebf 100644
--- a/public/viewjs/quantityunitform.js
+++ b/public/viewjs/quantityunitform.js
@@ -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();
diff --git a/public/viewjs/quantityunits.js b/public/viewjs/quantityunits.js
index af6bb150..504c30fe 100644
--- a/public/viewjs/quantityunits.js
+++ b/public/viewjs/quantityunits.js
@@ -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();
diff --git a/public/viewjs/recipeform.js b/public/viewjs/recipeform.js
index 53244ddd..0df9a397 100644
--- a/public/viewjs/recipeform.js
+++ b/public/viewjs/recipeform.js
@@ -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();
diff --git a/public/viewjs/recipes.js b/public/viewjs/recipes.js
index c8249a2c..ee48b654 100644
--- a/public/viewjs/recipes.js
+++ b/public/viewjs/recipes.js
@@ -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)'
diff --git a/public/viewjs/shoppinglist.js b/public/viewjs/shoppinglist.js
index a8b0714f..35fb577b 100644
--- a/public/viewjs/shoppinglist.js
+++ b/public/viewjs/shoppinglist.js
@@ -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)
diff --git a/public/viewjs/shoppinglocations.js b/public/viewjs/shoppinglocations.js
index bcff8388..abbad22b 100644
--- a/public/viewjs/shoppinglocations.js
+++ b/public/viewjs/shoppinglocations.js
@@ -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();
diff --git a/public/viewjs/stockentries.js b/public/viewjs/stockentries.js
index 0a5d64f5..152c3434 100644
--- a/public/viewjs/stockentries.js
+++ b/public/viewjs/stockentries.js
@@ -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();
diff --git a/public/viewjs/stockjournal.js b/public/viewjs/stockjournal.js
index 2450b03d..4a0fdbe0 100644
--- a/public/viewjs/stockjournal.js
+++ b/public/viewjs/stockjournal.js
@@ -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();
diff --git a/public/viewjs/stockjournalsummary.js b/public/viewjs/stockjournalsummary.js
index de5e2af7..49371773 100644
--- a/public/viewjs/stockjournalsummary.js
+++ b/public/viewjs/stockjournalsummary.js
@@ -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();
diff --git a/public/viewjs/stockoverview.js b/public/viewjs/stockoverview.js
index 1dc8c9b0..ba7f1e5b 100755
--- a/public/viewjs/stockoverview.js
+++ b/public/viewjs/stockoverview.js
@@ -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,
diff --git a/public/viewjs/taskcategories.js b/public/viewjs/taskcategories.js
index ac5e7472..ab798d79 100644
--- a/public/viewjs/taskcategories.js
+++ b/public/viewjs/taskcategories.js
@@ -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();
diff --git a/public/viewjs/tasks.js b/public/viewjs/tasks.js
index 8ee5e391..29cdb4e7 100644
--- a/public/viewjs/tasks.js
+++ b/public/viewjs/tasks.js
@@ -4,7 +4,7 @@
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 },
{ 'visible': false, 'targets': 3 }
- ],
+ ].concat($.fn.dataTable.defaults.columnDefs),
'rowGroup': {
dataSrc: 3
}
diff --git a/public/viewjs/userentities.js b/public/viewjs/userentities.js
index 7abd955d..cf97a39b 100644
--- a/public/viewjs/userentities.js
+++ b/public/viewjs/userentities.js
@@ -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();
diff --git a/public/viewjs/userfields.js b/public/viewjs/userfields.js
index f49c5c9e..d5d12bfc 100644
--- a/public/viewjs/userfields.js
+++ b/public/viewjs/userfields.js
@@ -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();
diff --git a/public/viewjs/userobjects.js b/public/viewjs/userobjects.js
index 80ff9645..e646e1dc 100644
--- a/public/viewjs/userobjects.js
+++ b/public/viewjs/userobjects.js
@@ -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();
diff --git a/public/viewjs/userpermissions.js b/public/viewjs/userpermissions.js
index 6f9907bc..150af021 100644
--- a/public/viewjs/userpermissions.js
+++ b/public/viewjs/userpermissions.js
@@ -24,15 +24,16 @@ $('#permission-save').click(
{
return $(this).data('perm-id');
}).toArray();
- Grocy.Api.Put('users/' + Grocy.EditObjectId + '/permissions', {
- 'permissions': permission_list,
- }, function(result)
- {
- toastr.success(__t("Permissions saved"));
- }, function(xhr)
- {
- toastr.error(__t(JSON.parse(xhr.response).error_message));
- }
+
+ Grocy.Api.Put('users/' + Grocy.EditObjectId + '/permissions', { 'permissions': permission_list },
+ function(result)
+ {
+ toastr.success(__t("Permissions saved"));
+ },
+ function(xhr)
+ {
+ 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");
diff --git a/public/viewjs/users.js b/public/viewjs/users.js
index db38b385..8be42c93 100644
--- a/public/viewjs/users.js
+++ b/public/viewjs/users.js
@@ -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();
diff --git a/services/StockService.php b/services/StockService.php
index 923f853d..809610e9 100644
--- a/services/StockService.php
+++ b/services/StockService.php
@@ -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([
diff --git a/views/components/userpermission_select.blade.php b/views/components/userpermission_select.blade.php
index b42ff6d2..1375ac1d 100644
--- a/views/components/userpermission_select.blade.php
+++ b/views/components/userpermission_select.blade.php
@@ -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) }}