From cf34df5e3f426152b975e25f4ba78c606f76f06b Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Mon, 7 Dec 2020 19:48:33 +0100 Subject: [PATCH] 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) --- changelog/58_2.7.0_2020-04-16.md | 2 +- changelog/60_UNRELEASED_2020-xx-xx.md | 13 ++- controllers/GenericEntityApiController.php | 2 +- controllers/StockApiController.php | 33 ++++--- controllers/StockController.php | 11 ++- controllers/Users/User.php | 4 - controllers/UsersApiController.php | 20 +++- grocy.openapi.json | 16 +++ localization/strings.pot | 20 +++- migrations/0103.sql | 1 + migrations/0105.sql | 6 +- migrations/0110.sql | 6 +- migrations/0120.sql | 25 +++++ public/js/grocy.js | 5 +- public/viewjs/batteries.js | 2 +- public/viewjs/batteriesjournal.js | 2 +- public/viewjs/batteriesoverview.js | 2 +- public/viewjs/chores.js | 2 +- public/viewjs/choresjournal.js | 2 +- public/viewjs/choresoverview.js | 2 +- public/viewjs/components/productpicker.js | 97 ++++++++++-------- public/viewjs/consume.js | 15 +-- public/viewjs/equipment.js | 5 +- public/viewjs/inventory.js | 18 ++++ public/viewjs/locations.js | 2 +- public/viewjs/manageapikeys.js | 2 +- public/viewjs/productform.js | 4 +- public/viewjs/productgroups.js | 2 +- public/viewjs/products.js | 98 +++++++++---------- public/viewjs/quantityunitform.js | 2 +- public/viewjs/quantityunits.js | 2 +- public/viewjs/recipeform.js | 4 +- public/viewjs/recipes.js | 5 +- public/viewjs/shoppinglist.js | 2 +- public/viewjs/shoppinglocations.js | 2 +- public/viewjs/stockentries.js | 2 +- public/viewjs/stockjournal.js | 2 +- public/viewjs/stockjournalsummary.js | 2 +- public/viewjs/stockoverview.js | 6 +- public/viewjs/taskcategories.js | 2 +- public/viewjs/tasks.js | 2 +- public/viewjs/userentities.js | 2 +- public/viewjs/userfields.js | 2 +- public/viewjs/userobjects.js | 2 +- public/viewjs/userpermissions.js | 21 ++-- public/viewjs/users.js | 2 +- services/StockService.php | 57 ++++++++--- .../userpermission_select.blade.php | 6 +- views/layout/default.blade.php | 1 + views/productform.blade.php | 19 +++- views/products.blade.php | 20 +++- views/recipes.blade.php | 5 +- views/stockoverview.blade.php | 8 +- 53 files changed, 387 insertions(+), 210 deletions(-) create mode 100644 migrations/0120.sql 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) }}
@@ -11,7 +12,8 @@ @foreach($perm->uihelper_user_permissionsList(array('user_id' => $user->id))->via('parent') as $p)
  • @include('components.userpermission_select', array( - 'perm' => $p + 'perm' => $p, + 'permParent' => $perm ))
  • @endforeach diff --git a/views/layout/default.blade.php b/views/layout/default.blade.php index aa2c3d46..ca03ee6e 100644 --- a/views/layout/default.blade.php +++ b/views/layout/default.blade.php @@ -677,6 +677,7 @@ + diff --git a/views/productform.blade.php b/views/productform.blade.php index 0c399dcb..bdc2d5c7 100644 --- a/views/productform.blade.php +++ b/views/productform.blade.php @@ -75,6 +75,19 @@
    +
    +
    + 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"> + +
    +
    + @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 '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 diff --git a/views/products.blade.php b/views/products.blade.php index 57944e4a..7b8f1d65 100644 --- a/views/products.blade.php +++ b/views/products.blade.php @@ -71,6 +71,17 @@ +
    +
    + + +
    +
    - - @if($product->active == 0) (deactivated) @endif {{ $product->name }}@if(!empty($product->picture_file_name)) name }} + @if(!empty($product->picture_file_name)) + @endif + title="{{ $__t('This product has a picture') }}"> + @endif {{ FindObjectInArrayByPropertyValue($locations, 'id', $product->location_id)->name }} diff --git a/views/recipes.blade.php b/views/recipes.blade.php index 53a785ad..f1963603 100644 --- a/views/recipes.blade.php +++ b/views/recipes.blade.php @@ -391,7 +391,7 @@ @endif
  • @if($selectedRecipePosition->product_active == 0) -
    {{ $__t('Deactivated Product') }}
    +
    {{ $__t('Disabled') }}
    @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)@elseif($selectedRecipePosition->need_fulfilled_with_shopping_list == 1)@else@endif @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 - + @if($selectedRecipePosition->need_fulfilled == 1 && GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) {{ $selectedRecipePosition->costs }} @endif + {{ $selectedRecipePosition->calories }} {{ $__t('Calories') }} @if(!empty($selectedRecipePosition->recipe_variable_amount))
    {{ $__t('Variable amount') }}
    @endif diff --git a/views/stockoverview.blade.php b/views/stockoverview.blade.php index e2362df2..a636ad2b 100755 --- a/views/stockoverview.blade.php +++ b/views/stockoverview.blade.php @@ -178,7 +178,7 @@ -
    {{ $currentStockEntry->quick_consume_amount }} {{ $__t('All') }} @if(GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING) - {{ $__t('Inventory') }} -